- guide record-search callers to the correct flag or command path without reporting recovery-only flags as invalid input
- reject blank form question IDs before destructive deletion and preserve keep-field request semantics
- cover typed validation and dry-run request contracts, then refresh Base E2E coverage
Co-authored-by: BASE Infra Harness <ai@base-infra-harness.noreply.local>
* feat(vfs): allow absolute paths under a built-in path allowlist
Path flags only accepted paths relative to the working directory, so an
agent passing a full path (typically under /tmp) failed on its first call
and had to retry with a relative one.
Absolute paths are now accepted when they resolve inside a built-in
allowlist: the working directory, /tmp, and ~/files. A built-in denylist
covers system and credential locations and wins over the allowlist,
including over the working directory. Both lists are compiled in and read
no environment variable, flag, or config file, so the effective policy is
fixed by the binary; upgrading is all it takes for the new behavior to
apply.
Containment is decided by file identity (device and inode) alongside the
resolved name, because a single directory has many spellings: APFS folds
U+017F onto "s", so ".sshh" spelled with it opens ~/.ssh, and NTFS and
APFS both compare case-insensitively.
Reads are hardened where the policy applies: O_NOFOLLOW pins the final
component, O_NONBLOCK keeps a FIFO from blocking before it can be
refused, and the opened descriptor is matched against the inspected
object, rejected when it is not a regular file, and rejected when it
carries extra hard links. The relaxed local-input tier used by apps
upload keeps its own contract (symlinks are legitimate arguments there)
and gains the denylist check instead.
Two behaviors are deliberate rather than incidental. Working inside a
denylisted directory now refuses even relative paths, since the denylist
is unconditional. Running as root leaves only the working directory and
/tmp, because the home directory is then /root, itself a deny root.
Existing tests asserted the old "every absolute path is refused"
baseline; they now assert the allowlist. Traversal fixtures escape to the
filesystem root, which stays outside every allowed root on Linux, where
the temp directory that hosts t.TempDir() is /tmp itself.
* fix(vfs): close two paths around the built-in denylist
A "~/..." argument had two readings: validation expanded it to the home
directory, while a caller that keeps the original string — SafeLocalFlagPath
returns it verbatim — opens whatever "~" names in the working directory. A
symlink there carried reads past the denylist, confirmed by reading
/etc/passwd through it. Every interpretation of an argument is now checked,
so the shorthand still reaches ~/files while the literal entry cannot
escape.
With no LARKSUITE_CLI_CONFIG_DIR and no reachable home directory,
core.GetBaseConfigDir keeps credentials in a bare ".lark-cli" resolved
against the working directory, which is an allow root. That fallback is now
mirrored as a deny root, so containers whose home lookup fails do not expose
their stored tokens.
* fix(vfs): enforce hard-link checks across readers
* fix(vfs): stop an output hard link from rewriting a file outside the allowlist
A hard link has no target for name resolution to follow, so a link inside an
allowed root looked like an allowed destination while sharing its inode with a
file outside every root. A caller that truncated the approved name in place
rewrote that outside file: `auth qrcode --output <link>` reported success and
replaced a 43-byte JSON file outside the allowlist with its PNG.
Output validation now refuses an existing target that carries more than one
name, which covers callers that write directly, and auth qrcode commits
through a temp file and a rename, which replaces the directory entry and
leaves the other names alone. Writers already going through FileIO.Save were
never affected, since that path has always committed by rename.
* fix(vfs): give the hard-link refusal a workable recovery hint
The message told the caller to copy the file into an allowed directory, which
answers a question they did not ask: the file that triggers this is normally
already inside one, with every one of its names there too. It now states what
the check actually cannot do — enumerate the other names a file is reachable
by — and offers the step that works, which is to copy the file and use the
copy.
* test(vfs): pick the denylist fixture for the platform under test
Two tests reached for "/etc/passwd" as a denylisted absolute path. That path
is not absolute on Windows, so one test met the foreign-path rejection instead
of the denylist it was asserting, and the other saw the path joined to the
working directory and no rejection at all. Both now ask for a deny root that
exists on the platform running them — the credential directories under the
account home qualify everywhere — which keeps the denylist covered on Windows
rather than skipping it there.
Verified on Windows 10.0.19045 by running the package's test binary from this
branch and from main: main passed, this branch failed these two, and both pass
after the change. The other packages this branch touches were compared the
same way and their Windows results are identical on both sides.
* fix(vfs): state the hard-link check as the condition it tests
The check read as "bail out unless the target can be inspected", which
nilerr reads as an error swallowed on the way out. It now names the case it
acts on — an existing regular file with more than one name — and the comment
carries what the early return used to imply: a target that cannot be
inspected has no link count to judge, and the write layer reports the real
failure with proper typing.
* docs(vfs): scope the policy's environment claim to what holds
The header promised that neither list accepts runtime input and that no
caller controlling the environment can widen them. Two inputs contradict
that: LARKSUITE_CLI_CONFIG_DIR contributes a deny root, and where the account
database cannot name the running uid, $HOME decides where ~/files points —
reproduced in a container running as an unregistered uid, which wrote into a
directory the environment chose.
The comments now state the preference and its boundary rather than a
guarantee, and record what the boundary costs: a directory named "files"
under the named path, with the home directory itself still outside the
allowlist and every candidate home still carrying the credential deny roots.
The trustedHome note also said the pure-Go lookup falls back to $HOME
silently; it does so only when $USER is set as well, and returns an error
otherwise, which drops the ~/files root instead of moving it.
No behavior change.
* fix(auth): keep the mode of a QR output file that already exists
Committing the QR write by rename fixed a hard link from rewriting a file
outside the allowlist, but it also changed what happens to the target's mode.
A rename installs the temp file's inode, mode included, where the previous
in-place write left the existing file's mode untouched. Overwriting a target
the caller had restricted to 0600 therefore published it as 0644.
The mode now comes from the file already at the path; only a path with
nothing at it takes the default. Verified against main, which preserved 0600
here, and covered by a test that fails when the fixed mode is restored.
* test(sheets): move the csv file-alias tests onto the new path baseline
Merging main brought #2559's tests for the --file → --csv alias, written
against the policy this branch replaces. Two of them fail on it, both because
the verdict they describe moved rather than disappeared.
The out-of-tree case used /tmp, which the allowlist now accepts, so the value
came back as a missing file instead of an out-of-tree one; it now names a path
no allow root can contain. The directory case is refused when the descriptor
is inspected, before a read is attempted, so the message reads "not a regular
file". What the caller sees of both — the flag named, the cause kept, stdin
offered — is unchanged.
That message listed the kinds it refuses and omitted directories, which is how
it reached a directory test reading as a mismatch. It now names them.
* fix(im): let the path policy judge a download target
`+messages-resources-download` refused an absolute --output before the shared
policy saw it, so the flag stayed relative-only after the policy learned to
accept full paths. It is the command behind 99% of a reported 1,189 download
path errors in one week, where 97.2% of first calls passed an absolute path
and every later success had switched to a relative one.
The shape checks are gone. Both call sites already hand the result to
ResolveSavePath, which applies the allowlist, the denylist and symlink
resolution, so refusing a shape here decided nothing the policy would not
decide better — an absolute path is now answered by where it points rather
than by how it is written.
The file-key checks stay, and they are what the batch caller relies on: it
embeds the key in the path, and a key carrying a separator is refused as a
malformed key, so a traversal cannot be built from one. Verified against a
real tenant: /tmp and ~/files now save, while ~/.ssh, /etc and a path outside
every root are still refused.
* test(im): pin the download output contract the policy now decides
The dry-run suite listed an absolute path among the values --output must
refuse. That held while the command rejected the shape itself; now that the
built-in policy decides, /tmp is an allowed root and the path is accepted, so
the case asserted a rule that no longer exists.
It is replaced by the two halves of the real contract: an absolute path
inside an allowed root reaches the request, and a path that resolves outside
every root — a parent escape from this working directory, or a denylisted
directory — is still turned down as a validation error naming --output.
* fix(vfs): hold a relative path to the working directory
Accepting /tmp as an allow root gave a relative path somewhere new to go.
A process whose working directory sits under /tmp — CI runners, containers
and agent sandboxes commonly arrange that — could climb out with "../" and
still satisfy the allowlist, because the sibling it landed in was also under
/tmp. /tmp is world-writable, so that sibling can belong to another user or
another session, and the write side commits by rename, which replaces an
existing target unconditionally. The previous policy refused this: it
required every resolved path to stay under the working directory.
Naming a full path and climbing out of the working directory are different
acts and no longer share one verdict. An absolute path is judged by the
allowlist, which is what this branch set out to allow; a relative one has to
resolve inside the working directory, whatever wider root contains it.
The home denylist grows at the same time and for the same reason: the working
directory is an allow root and running from the home directory is ordinary,
so a credential store there is reachable by a relative name unless the list
covers it. It now names the common ones — netrc, git and shell credentials,
kube, docker, azure, gh, gcloud, the language package registries — and the
shell histories, which carry pasted keys as reliably as a credential file.
---------
- preserve explicit false values and validate partial share updates
- add dry-run and deployment-gated live E2E coverage
- document share routing in the bundled Base skill
* feat(base): add --position and statistics number_format to dashboard-block create/update
Add an optional top-level --position flag ({x,y,w,h} JSON, parsed but not
coordinate-validated, passed through as a sibling of name/type/data_config) and
optional statistics data_config.number_format ({formatName,precision}) with
light enum + 0-9 integer validation. Both are backward compatible. Body
assembly is unified in a shared buildDashboardBlockBody helper so DryRun and
Execute stay isomorphic. Adds toIntStrict for strict precision parsing, focused
helper/execute/dry-run tests, an E2E dry-run test, and syncs the lark-base
dashboard + data-config skill references.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* fix(base): validate number_format on update path and add symmetry tests
The dashboard-block-update command parsed data_config but never ran the
statistics number_format check, so an illegal formatName/precision slipped
through locally while create rejected it — violating the SSOT + backend-design
§4.5 promise of CLI-side interception on BOTH paths. Update has no --type flag
(block type is immutable) and intentionally skips strong type validation, so it
now reuses the shared validateNumberFormat sub-validator that
validateBlockDataConfig delegates to, keeping create/update symmetric without
demanding table_name/series on a number_format-only update.
Also: add tests for the --no-validate bypass on create+update, a combined
update carrying position + number_format + name, and extend the DryRun/Execute
body isomorphism assertion to the update path. Clarify the --position flag Desc
that coordinate bounds are advisory (not validated locally or server-side) and
sync the lark-base SKILL.md routing table for --position / number_format.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* fix(base): align dashboard block validation paths
Validate dashboard block JSON consistently across dry-run and execute paths, enforce statistics number_format boundaries, and add layout precision workflow coverage and documentation.
* fix(base): resolve dashboard layout doc contradictions and harden isomorphism test
Follow-up to the --position / number_format feature, addressing review findings.
Docs (SSOT contradictions):
- SKILL.md:135 and lark-base-dashboard.md still told agents that dashboard
shortcuts cannot set x/y/w/h and to offer auto-layout instead, which would
have left --position unreachable through the skill. Both statements are now
scoped to +dashboard-arrange, which genuinely cannot take coordinates.
- number_format was documented as supporting sub-field merge on update. That
contradicts the update Tips and lark-base-dashboard.md's own data_config
rule ("每个传入的字段内部是全量替换"). Documented as whole-key replacement
and made the update Tip example carry formatName back.
- Trimmed both reference sections: dropped the duplicated field table, the
restated validation blockquote, the standalone bash example and the 4-column
comparison table; kept the enum table and the two load-bearing gotchas.
Reformatted the example to the file's multi-line JSON style, and generalized
the 场景 3 --position argument to '{...}' like its neighbours.
Tests:
- The isomorphism check called buildDashboardBlockBody twice with the same
arguments, so it could never fail. Replaced with an end-to-end comparison of
the --dry-run preview body against the body captured from Execute; verified
it fails under single-path fault injection.
- The live workflow now updates to values distinct from the create call and
asserts them on read-back, instead of asserting substrings that the created
state already satisfied. Dropped the position read-back assertion: this
iteration does not contract get to echo coordinates.
- Filled in the two missing --no-validate cells (create data-config, update
position).
Cleanup:
- Deleted the inline DryRun closures; both commands now point at the
dryRunDashboardBlock* functions, matching the DryRun: dryRunX convention used
across the package and removing the second body-assembly site.
- Rewrote the update comment that referenced review-round codenames and an
external design doc section to be self-contained.
* fix(base): keep dashboard dry-run previews free of empty identifiers
Wiring the block create/update commands to the shared dryRunDashboardBlock*
functions routed them through dryRunDashboardBase, which Set all three
identifiers unconditionally. A create preview has no block_id yet, so it began
advertising "block_id": "" — an argument that reads as failed to resolve.
Skip empty values in the shared helper rather than special-casing create, which
also clears the same pre-existing noise from the +dashboard-arrange preview.
Pinned with a test asserting a create preview carries base_token and
dashboard_id and no block_id.
* fix(base): require complete --position objects and close the arrange/position gap
Round-2 review follow-up. Three findings, all one-liners in effect, that
compounded into a real failure mode: an agent told to "move this chart to the
right half" could send a partial position, have it accepted, and silently
resize the block to nothing — with no coordinate read-back to diagnose it.
- --position now requires all four of x/y/w/h. The server fills missing
coordinates with zero rather than leaving them alone, so a partial object is
a resize disguised as a move. Only the object's shape is checked; coordinate
VALUES stay unvalidated (out-of-range, negative and overlapping still pass
through) as documented. The check is semantic, so --no-validate skips it
while the JSON parse still runs — the same split the rest of this command
pair already uses. Rejected the alternative of validating ranges too: that
would contradict the documented dws-aligned pass-through contract.
- +dashboard-arrange's Tips now point at --position. The cross-reference was
one-directional: create/update told agents about arrange, but arrange — the
command an agent reaches for first when asked to "fix the layout" — never
mentioned that exact placement had become possible.
- Documented that coordinates are write-only this iteration. The reference doc
offered "replicate an existing dashboard's layout" as a use case while the
PR itself scopes out coordinate read-back, sending agents to look for x/y/w/h
that get/list do not return.
Also from the same review:
- The dry-run builders no longer discard buildDashboardBlockBody's error. It is
unreachable while Validate parses the same flags first, but returning nil
makes the runner fail loudly instead of previewing a body with a field
silently missing.
- Added precision cases that run through the real command. The existing
table-driven ones decode with UseNumber and hit toIntStrict's json.Number
branch, which production never takes — parseJSONObject uses a plain
json.Unmarshal, so precision always arrives as float64.
- coverage.md now says which four commands rest solely on the credential-gated
live test that has not been executed yet.
- Marked the number_format fallback claim as unverified against the backend.
* fix(base): close the position guard's null hole and the contract drift it left behind
Round-3 review follow-up. Two of these were introduced by the previous
follow-up commit, not by the original feature.
- The --position completeness guard only asked whether the key was present,
and a JSON null key IS present. `{"x":6,"y":null,"w":null,"h":null}` sailed
through the very check meant to stop it — the exact scenario the guard's own
comment describes. Each coordinate must now actually decode as a number, so
null, strings, objects and bools are rejected alongside missing keys. This is
still a shape check: out-of-range, negative and fractional values keep
passing through as documented. The package's neighbours (`cfg["text"].(string)`,
`table_name`) already validate required fields with a type assertion; this
was the one place that did not. Mutation-verified: reverting the assertion
turns the explicit-nulls case red.
- coverage.md claimed `+dashboard-block-get` "reads back position" while the
test it cites deliberately stopped asserting coordinates — a line the
previous commit invalidated and did not update. It now says number_format
only. The `+dashboard-block-update` row also claimed dry-run coverage for
number_format that only the unexecuted live test provides.
- dashboard-block-data-config.md still said the update path does no local
validation, which commit bb7d8fbc made false in this same PR. An agent
reading it would not expect exit 2 and might reach for --no-validate, which
now also disables the position guard.
Also from that review:
- --no-validate's flag Desc only mentioned data_config; it silently covers the
--position check too. Said so, in both commands.
- Four places stated unverified backend behaviour as fact — including a claim
that the server zeroes missing coordinates, which was the guard's entire
premise, and a "backend defaults to digital" line 23 lines above a blockquote
saying that very fallback was unverified. All reworded to what is actually
known; the guard's rationale is now stated in terms of the request we send.
- E2E dry-run assertions were whole-output substring matches (`"w": 6` could
match anywhere); switched to clie2e.DryRunGet path assertions like the
sibling suites, which also lets them prove position is a top-level sibling
rather than nested in data_config.
- Documented that formatName is case-sensitive, unlike rollup which is
normalized — same object, two conventions, worth saying out loud.
- The --position canonical rewrite's comment claimed it kept Validate/DryRun/
Execute consistent; they re-parse anyway. Its real job is folding @file input
inline so the two paths cannot read a changed file. Comment now says that.
- Named buildDashboardBlockBody's bool at the call sites; covered all three
branches of the identifier skip, not just block_id.
* fix(base): stop dry-run previews leaking route templates; finish the unverified-claim sweep
Round-4 review follow-up. Both findings trace back to earlier follow-up commits
rather than the original feature, and both are the same failure shape: fixing
the instance instead of the class.
- 68bdaccd made dryRunDashboardBase skip empty identifiers, but Set() doubles as
the substitution source for :param placeholders in the URL. Skipping a
declared-but-empty identifier therefore printed the raw route template —
`.../blocks/:block_id` — while also removing `"block_id": ""`, the one signal
that told the caller their argument was empty. An agent whose `$BLOCK_ID` did
not expand would see a preview that looks like the CLI failed to substitute,
with nothing pointing at the real cause. The condition is now whether the
command declares the flag, which is what the comment claimed all along: create
genuinely has no block-id, and that is the case worth omitting.
Not fixed here: a declared-but-empty required identifier still reaches the
wire as a request to the collection endpoint (`baseV3Path` drops empty
segments). That predates this PR and spans the whole base package — worth its
own change rather than guarding two commands and leaving nine inconsistent.
- The isomorphism test only compared bodies, so a preview could target a
different endpoint than Execute and still pass. It now compares method and URL
as well, and rejects any leftover ":" placeholder — that is the mechanism that
would have caught the above.
- 82f72540's message claimed all four unverified backend statements had been
reworded; five survived, three of them in `--help`, where the --position Desc
said server-side acceptance was unverified two lines above a Tip asserting
overlaps are not server-checked. All five now match the wording already used
in lark-base-dashboard.md, and the PR body Summary no longer contradicts its
own Known limitations.
The rejected-alternative for the first item: guarding empty required identifiers
in Validate would be the root-cause fix, but applying it to the two commands
this PR owns while nine sibling dashboard commands keep the old behaviour trades
one inconsistency for another.
* fix(base): stabilize dashboard block validation inputs
* docs(base): clarify precise dashboard layout workflow
* docs(base): align dashboard live coverage status
* docs(base): soften absolute dashboard layout phrasing in skill
Replace "run exactly once / stop" wording for +dashboard-arrange and
--position with intent-based guidance (prefer whole-dashboard arrange,
generally no need to re-read position) so the skill routes agents away
from per-block churn and useless retries without forbidding legitimate
user-driven follow-up adjustments.
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* docs(base): clarify dashboard layout guidance
* docs(skills): move dashboard layout guidance to reference
* docs(base): verify dashboard number format defaults
---------
Co-authored-by: wanglei.75 <wanglei.75@bytedance.com>
Co-authored-by: TRAE CLI <noreply@bytedance.com>
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* fix(base): make record history queries explicit and readable
1. Require an explicit confirmed record ID and document deterministic record resolution
2. Add shared-format pretty history output and projection aliases without changing default JSON
3. Reject explicitly non-positive history cursors and cover request, help, formatting,
and dry-run behavior
```ai-signature
改动范围: 将 Codebase MR 1438 的最终十文件差异移植到 GitHub PR 2298,覆盖 record history 命令、record list 投影别名、Base 引导文档与对应单元及 dry-run 回归测试
思考过程: 以 GitHub 最新 main 为基线执行三方内容合并,保留 GitHub 独立演进;删除原 PR 针对 base_history_003 的 Skill 改动,并仅按 GitHub 当前分母重算 coverage 指标
改动原因: GitHub PR 原先承载的是已确认应撤回的单题文档方案,需要由已评审的 MR 1438 最终通用实现完整替换,同时避免复制 Codebase 的多轮提交与 revert 历史
Break Change: 行为 breaking | record history 要求显式确认的 record ID,且显式非正 --max-version 现在返回 typed validation error
```
Co-authored-by: BASE Infra Harness <ai@base-infra-harness.noreply.local>
AI-SHA256: 5bf66267670f435c7d577d2444b61b209322eb5a73f15e47e671a5b8dd2603bf
* fix(base): validate history cursors and NDJSON dry runs
1. Reject missing or invalid next_max_version values before emitting pagination guidance
2. Use the execution page size in NDJSON dry runs and assert the requested output path
3. Cover valid and invalid history cursors and prove a 2000-row request is capped to a 500-row first
page
```ai-signature
改动范围: shortcuts/base/record_history_list.go、record_ops.go 及邻近单元和 dry-run E2E 测试,仅处理 PR 2298 中有效 CodeRabbit 评论,不新增 live E2E 或修改主 Skill
思考过程: 分页提示只接受解码后的正整数游标;NDJSON dry-run 复用执行阶段的五百行页大小,并以两千行请求验证真实截断而非相等值偶然通过,同时直接断言导出路径契约
改动原因: 原实现可能把不可用的服务端游标打印成命令,同时 dry-run 对 201 到 500 条请求报告的首屏大小与真实执行不一致,初版测试也未真正证明五百行上限或 search 输出路径
Break Change: 否
```
Co-authored-by: BASE Infra Harness <ai@base-infra-harness.noreply.local>
AI-SHA256: a362c47d739d6de5676b87431fbc015d1eaa3d47d46d711e2535baa4dad7ffce
* docs(base): simplify record history prerequisites
1. Condense the record history prerequisites into generic record selection rules.
2. Remove the positive and negative examples from the reference.
```ai-signature
改动范围: 仅修改 skills/lark-base/references/lark-base-record-history-list.md,精简使用前置并删除正反例章节。
思考过程: 保留调用前确认同表 record_id、不得自行选择记录或扩展整表扫描的核心约束,移除具体链接、视图位置和命令示例以提升通用性。
改动原因: 用户要求使用前置更精简、表述更通用,并删除正反例;本次不涉及命令行为或测试。
Break Change: 否
```
Co-authored-by: BASE Infra Harness <ai@base-infra-harness.noreply.local>
AI-SHA256: 7e69951d47c1535cd5fe0fa9ebd7018af2e17373e5851c229dac46d68eaefa84
* fix(base): align history guidance with affordance
* fix(base): keep history guidance in the existing reference
1. Remove the new Base affordance file and its Markdown-only tests
2. Keep selection and field quoting guidance in the existing record history reference
3. Restore the pre-existing command tips and retain only behavior-focused regressions
```ai-signature
改动范围: 删除新增 affordance/base.md 与对应 source/help 文案测试,调整 record-history 现有 reference、覆盖说明和原命令 Tips
思考过程: 用户要求不新增 Base affordance 文件且 Markdown 不需要专门测试,因此保留运行时代码测试,把跨命令选行和字段引号说明收敛到已有 reference
改动原因: 新增 affordance 与文案测试扩大了 PR 改动面,并重复承载已有 history reference 的 agent 工作流说明
Break Change: 否
```
Co-authored-by: BASE Infra Harness <ai@base-infra-harness.noreply.local>
AI-SHA256: 4c7e3db10025f412d4b2cb4443d94724b5997b72b7e8f97fafdae2f08633b5a2
* docs(base): clarify record history target selection
1. Describe the record ID as uniquely resolving the user-selected target
2. Avoid implying that users must confirm an opaque internal record ID directly
```ai-signature
改动范围: 仅调整现有 lark-base record-history reference 的一处目标记录选择表述
思考过程: CLI 运行时只要求有效 record_id,agent 工作流要求用户确认目标而不是直接确认内部 ID,因此需要区分产品事实和操作约束
改动原因: 原表述可能被误解为 base-cli 能验证 record_id 的用户确认来源,与实际产品边界不一致
Break Change: 否
```
Co-authored-by: BASE Infra Harness <ai@base-infra-harness.noreply.local>
AI-SHA256: bd6c9584cc38689e1c3d9f26514425e3e38ffe2f2cff7b2bb0c65ff316aaf8dc
* docs(base): preserve serial history queries
---------
Co-authored-by: BASE Infra Harness <ai@base-infra-harness.noreply.local>
* feat(base): add BaseApp workspace, page and block shortcuts
Implement the CLI layer of the BaseApp CLI/OpenAPI protocol design: 17 new
shortcuts covering workspace entities, blank app creation, page CRUD and page
block CRUD, plus skill references and dry-run E2E for each.
The data_config validator moves to a neutral block_data_config.go with chart
logic unchanged; list and richText dispatch are new branches, so dashboard
behaviour is untouched. Command spaces stay separate — dashboard commands never
take --app-token and app block commands never take --dashboard-id. The one
exception is +app-block-get-data, which shares the dashboard endpoint, execute
and dry-run hooks and therefore takes --base-token instead of --app-token.
This phase ships no +app-block-delete and no page arrange command; both the
help text and the skill docs spell out that a block type cannot be changed
after creation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(base): implement app mode shortcuts and list components
* feat: support deleting BaseApp via drive delete
* fix: 修复 workspace scope
* fix: correct BaseApp permission scopes
* feat: 新增 moveIn workspace 逻辑
* feat(base): support multi-datasource data_config for BaseApp charts
BaseApp page charts follow section 8 图表协议 of the App CLI RPC 协议,
which differs from dashboard charts by supporting multiple data sources:
base_token is a single top-level value shared by every source, while
table_name/series/count_all/group_by/filter move into each data_sources[]
element (plus top-level data_source_mode and sort). The per-source value
semantics are identical to dashboard charts, so each data_sources[] element
reuses normalizeDataConfig / validateChartDataConfig; the wrapper only adds
the top-level structure. Dashboard charts keep the flat shape; the list
protocol is untouched.
- block_data_config.go: add normalizeAppChartDataConfig /
validateAppChartDataConfig / validateAppBlockDataConfig
- app_block_create/update: route chart blocks to the multi-datasource
normalize/validate; refresh tips and examples
- reference doc: rewrite the chart section for the multi-datasource shape
- unit + e2e tests: migrate chart cases to data_sources; add a
multi-datasource combo case
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(base): map BaseApp richText block type to the wire type "text"
The rich-text widget's API type is "text" (App CLI RPC 协议 §10), but the
CLI exposes the friendlier "richText" alias and was sending it verbatim, so
the backend rejected +app-block-create --type richText with "type is
invalid". Map richText -> text when building the request body; the
user-facing --type richText is unchanged. Add TestAppRichTextTypeMapsToText.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(base): remove internal protocol doc link/reference from skills
The BaseApp skill references pointed at an internal Lark doc (deep link with
a private token) as the source of truth, which must not ship in this repo.
Drop the link and the doc name entirely from the reference markdown and from
code comments; describe behavior in neutral terms ("服务端协议 / 服务端返回和校验")
instead. No functional change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: create workspace for BaseApp when omitted
* fix(base): align BaseApp block protocol
* fix: 删掉废弃的 workspace-entity-remove
* fix(base): align app get reference response
* fix(base): align app shortcuts with API contract
* fix(base): refine app mode shortcut contracts
* fix: use entity_type for workspace entity filtering
* fix: return workspace URLs for base workspace ops
* fix(base): enforce unique app block names
* fix(base): use chart token for app block data
* 明确baseapp边界,不导向到dashboard-arrange
* docs(base): clarify app copy is unsupported
* docs(base): define unsupported app page operations
* fix(base): align app block text type with dashboard
AppMode 的文本组件此前对外叫 richText,发送时再映射成 wire 上的 text,
而读取方向没有反向映射:写进去用 richText、读回来是 text,同一个 CLI
表面自相矛盾,回填或幂等复建时会被枚举校验拒掉。
统一成 text,与 Dashboard 文本组件同名同义:
- appBlockTypes/isAppBlockType/textBlockTypes 去掉 richText
- 删除 appBlockBody 里的 richText → text 发送期映射
- help、枚举、示例、tips 与 baseapp block data_config reference 同步
- 新增回归测试,确保 richText 不再被接受也不再出现在枚举里
richText 不保留别名:+app-* 尚未随已发布版本对外,无存量调用方。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(base): resolve BaseApp URLs
Migrate the net changes from bitable/base_cli!1308 onto the current BaseApp development branch.
* fix(base): remove app block list type filter
* docs: preserve explicit intent when reusing BaseApp blocks
* fix(base): route +app-block-get-data to base_apps endpoint
Move the shortcut off the dashboard route and onto the dedicated
BaseApp block-data endpoint:
- URL: /open-apis/base/v3/base_apps/:app_token/blocks/:block_id/data
- base_token is passed as a required query parameter per the new IDL
- Refresh --block-id description and tips to list all producers of the
chart_token (create/list/get) and note the cht… prefix
- Update the dryrun test to expect the new URL
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: clarify unsupported BaseApp copy paths
* fix: front-load BaseApp copy stop rule
* fix: surface unsupported PageGroup operations
* fix: preserve PageGroup support boundary
* docs(base): clarify unsupported app block handling
* docs(base): clarify how to read text block content
Text blocks have no /data endpoint; calling +app-block-get-data on
one returns a generic server 500. Point readers at +app-block-get,
whose data_config.text carries the Markdown source.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs: clarify immutable BaseApp block types
* docs(base): correct chart date filter format
* docs(base): explain inaccessible app pages
* docs: require workspace removal lookup
* fix(base): return workspace move-in result faithfully
* chore(base): adapt BaseApp changes to upstream main
* fix(base): align app mode changes with upstream scope
* fix(base): address app mode review feedback
* test(base): assert app data transport error contract
* fix(base): satisfy app mode merge requirements
* refactor(base): align app mode filenames
* docs: fix BaseApp rename guidance
* fix(base): remove unsupported workspace icon
* docs(base): clarify app mode concepts and config reuse
---------
Co-authored-by: weibiao.x <weibiao.x@bytedance.com>
Co-authored-by: zhangbinkai.zbk <zhangbinkai.zbk@bytedance.com>
Co-authored-by: yurunjie <yurunjie.xx@bytedance.com>
Co-authored-by: Codex <codex@example.com>
* feat(base): require --fields on +table-create
A table created without --fields gets the platform default schema. Those
default fields then sit in the table alongside every field the caller adds
afterwards, and no field command removes them all, so the only clean recovery
is to drop the table and start over.
Make --fields required so the schema is declared up front, the way
+base-create already recommends via --table-name + --fields.
- Mark --fields Required on +table-create, and reject blank / non-array /
empty-array values in Validate: cobra's MarkFlagRequired only checks that the
flag was set, so --fields "" and --fields "[]" would still reach the API with
no fields body and fall back to the default schema.
- Validate runs ahead of the dry-run branch, so --dry-run can no longer preview
an invocation the real call would reject.
- Update the lark-base skill, e2e coverage notes and the live e2e helper.
BREAKING CHANGE: `lark-cli base +table-create --base-token <t> --name <n>`
without --fields now fails with a validation error instead of creating a
default-schema table. Callers that relied on create-empty-then-add-fields must
pass the schema to --fields.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(base): pin typed metadata on the invalid-schema rejection
Address review feedback on the +table-create schema validation.
- The invalid-fields-JSON rejection now asserts category, subtype and param
through assertInvalidArgumentValidation, plus the preserved *json.SyntaxError
cause, instead of only asserting that some error came back.
- Document why the missing-flag test asserts cobra's text rather than errs
metadata, and pin that layer boundary: cobra's ValidateRequiredFlags emits a
plain error and the dispatcher types it later (cmd/root_test.go). The test now
fails if that boundary moves, so the weaker assertion cannot silently outlive
its reason.
- Reword the --fields tip and the lark-base skill note: both described the
fieldless path as if it were still reachable through +table-create. They now
say the command rejects omitted / blank / empty schemas up front, while
keeping why the schema must be declared here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(base): enrich +table-create field example and drop redundant tips
The cobra Required declaration and flag Desc already advertise the
--fields requirement, so the tips paragraph restating it (and its
SKILL.md / coverage.md echoes) is dropped. The select-field example
now carries multiple/hue/lightness so agents copy a complete option
shape.
* chore: remove useless example
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(base): clarify form and file operation routing
* docs: clarify complete base role table rules
* docs: clarify base advanced permission status
* docs: clarify base form field lifecycle
* docs: guide base form question creation
* fix(base): address form dry-run review findings
* docs(base): add complete editable role example
* fix(base): validate form question create inputs
Form questions can now carry a visible_rule (display condition) so a question shows only when earlier questions match the rule. The rule shares the exact same structure as the view filter, so extract that structure into a single shared reference (lark-base-filter-condition.md) that both view-set-filter and visible_rule point to.
- create/update shortcuts: document visible_rule in --questions help and transcribe the questions body (including visible_rule) into dry-run output
- document that form question updates use full overwrite semantics and must preserve existing fields via read-modify-write
- skill refs: add visible_rule sections to form-questions create/update, note it is only needed when the user asks for a display condition, and clarify that the shared tuple filter protocol does not apply to data-query filters
- tests: pin flag help, verbatim visible_rule passthrough on create/update/list, and add dry-run E2E coverage
Co-authored-by: yballul-bytedance <273011618+yballul-bytedance@users.noreply.github.com>
Co-authored-by: TRAE CLI <noreply@bytedance.com>
Form submission writes and submits data through a public share link, an
irreversible action that should require explicit confirmation. Reclassify
the shortcut from write to high-risk-write so the runner's --yes gate fires
before execution, matching +form-delete and other high-risk base commands.
Update the lark-base skill docs (--yes on all examples, param table, tips)
and add tests pinning the confirmation gate (unit) and dry-run structure (e2e).
Co-authored-by: yballul-bytedance <273011618+yballul-bytedance@users.noreply.github.com>
* fix: standardize CLI shortcut text in English
- translate Docs create and update help descriptions
- remove localized permission annotations
- replace Chinese examples and fallback text
- use English labels for Docs IM Markdown resources
- update regression tests for English output
* test: strengthen English output contracts
* fix(base): improve dashboard shortcut guidance
* docs(base): refine dashboard funnel guidance
* docs(base): drop redundant block-get audit tip
The 'do not audit every block after creation' hint duplicates the
create-then-suppress-get guidance already in lark-base-dashboard.md,
so remove it from the +dashboard-block-get tips to keep them focused.
* test(base): drop stale block-get audit tip assertion
Commit 778da63a removed the 'do not audit every block' tip from the
+dashboard-block-get source as redundant but left the matching
assertion in TestBaseDashboardHelpGuidesAgents, breaking the unit
test. Remove the stale assertion to realign the test with the tips.
* docs(base): clarify when NOT to use helper table for dashboard blocks
* fix(base): defer record-list --json to framework shorthand (align with main)
* fix(base): reject non-string dashboard sort.order instead of silently defaulting to asc
* docs(base): fix reversed cumulative-funnel direction (suffix sum + assumptions)
* docs(base): scope dashboard-arrange to explicit request or fresh new dashboard
* test(base): pin missing sort.order behavior; clarify --no-validate is raw pass-through
* docs(base): show real CLI envelope {ok,identity,data} for get-data and data-query outputs
* fix: decouple --json shorthand registration from default format injection
* fix: fold --json shorthand into format flag before consumption
* fix: enable --json shorthand for mail +triage and mail +watch
* fix: enable --json shorthand for base +record-list
* docs: document --json shorthand for triage, watch and record-list
* docs: clarify record-list JSON output for script consumption
* docs: guide agents to JSON output for machine consumption scenarios
* test: make test comments self-contained
* test: assert typed error metadata in enum validation test
* docs: correct mail +watch --format default and enum in skill doc
* docs: keep --json shorthand undocumented as a silent fallback
* feat(base): add URL and title resolve shortcuts
* docs: clarify base coordinate resolution
* fix(base): address resolve shortcut ci
* fix(base): format resolved record share hint
* fix(base): simplify record share hint data
* fix(base): use field ids in resolved record data
* fix(base): guide record share resolve to update record
* fix(base): include record upsert example in resolve hint
* fix(base): reject add-record urls in resolver
* fix(base): validate title resolve query length
* fix(base): hide resolve alias flags from help
* fix(base): prefer title flag for title resolve
* docs(base): clarify token resolution wording
* refactor: retire legacy error envelopes and enforce typed contract
Consolidate all command error reporting onto the typed errs.* contract, remove
the legacy error surface that predated it, and tighten the lint guards so the
contract holds across the whole repository going forward.
Every failure now reaches stderr as one envelope shape: a category, an
optional subtype, a human- and agent-readable message, and a recovery hint,
with invalid parameters listed under `params`. The legacy ExitError envelope,
its constructors, and the boundary bridge that promoted untyped config and
authorization errors are deleted, leaving a single path from error to wire.
Predicate commands keep their silent-exit behavior through a dedicated signal
that carries only an exit code.
Infrastructure paths that still emitted ad-hoc envelopes — flag parsing,
unknown commands and subcommands, plugin and policy guards, confirmation
prompts, and auth/config failures — now classify into the same taxonomy.
Business, API, auth, and config exit codes are preserved; the one behavioral
change is that Cobra usage failures (missing required flag, unknown command,
bad arguments) now emit the typed validation envelope and exit 2, matching the
explicit flag and subcommand guards, instead of Cobra's plain-text exit 1.
Enforcement is repo-wide rather than per-path:
- The errscontract guards run by default everywhere instead of through a
migration allowlist, so legacy envelopes cannot be reintroduced anywhere.
- errorlint runs across the whole repository: every error wrap must use %w and
every comparison must use errors.Is/errors.As, so interior wraps stay legal
but can no longer break the chain the typed boundary relies on.
- The errs-no-bare-wrap guard is keyed by structural prefix instead of an
explicit per-domain allowlist, so new shortcut domains are covered without
editing a list. It runs where forbidigo is enabled (the shortcut domains and
the auth/config/service command groups); repo-wide chain integrity for the
remaining command paths is carried by errorlint above.
* test: align cli_e2e success assertions to the ok envelope
The api and service success path now emits the {"ok":true} envelope, so the
cli_e2e workflow assertions that still expected the old {"code":0} shape via
AssertStdoutStatus(t, 0) fail once they run with live credentials. Switch those
workflow assertions to AssertStdoutStatus(t, true); the fake-payload helper test
in core_test.go keeps its code-shape assertion.
* feat(base): add base block shortcuts
* fix(base): use block scopes for base block shortcuts
* fix(base): split base block shortcut scopes
* docs(base): consolidate base block help
* docs(base): simplify block help wording
* test(base): cover base block shortcut execution
* feat(base): filter base block list by type
* docs(base): clarify base block ids
* docs(base): simplify docx block help
* docs(base): refine base block agent help
Every failure on the authentication, authorization, and configuration
path now surfaces as a typed structured error instead of an ad-hoc
envelope. Users and scripts that consume CLI output get:
- a fixed nine-category taxonomy on the wire, each mapped to a
stable shell exit code (authentication/authorization/config = 3,
network = 4, internal = 5, policy = 6, confirmation = 10)
- identity-aware detail fields (missing_scopes, requested_scopes,
granted_scopes, console_url, log_id, retryable, hint) carried
uniformly on the envelope
- a single canonical policy envelope at exit 6; the legacy
auth_error carve-out is retired
- per-subtype canonical message + hint that preserves Lark's
diagnostic phrasing and routes recovery to the right actor:
app developer (app_scope_not_applied), user (missing_scope,
token_scope_insufficient, user_unauthorized), or tenant admin
(app_unavailable, app_disabled)
- wrong app credentials classify as config/invalid_client whether
surfaced by the Open API endpoint (99991543) or the tenant
access-token mint endpoint (10003 / 10014), instead of
collapsing to a transport error or api/unknown
- local shortcut scope preflight emits the same
authorization/missing_scope envelope (identity + deterministic
missing-scope set) used by the post-call permission path, so AI
consumers read the same structured shape from precheck and from
server-returned permission denial
- streaming download/upload failures keep the same network subtype
split (timeout / TLS / DNS / transport) as the non-stream path
instead of collapsing every cause to a generic transport failure
- console_url is carried only on the bot-perspective
app_scope_not_applied envelope (where the recovery action is
"developer applies the scope at the developer console"); the
user-perspective missing_scope envelope drops the field, since
the only actionable user recovery is `lark-cli auth login --scope`
and pointing an end user at a console they cannot modify is
misleading
- bind workflows (Hermes / OpenClaw / lark-channel) flatten dynamic
Type tags to wire 'config' with the original module name kept
as a metric label
All 10 typed errors are cause-bearing, nil-safe on .Error() and
.Unwrap(), and defensively clone slice setter inputs. Four lint
rules (CheckNilSafeError / CheckBuilderImmutable / CheckUnwrapSymmetry
/ CheckBuildAPIErrorArms) lock these invariants on migrated paths.
Introduce a typed error contract framework for lark-cli so in-process
Go callers can branch via errors.As(&errs.XxxError{}) and shell scripts,
AI agents, and protocol adapters can branch on stable JSON type/subtype
fields instead of regex-parsing free-form messages.
Adds:
- Canonical taxonomy under errs/ (9 categories + typed Error structs
embedding a shared Problem, RFC 7807-aligned)
- Centralized Lark code metadata + identity-aware BuildAPIError dispatch
- Typed JSON envelope writer alongside the legacy envelope writer
- MCP / OAuth (RFC 6750 Bearer) projection adapters
- Five CI lint guards preventing ad-hoc taxonomy drift
Backward compatibility: legacy *output.ExitError producers (ErrAPI,
ErrWithHint, Errorf, ErrBare) and business shortcuts that use them
continue to render the legacy envelope unchanged. SecurityPolicyError
wire format and exit code are preserved via a carve-out; taxonomy
migration is deferred to PR 2. Domain-specific business migration is
staged across PR 3+.
Framework-direct paths now return typed *errs.*Error: ErrAuth /
ErrValidation / ErrNetwork emit category literals on the wire
(authentication / validation / network), *core.ConfigError is promoted
at the cmd/root boundary with exit code aligned from 2 to 3, and Lark
API permission denials classified by BuildAPIError exit 3.
At the SDK boundary, WrapDoAPIError preserves any already-classified
error (legacy *output.ExitError or typed *errs.*) so output.ErrAuth
from missing credentials surfaces with the auth category and exit 3
intact instead of being downgraded to a network error. Policy responses
classified by BuildAPIError (codes 21000 / 21001) extract challenge_url
and the canonical hint from the response body, matching what the
auth transport already surfaces at the HTTP layer; non-https
challenge URLs are dropped.
First PR in the feat/error-contract-* series.