1170 Commits

Author SHA1 Message Date
zgz2048 c631597573 fix(base): repair skill references and clarify view creation (#2725)
* fix(base): repair skill references and clarify view creation

* docs(base): remove self-routing guidance and extra embed test

* docs(base): keep view creation examples in command help
2026-09-14 15:56:10 +08:00
zhicong666-bytedance 39aaf9fca0 fix(minutes): enforce deterministic search ordering (#2714) 2026-09-11 15:42:00 +08:00
木杉 0493db0cd1 feat(apps): add +export to download an app's source code as a zip (#2594)
* feat(apps): add +export to download an app's source code as a zip

Adds `lark-cli apps +export` to export a Miaoda app's source as a zip
archive via POST /open-apis/spark/v1/apps/export. Accepts exactly one
locator: --app-id or --meta-token (a creative app's share-link token),
plus optional --checkpoint-id and --output.

The client rejects non-archive responses instead of saving them: an
error envelope (JSON, or any non-archive content-type) is surfaced as an
error rather than written to the output file, using a content-type
allowlist. Includes the +export shortcut, its tests, and lark-apps skill
docs.

* fix(apps): correct +export not-published guidance to match gateway

The gateway reports an unpublished artifact-hosted app as HTTP 200 + JSON
{"code":40901,"msg":"app not published"}, which flows through the envelope
classifier rather than classifyExportErr's HTTP-422 branch. That code is not in
the shared spark table, so the caller got the raw message with no next step.

- annotate the live 40901 path with a publish-first hint and the
  failed_precondition subtype so its taxonomy matches the 422 sibling
- correct the defensive HTTP-422 branch: replace the stale 'code not in git /
  file storage' guidance with the publish-first semantics; share the hint text
  via a const so the two paths cannot drift
- update tests and the skill reference to the real not-published behavior; add a
  negative case pinning that non-40901 envelope codes get no publish hint
- drop an orphaned exportLookup comment left by the earlier path->body change

* fix(apps): drop +export --checkpoint-id until its value source exists

The flag took a checkpoint id but nothing in lark-cli surfaces a valid one:
there is no command to list an app's checkpoints, and the id is a server-side
DB row key. A caller (human or agent) has no way to know what to pass, so an
arbitrary value just yields a server invalid-argument. Removing the flag until
the feature is designed avoids exposing an uncallable knob.

- remove the --checkpoint-id flag, its validator, and the checkpoint_id body
  field; drop the now-unused strconv import
- remove the two checkpoint-specific tests and the flag's use in the dry-run test
- update the skill reference: drop the flag, its example, and the error-table
  mention; reword the commit-vs-sandbox note to not lean on the flag concept

Export still works by app_id or meta_token and always returns the latest commit
(git-form) or latest published build (artifact-hosted).
2026-09-11 14:50:38 +08:00
wanghm-bytedance b67a4e85b1 feat(base): support dashboard NPS config (#2562) 2026-09-11 14:47:50 +08:00
bytedance-zhangbinkai b8b21da3a5 fix: 优化工作流记录相关触发器的 Skill,消除歧义和语义错误 (#2670)
* fix: 优化工作流记录相关触发器的 Skill,消除歧义和冲突

* fix: polish skill

* fix: polish skill

* fix: polish skill

* fix: polish skill

* feat: support development environment overrides

* Revert "feat: support development environment overrides"

This reverts commit 2c550d7248.

* fix: polish skill

* fix: polish skill

* fix: polish skill

* feat: support development environment overrides

* fix: polish skill

* Revert "feat: support development environment overrides"

This reverts commit 5c57dea0c9.
2026-09-11 11:50:52 +08:00
lark-cli-external-pr-digest[bot] 30571b7ba4 chore: release v1.0.95 (#2708) v1.0.95 2026-09-11 00:44:00 +08:00
zhaojunlin0405 fda8d7cdae fix: reduce vulnerable dependencies while retaining Go 1.23 (#2659)
* fix: reduce vulnerable dependencies while retaining Go 1.23

* fix(imageconfig): own the standard-library codec registration

Decode dispatches PNG, JPEG and GIF to image.DecodeConfig, which only
answers for codecs some package in the binary has imported. The package
did not import them; it worked because all five call sites still carried
blank imports left over from calling image.DecodeConfig directly. Those
files no longer mention image at all, so the imports now read as dead
weight and the next tidy-up removes them -- silently for base, calendar
and doc-media, as a hard command failure for sheets +set-cell-image and
docs remote images.

Register the three codecs where they are used and drop the call-site
imports. The guard lives in deptest because that package imports no
codec of its own and can therefore prove the ownership.

* fix(imageconfig): keep WebP dimensions readable when the final pad byte is absent

readWebP required every chunk to fit inside the container *with* its
even-padding byte, and required the container size itself to be even,
before it looked at the chunk at all. A writer that omits the pad after
a final odd-sized chunk, or that counts trailing bytes in the RIFF size,
therefore lost its dimensions -- files golang.org/x/image reads without
complaint. That is a silent downgrade on the base, calendar and
doc-media paths and a hard failure on sheets +set-cell-image and docs
remote images, which surface the decode error to the user.

Separate the two bounds. The chunk payload must lie inside the
container, which still rejects a chunk claiming to reach past it; the
padding byte is only required where it is actually consumed, when
skipping to the next chunk.

Differential against x/image v0.30.0 over 300k mutated inputs: 168450
inputs accepted by both, zero dimension disagreements, and x/image-only
acceptances down from 4806 to 3442.

* test(imageconfig): reach the format readers when asserting error preservation

TestMetadataPreservesReadCause injected its failure at offset 0, which
Decode consumes for the magic bytes before it dispatches. readBMP and
readWebP were never entered, so both could discard the source error and
the test would still pass -- verified by mutation: making readBMP return
errMetadata instead of the read error leaves the old assertion green.

Inject at the first offset each reader requests on its own, and assert
the reader ran by checking the format it reports.

Raised by coderabbitai on internal/imageconfig/metadata_test.go.

* test(deptest): pin the binary's external package surface

Adding a module is visible: go.mod changes and the diff invites a look.
Adding a subpackage of a module already required is not. The diff is one
import line, go.mod is untouched, and the binary silently grows a new
package graph.

That is exactly how golang.org/x/net/idna entered this CLI -- via a
single httpguts import added in #1910 for a header check that turned out
to be redundant -- bringing three x/text packages with it. Nobody looked
until an advisory landed on idna. The enumerated guard added alongside
it only names the three packages already known to be a problem; it
cannot see the next one.

Record the non-stdlib package set of the release binary per GOOS and
diff against it. Replaying the #1910 import against this guard reports
the five packages it added, by name, on all three platforms. Regenerate
with -update-import-surface after confirming an addition is intended.

Also assert golang.org/x/image stays out of both the binary and the test
graph, which is what this branch set out to remove and what nothing
currently guards.

* fix(deptest): read only stdout when recording the import surface

The recorder used CombinedOutput, so "go: downloading ..." notices --
which go list writes to stderr -- were parsed as package names whenever
the module cache was cold for the platform being listed. It passed here
and failed on CI, which had never fetched the windows-only modules:
go-winio, coninput, mousetrap and go-localereader showed up as four
added packages.

Read stdout only, keep stderr for the failure message, and fail loudly
on any line containing whitespace, since an import path never does.

Verified against a cold GOMODCACHE: the download notice lands on stderr
and stdout stays clean.

* fix(imageconfig): ignore the VP8X reserved fields, as the spec requires

readWebP rejected a VP8X chunk whose reserved bits were non-zero: the
two high flag bits, the low flag bit, or the 24-bit reserved block. The
container spec says of each of them "MUST be 0. Readers MUST ignore this
field." Writing a non-zero value is the writer's violation; refusing to
read it is ours.

Reproduced against a real cwebp VP8X file: with any one reserved bit
set, golang.org/x/image reads 37x23 from both DecodeConfig and a full
pixel decode, while this reader returned an error -- which surfaces to
the user as a blocked docs image import or a failed sheets
+set-cell-image.

Keep the 10-byte chunk length and the container bounds, drop the
reserved-field check. The malformed-metadata case that pinned the old
behaviour now covers the chunk length instead.

---------
2026-09-11 00:14:28 +08:00
liujinkun2025 dd8edc738f docs(skills): use wiki node shortcut in guidance (#2702) 2026-09-10 20:21:31 +08:00
xuzhigang 5975fdda2c docs(im): document folder expansion and download guidance (#2633)
Message attachments can be folders, which are not directly downloadable.
Guide expanding them first with `im files folder --recursive` and
downloading the files inside, and point mget's folder note at the SKILL
instead of a raw API path.

Co-authored-by: jackie3927 <271680885+jackie3927@users.noreply.github.com>
2026-09-10 20:01:24 +08:00
zgz2048 7264c59808 fix(base): preserve unsupported record values in NDJSON exports (#2701) 2026-09-10 19:20:01 +08:00
liujinkun2025 dbc1559411 fix(docs): migrate wiki lookups to node_by_token (#2689) 2026-09-10 18:14:35 +08:00
liujinkun2025 4d986eaabd fix(base): migrate wiki lookup to node_by_token (#2699) 2026-09-10 17:52:23 +08:00
liujinkun2025 f0f2fb9293 fix(sheets,slides): migrate wiki lookups to node_by_token (#2696) 2026-09-10 17:19:16 +08:00
wangweiming-01 cac40073ef fix(drive): improve token recognition for download and preview (#2680) 2026-09-10 16:29:26 +08:00
liujinkun2025 5ac7f1a49c fix(drive): migrate wiki lookups to node_by_token (#2682) 2026-09-10 14:21:49 +08:00
zhaojunlin0405 ad8766b616 feat: add lazy API catalog routing (#2232) 2026-09-10 13:29:26 +08:00
bytedance-zhangbinkai 4203560c76 feat: Supports sorting of questions in the Base form (#2598)
* feat: 支持表单题目排序

* feat: support development environment overrides

* fix: 提高 form 和 非 form 链路的复用性

* fix: polish skill

* fix: compress skill

* fix: polish skill

* chore: coverage.md

* fix: 只能用 fieldID 更新表单标题配置

* Revert "feat: support development environment overrides"

This reverts commit 9102c97cf0.

* feat: support development environment overrides

* fix: polish skill

* Revert "feat: support development environment overrides"

This reverts commit 58066605af.

* fix: ut

* fix: ut
2026-09-10 11:44:44 +08:00
zgz2048 9aaedb981b fix(base): make table and field lists fetch all items (#2674)
* fix(base): make table and field lists fetch all items

* fix(base): cap list compatibility parameters at 300

* test(base): update list limit e2e bounds
2026-09-10 11:23:02 +08:00
wanghm-bytedance 27ff2e56bd fix(base): validate dashboard update filters (#2631)
Co-authored-by: TRAE CLI <traecli@bytedance.com>
2026-09-10 11:20:17 +08:00
xiaoxiangyu-123 644d11324a feat(im): add concise message output (#2567) 2026-09-10 10:37:00 +08:00
liujinkun2025 4fddd6bc37 fix(wiki): migrate mutation lookups to node_by_token (#2676) 2026-09-09 19:54:58 +08:00
SunPeiYang996 9a29abeac0 fix(docs): resolve draft resources and validate explicit constraints (#2675)
* fix(docs): resolve draft resources and validate explicit constraints

* test(docs): assert validation metadata and preserved causes
2026-09-09 19:08:56 +08:00
liujinkun2025 52a7c152bf fix(wiki): resolve node-get through node_by_token (#2665) 2026-09-09 15:42:32 +08:00
SunPeiYang996 0ee59b8d94 feat(docs): await asynchronous document creation (#2641)
* feat: poll async document creation tasks

* feat: opt document creation into async promotion

* feat: surface log id on async document creation failures

A task that ends in "failed" arrives inside a successful HTTP envelope,
so the caller builds the error itself and dropped the x-tt-logid that
support escalations need. The create response's log id is carried in
too, covering a task that already failed before the first poll.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(doc): keep async document creation synchronous to callers

* feat(doc): add internal creation timing diagnostics

* test(docs): verify async polling before resource uploads

* refactor(docs): keep creation log IDs out of shared runtime

* test(docs): cover creation client and empty-response errors

* fix(docs): simplify async creation failure recovery

* fix(docs): shorten creation timeout guidance

* fix(docs): validate creation recovery command examples

* fix(docs): clarify async timeout recovery and cover unknown status

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-09 15:42:01 +08:00
kiraWangRuilong 7a6a4dbdf5 fix: handle credential and config read failures (#2583)
* fix: preserve credential and config read failures

Report corrupt or inaccessible stored credentials as typed errors instead of treating them as missing. Preserve config load failures across auth diagnostics and keep trusted request metadata available when host signal collection is unavailable.

Fixes #1925

* fix(auth): attach re-authorization recovery to corrupt stored token errors

A stored token that fails to decode or fails semantic validation is now
reported as internal/storage but carried no recovery hint, so `api --as
user`, `auth check`, `auth status`, and `doctor` all ended in a dead end.
A new login overwrites the damaged entry, so attach the canonical
user-authorization recovery on the two read-side corruption branches.
The write-side validator stays hint-free: a rejected write leaves nothing
on disk to re-authorize.

`auth status` keeps the storage failure in `note` and appends the recovery
instead of replacing it.

Tests pin the hint on GetStoredToken, GetValidAccessToken, identitydiag
(including reduced-distribution projection when auth login is concealed),
auth check, and auth status, and pin that SetStoredToken replaces a
corrupt entry.

---------
2026-09-09 11:15:53 +08:00
chenxingyang1019 1a6ac671d1 fix(apps): classify db failures by their k_dl subcode (#2650)
The db OpenAPI collapses every dataloom business failure into one of two numeric
codes by the first digit of the upstream 7-digit code, so a dozen unrelated
states share code 400002476 and the specific reason survives only as a
`k_dl_<digits>` prefix on the message. Classifying on the numeric code therefore
cannot distinguish them, and all of these surfaced as subtype "unknown" with a
per-command hint that described the wrong problem — enabling audit on a
multi-env app's online branch was advised to "verify --app-id and --table", both
of which are correct, and an import whose cell value did not fit its column type
read as a server-side failure worth retrying when only the file can be fixed.

Classify on the subcode instead, keyed on the subcode string rather than on
where it was read from, so exposing it as a structured field later changes only
the extraction step. Six subcodes observed on the wire are mapped; unmapped ones
keep today's behaviour and their prefix, which is the only remaining clue for a
case the CLI does not know.

Turning table audit on or off also makes the server initialize the app's
data-sync task under a workspace-wide lock, and a concurrent holder is reported
as a failure rather than waited for. That specific failure is now retried a
bounded number of times with jittered backoff, and the wait honours the command
context so a cancelled command cannot issue another audit write. Safe to repeat,
unlike writes in general: the server rolls its own changelog write back before
returning, so a contended call leaves audit off rather than half-applied, and a
repeat either succeeds or reports "already enabled".

Matching requires the lock marker as well as the subcode, because that subcode
covers a second cause — a sync task stuck in a terminal state — which repeating
the call does not resolve. The two are kept apart all the way to what the caller
sees: contention is reported as a collision with a concurrent request and marked
retryable, while the unrecognised variant keeps neutral wording and no retryable
flag. Only contention has been observed to clear on its own, and promising
otherwise would walk an agent through a round of attempts that cannot work
against a write endpoint.

The numeric code is left as the server reported it, since the troubleshooter URL
in the same envelope is keyed on it. Message falls back to per-entry wording when
the server sends a subcode with no text: assigning the empty remainder would
blank Message, which errs.Problem requires and whose absence makes Error()
return "".
2026-09-09 10:23:07 +08:00
bytedance-zhangbinkai 1e91c56288 fix: list workspace entity 上限调整为 30 (#2646) 2026-09-08 15:39:53 +08:00
zhengzhijiej-tech 4120a6b47c feat(sheets): add chart sizing and quality checks (#2504)
* fix(sheets): preserve chart axes and clarify label defaults

* docs(sheets): refine chart display defaults

* feat(sheets): support per-series chart data labels

* Revert "feat(sheets): support per-series chart data labels"

This reverts commit fc7a9cc745cf4963447b0682ce2df023994bf70d.

* refactor(sheets): use sparse scatter markers for point labels

* fix(sheets): validate chart data sources in layout check

* feat(sheets): support per-series chart data labels

* Revert "feat(sheets): support per-series chart data labels"

This reverts commit fc7a9cc745cf4963447b0682ce2df023994bf70d.

* fix(sheets): expose raw cell values for chart checks

* feat(sheets): support aggregate categories flag

* feat(sheets): add chart quality and size helpers

* fix(sheets): refine chart data label handling

* fix(sheets): refine chart sizing recommendations

* fix(sheets): deduplicate aggregated category labels

* fix(sheets): sync refined chart sizing guidance

* docs(sheets): sync dense data label guidance

* fix(sheets): align chart size label default

* fix(sheets): remove dense data label advice

* docs(sheets): clarify chart labels and category axis

* fix(sheets): detect overwide charts in quality checks

* fix(sheets): detect unbound secondary axes

* fix(sheets): sync chart quality guidance

* fix(sheets): bound chart source sampling and align schemas

* fix(sheets): validate chart source types without cells-get extensions

* fix(sheets): address chart quality review feedback

* fix(sheets): harden chart source and axis sizing checks

* fix(sheets): address chart quality review findings

* fix(sheets): keep bar Top-N threshold above 24 categories

* fix(sheets): align chart examples with size minimums

* fix(sheets): detect positionless continuous X axes
2026-09-08 14:45:22 +08:00
xiongyuanwen-byted c2afcce1b5 feat(sheets): accept the payload spellings the 08-29..31 reflow rejected (#2611)
* feat(sheets): accept the payload spellings the 08-29..31 reflow rejected

The 08-29..31 reflow report attributes 2839 failures across 31388 sheets
calls, and a large share of them carry well-formed intent in a spelling the
flag does not document: a numeric column whose cells arrived as JSON strings
(155), a row cut short at its last non-empty cell (25), `columns` written as
one object per column (35), the sub-sheet list without its {"sheets":...}
envelope (10), a quoted font_size (30). Each rejection cost a retry that
rewrote the same data differently, and each shape has exactly one reading.

Accept them, on the acceptance-layer contract already stated in
style_vocab.go: one canonical form documented, a wide layer undocumented, and
no rewrite whose meaning is in doubt. A non-numeric string in a numeric
column, a row wider than `columns`, and a column-less sheet that does carry
rows all still fail. `dtypes` additionally reads the writer's own type
vocabulary (number, date), which used to fall through to the string default
and silently write figures as text -- a wrong result rather than a rejection,
so it never surfaced as an error at all.

Three more commands get the same treatment. --properties on
+cond-format-create renames `operator` to compare_type under the {value} /
{text} shapes, canonicalizes symbol and abbreviation comparisons, stringifies
a numeric threshold to match the schema, joins a two-element between list,
and lifts a bare attrs object into the one-entry list; rules whose own
contract spells `operator` keep it. +csv-put answers to --data / --content /
--csv-file, and a path-shaped --csv value naming a real file is read as
@<path> would, reported in the result's warnings rather than silently --
requiring the prefix there was an inconsistency of that flag's own making,
since every sibling path-valued flag takes the same value bare. --font-line
takes the CSS text-decoration words.

Where a fix would have to guess, the error carries the fix instead:
+cells-get rejects an Excel multi-area range client-side with the enclosing
rectangle spelled out (54 rejections the backend answered with a bare
"invalid range"), and row_height / col_width / wrap / unmerge_cells inside a
cell_styles item name where they really live.

Everything stays inside shortcuts/sheets. Deliberately unchanged: shrinking
--range to fit --cells (the dimension check documents why), chunking a
--styles spec past the 100-operation cap (it would drop the atomicity of one
batch_update), and the server-side merged-region conflicts.

* feat(sheets): answer the reflow report's full error breakdown

The 08-29..31 reflow report gained two sections: a 58-row table covering
every long-tail command, and a per-command breakdown of the "other" buckets
its first pass had summarized as a count. Together they name ~1000 failures
the earlier commit could not see. This works through them on the same rule:
accept what has one reading, prescribe what does not, and leave alone what
needs a guess.

The one defect among them is on --cells. A cell carrying style fields at its
top level ({"value":"x","font_weight":"bold","border":{…}}) passed every
client check and reached the backend verbatim, which answered
`[cells[0][0].border] unexpected property "border" is not defined` -- 33
rejections on payloads the --styles path accepts, which is exactly the
vocabulary-parity break style_vocab.go's contract forbids. Style fields now
fold into the carrier that holds them (border into border_styles, scalars
into cell_styles) and their values canonicalize on the same pass, so a
boolean word_wrap no longer dies on the schema's "expected type string" one
step later. A key this domain does not know is still passed through: the
tool contract may gain fields between builds.

Accepted, each with the rejection count it removes: the two object spellings
of --writes, the {"writes":[…]} envelope and a lone write object (54);
`type` as the line-kind slot inside a border spec, which is the Lark OpenAPI
and openpyxl spelling (29); a blank column heading, which is a spacer column
or the cells under a merged title (13); blank text in a date column, mirroring
the numeric rule (7); `custom` as a row/col sizing type when the op carries a
size (Excel's and Lark's own UI word); and cell-style vocabulary inside a
conditional-format rule's style block, where background_color / font_color /
font_weight / font_line fold onto back_color / fore_color / font /
text_decoration (9).

Prescribed, where the fix changes the shape: --position on +dim-delete, whose
sibling +dim-insert does take it (18); --ranges on +cells-unmerge (7);
--include on +csv-get (3); the sheet selectors on +styles-put; --payload on
+table-put; a whole-column or whole-row range where a cell style needs a
rectangle, naming row_sizes / col_sizes as the carrier that does take that
form (28); and border keys written on a styles item instead of a cell_styles
entry. Four invented subcommands (+cells-get-style, +dropdown-list, +meta,
+sheets-list) name the command they meant.

Renamed silently, where the command already has the input under another
spelling: --title on +workbook-import (16), --file / --outdir on
+workbook-export (15), --replace on +cells-replace (7), --output on +csv-get,
plus --sheet-name on +sheet-create and --new-title on +sheet-rename.

Finally, the Excel multi-area --range moves from +cells-get's own Validate to
the shared --range chain: 5 more of those landed on +csv-get and 4 on
+cells-set-style, so the habit is not specific to reads. It is chained after
the sheet-prefix rewrite, so a sheet name containing a comma cannot be read
as several areas.

* feat(sheets): retry transient reads, and answer three stale-state failures

Four fixes from the reflow report's long-tail table that need no protocol
decision, in descending order of the rejections they remove.

A read tool call is now reissued up to twice on a transient failure. The
report's single largest cause on +csv-get was "API call failed: server time
out error" -- 25 of its 71 rejections -- with more on +cells-get and
+workbook-info, each on a command that was written correctly and succeeded
when the agent reissued it by hand. Reads only: this API has no idempotency
key, so a create that timed out after the backend committed it would be
committed twice, which is why the shared RetryTransport is installed with
MaxRetries at 0 and why this sits where the read/write classification is
already known. A rate limit is excluded even though the classifier marks it
retryable -- the server is asking for less traffic, and a fixed sub-second
backoff answers that by sending more.

+filter-create accepted its own documented contract only in the flag
description. "The flag is optional overall -- if omitted, an empty filter is
created on --range" was false: `rules` is required at the properties root, so
omitting --properties failed with `required property "rules" is missing`, an
error about a flag the caller deliberately did not pass. The empty rule set
is now the default, which is what a filter with no column conditions is.

A "not found" on an id-addressed update or delete now points at the list
command that reports the live ids. The backend answers with the id alone
("conditional format iXGbyDwC not found"), which reads like a transport
problem rather than a stale reference; +cond-format-update and
+cond-format-delete contributed 14 rejections between them, and the same
prescription covers every id-addressed object.

Not done here, and not because they are hard: the ~480 rejections behind the
four protocol decisions (a --title default, shrinking --range to fit --cells,
merged-region conflicts, chunking past the operation cap) are the caller's
call, not mine; the ~185 PowerShell argv failures are destroyed before the
process starts and can only be answered by the Windows reference; and write
retries need an idempotency key from the backend.

* feat(sheets): narrow an oversized --range, prescribe merge conflicts, split large style specs

Three of the four decisions the reflow report left open, as decided by the
caller. The fourth, defaulting --title from the first sub-sheet name, is
declined: the title is what the workbook is called in Drive, and deriving it
would produce a shelf of spreadsheets named after their first tab. That one is
answered in the skill reference instead.

--cells that FITS inside --range now narrows the write to the payload instead
of failing. The anchor is unchanged, so every cell lands where the caller put
it, and no cell outside the payload is touched -- the stated range only ever
said how far the caller thought the payload reached. The dominant shape is a
one-cell title against the range it will occupy once merged, where writing the
top-left is what a merged region needs anyway. A payload that OVERFLOWS its
range is still rejected: growing the range would write over cells nobody
named. The narrowing rides back in the success envelope's warnings, because
the caller did state an extent. 122 rejections.

A merged-cell rejection now carries the commands that resolve it. The backend
names the obstacle -- the top-left of the region a write landed inside, or the
0-based bounds of the region a merge would overlap -- but never in A1
notation, and never with the command that clears it; "[0,0-0,6]" becomes
`+cells-unmerge --range "A1:G1"`. The message is read, never used to rewrite
the request: auto-redirecting a write would put data where the caller did not
ask for it, and auto-unmerging would discard a merge nobody agreed to lose. A
parse that finds nothing adds no hint. 193 rejections, which stay failures but
become one-retry failures.

+styles-put splits a spec past the per-request cap instead of refusing it. The
100 was never a server contract -- it is our own materialization guard -- so
the fix is to keep each request at that known-good size and send several,
which is the same license coalesceStyleStamps already takes when it fuses
adjacent stamps and the same thing +table-put does when it slices a large
write. The whole-spec ceiling moves to 1000, the request count rides in the
result, and a mid-way failure names the chunks that already applied and says
re-running the whole spec is safe. --writes keeps its cap: it is one atomic
batch_update by contract. 48 rejections.

* fix(sheets): make the payload prescriptions correct on PowerShell

Every composite flag in this domain takes its payload three ways -- inline, as
a relative @file, or on stdin -- and the prescriptions named the third one in
a spelling PowerShell does not have. `--cells - < cells.json` fails there with
"The '<' operator is reserved for future use": an error about the shell, on a
line this CLI told the caller to run. The pipe is no better, since PowerShell
5 re-encodes non-ASCII on the way through and turns a CJK payload into the
same invalid JSON by another route; the windows-compat skill reference rules
both out, and this code was contradicting it.

On windows every prescription now lands on @file, quoted -- a bare @ opens a
splatting expression -- and says why inlining failed in the first place: the
shell splits a JSON argument on the quotes and commas inside it, and single
quotes do not prevent that. The caller quoted the argument and has no reason
to suspect the quoting, which is what made these 89 rejections cost more than
one retry apiece. POSIX shells keep the redirection form they do have.

The other half of that class arrived as "positional arguments are not
supported": once the quote stack is split, the tail of the JSON is a
positional argument. The framework's message ("pass values via flags") is
correct and useless there -- the caller DID pass a flag -- but it is one
message for every command on every platform, so the cause is named here
instead, where the domain knows both the shell and which of its flags carry a
payload big enough to split. Chained onto cobra's Args the same way the
unknown-flag and enum rewrites chain onto their hooks; non-windows keeps the
framework wording, where a positional argument usually is one. 35 rejections.

* fix(sheets): answer the review of the reflow leniency PR

Nine defects the review found in the acceptance layer, plus the lint /
deadcode failure that blocked CI.

Silent wrong values. A `between` threshold list of any length was joined
into a comma string, so one or three thresholds passed the local shape
check and failed at the backend. The literal "null" decodes into any
destination without an error and leaves it at the zero value: in a
numeric column it marshalled back out as 0, and under `font_size` it
became a zero-point font. Both now reject it. `outer` and `all` in one
border spec dropped `outer` even when the two named different boxes,
applying half the caller's intent; a duplicate is still folded away, a
conflict now reaches the invalid-side check.

Rejections the acceptance layer meant to absorb. The multi-area `--range`
prescription took its rectangle from the first and last area, so
"A3,J3,G3" prescribed "A3:G3" and dropped J3; it now spans every area on
both axes. `type:"custom"` was rewritten to pixel only when the op
carried `size`, so the `height` / `width` alias the same parser accepts
everywhere else failed the enum check first.

False reports. `+cells-set` reconstructed the stated range without the
payload, so every bare `--range A1` with a multi-cell payload reported
itself as narrowed. `+table-put --dry-run` skipped the style expansion in
append mode, hiding a style-only write the execute path performs.

Contract. `+styles-put` still promised one fail-fast batch request after
gaining chunking, and its partial-failure advice called a whole-spec
retry safe — replaying an applied `merge_cells` is rejected as an
overlap. Both now state that a large spec can partially apply, and the
merge route is to read back and resend only what did not land.

The windows positional-argument annotation flattened the framework's
error to text; it keeps it as the cause, and the annotation is split out
so its typed shape is asserted on any host rather than only on windows.

CI: `payloadStdinForm` was unreachable — only its `goos` variant is used.

* fix(sheets): answer the second review round

Six findings, one of them a revert.

P1, --csv. A path-shaped value naming a real file was read as @<path>
would. That makes the flag's meaning depend on the working directory: a
caller writing the literal "./data.csv" into a cell uploaded a same-named
local file instead, and learned of the substitution only from a warning
that arrived after the remote write. File intent has to be explicit, so
the read is gone and the existing prescription -- @<path> or stdin --
stands on its own again. The substitution machinery goes with it.

--outdir and --output-dir were aliased onto --output-path, which reads a
value as a directory only when one already exists there. "--outdir
./exports" with no ./exports therefore wrote a file named "exports".
Both spellings say directory in their own name, so they now carry that
intent through the alias-provenance channel already built for +csv-put's
--file, and applyWorkbookOutputPath honors it without probing disk.

Range narrowing reached only the standalone +cells-set. The narrowing
happens in the shared input builder, so a --writes item or a
+batch-update sub-op shipped the shrunken range in silence. The builder
now returns the note fitCellsRange already produced, and all three paths
report it; the standalone path stops reconstructing it from its flags.

A conditional-format style folded any existing `font` value that was not
the incoming word into "bold italic", so {"font":"normal",
"font_weight":"bold"} became valid input with a slant nobody wrote. Only
bold and italic combine now; anything else keeps its value for the schema
to reject.

The stale-object-ID hint matched any "not found", including one about the
sheet selector, and told the caller to refresh rule ids when the ids were
the one input that was right. It now requires the message to name the id
the command addressed, and skips sheet/workbook-scoped misses.

P3: a first-chunk failure in +styles-put reported "requests 1-0 already
applied". That case says nothing landed instead, without the merge
caveat, which does not apply when the sheet is untouched.

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(sheets): answer the third review round

Three follow-ups on the previous round, one of them a correction to it.

The narrowing note reached only the success envelope. A batch is
fail-fast but not transactional, and a transport failure leaves a single
write's outcome unknown, so a narrowed write can be on the sheet after an
error. Both branches now attach the note to the failure as well; a caller
reconciling against the range they stated rather than the one written is
exactly the confusion the note exists to prevent.

A `font` already reading "bold italic" sent the redundant flat spelling
along with it: the previous round stopped combining unrecognized values
but reached the same branch for this one, so the alias was left in the
outgoing style and the schema had no field for it. It is dropped now,
while a genuinely unknown `font` value still keeps its flat sibling
visible for the schema to reject.

The first-chunk case overcorrected. A failed FIRST request does not mean
an untouched sheet: operations before the failing one inside that request
stay applied, and a timeout settles nothing at all. Claiming otherwise
sent the caller into a whole-spec retry that can replay an applied
merge_cells. Only the backend's own "0 succeeded" now earns that message
-- the same test flattenToolErrorMsg uses, so the two cannot disagree
about what a failed batch left behind. Every other first-request failure
keeps the read-back route and the merge caveat.

Co-authored-by: TRAE CLI <traecli@bytedance.com>

---------

Co-authored-by: TRAE CLI <traecli@bytedance.com>
2026-09-07 21:29:15 +08:00
lark-cli-external-pr-digest[bot] f065bf5b64 chore: release v1.0.94 (#2636) v1.0.94 2026-09-07 21:22:22 +08:00
dc-bytedance fdc1ba4df7 feat: use remote scopes.json for login scope recommendations (#1799)
* feat: use remote scopes.json for login scope recommendations

Switch auth login's recommended-permission source from the compiled-in
local table to a scopes.json fetched from the platform at login time.

- add internal/auth/remote_scopes.go: brand-addressed GET with a ~1s
  timeout and whole-file (binary) validation; scopes are used verbatim
  when the file is valid, and any fetch/parse/format failure falls back
  silently to the existing local computation
- login.go: fetch remote scopes once per login and use them for domain
  validation, all-expansion, and per-domain scope selection; drop the
  terminal interactive page and the local auto-approve filter chain, so
  --recommend is now equivalent to --domain all
- remove login_interactive.go and the service-description getters left
  orphaned by the interactive-page removal
- keep the three entry flags (bare login / --recommend / --domain all)
  as an equivalent transitional surface

* refactor: remove orphaned auto-approve loader and harden scope check

* fix: accept variable segment counts in remote scope validation

* feat(auth): add support for status message from device flow auth

* fix(auth): remove message field from login warning payload

* fix(auth/login): improve login result heading and warning hint logic

* fix: resolve auth scopes locally for custom builds and exclusions

Remote-first login (reading the published scopes.json) can drop scopes
that a specific build or the local resolution still legitimately covers.
Two cases are now handled locally:

- A build that injected business commands via WithCommandSets has a
  scope universe the standard-CLI scopes.json does not cover. Detect it
  by comparing registered command paths against the built-in set and
  skip the remote fetch, so such a build resolves locally instead of
  silently losing its custom scopes.
- Fold the local domain resolution into the --exclude validation
  universe. Once the server drops a batch-withheld scope
  (im:message.send_as_user) from the published list, the remote
  candidate set no longer carries it, but --domain im --exclude
  im:message.send_as_user must stay a valid no-op; a domain that never
  had the scope still rejects it as unknown.

Also raise the remote fetch timeout from 1s to 2s, and make the auth
tests hermetic by defaulting the remote fetch to unavailable in
TestMain (a test that needs a specific remote overrides the seam).

---------
2026-09-07 20:19:53 +08:00
hugang-lark 5d6bf9fa3b fix: optimize calendar and vc (#2616) 2026-09-07 16:37:31 +08:00
liuxin-0319 1f19e17093 chore(slides): sync XML schema (2026-09-01) (#2589)
Co-authored-by: liuxin-0319 <294690258+liuxin-0319@users.noreply.github.com>
2026-09-07 15:55:12 +08:00
yangr-happy 3e8b6c6659 Add mail thread management shortcuts (#2370)
* feat: add mail thread manage shortcuts

* Validate mail thread batch size locally

Reject thread management requests with more than 20 unique thread IDs before making an API call. This mirrors the server-side batch limit and keeps the CLI error in the validation path.

* docs(mail): preserve message shortcut guidance

* feat(mail): expose add-folder thread flag

* docs(mail): tighten thread shortcut guidance

* docs(mail): localize thread shortcut sections

* docs(mail): align thread shortcut headings

* test(mail): cover thread management review gaps

* fix(mail): harden thread manage validation

* fix(mail): skip thread label folder prevalidation

* test(mail): cover thread shortcut registration

* docs(mail): localize thread management skills

* docs(mail): tighten thread skill guidance

* docs(mail): trim thread modify guidance

* docs(mail): remove redundant output note

* fix(mail): align thread batch management output

* docs(mail): simplify thread output wording

* fix(mail): remove unused thread folder scope

* docs(mail): remove thread raw endpoint preface

* fix(mail): support bot thread shortcuts

* docs(mail): keep thread skill updates minimal

* docs(mail): minimize mail template bot note

* docs(mail): align bot identity guidance

* fix(auth): mint bot token from env app secret

* Revert "fix(auth): mint bot token from env app secret"

This reverts commit 648460e00dfce136df1dbe3b7448fa52ad6d31a4.

* fix(mail): tighten thread management guidance

* docs(mail): avoid repeating identity support

* docs(mail): keep read identity guidance focused

* fix(mail): simplify thread receipt label error

* docs(mail): avoid repeating shortcut identity details

* docs(mail): simplify thread shortcut guidance

---------

Co-authored-by: yangr-happy <301323675+yangr-happy@users.noreply.github.com>
2026-09-07 15:18:23 +08:00
xuzhigang 7fd6ef3c07 feat: expand folder children one level in IM message output (#2606)
mget / list / search / thread list 读到 folder 消息时,展开一层子项渲染进
folder 标签(cap-10 + has_more + child_count);sub-folder 不递归只带
child_count 深度提示。converter 支持 prefetch 缓存复用 + 并发安全。

1.改动原因
消息内 folder 附件此前只渲染单行标签,用户看不到内容也无法直接取到
子文件 key 去下载;本次在渲染层展开一层子项,sub-folder 保留 key 供
im files folder 继续展开。

2.影响范围
shortcuts/im 消息渲染链 + lark-im skill 文档

| 文件 | 函数 | 改动前 | 改动后 |
|------|------|--------|--------|
| convert_lib/misc.go | folderConverter.Convert | 单行 <folder/> | 展开一层(cap-10/has_more/child_count),folderWarnf 并发安全告警 |
| convert_lib/content_convert.go | ConvertContext | — | +FolderChildren prefetch 缓存 |
| convert_lib/merge.go/text.go/thread.go | 渲染 | 无展开 | 接入 folder 展开 + prefetch |
| im_*(list/mget/search/threads) | 命令 | — | 单次 prefetch 复用 |
| folder_test.go | 单测 | — | C1-C6 覆盖 |
| skills/lark-im/references/*.md | 文档 | — | folder 展开/下载指引(用 im files folder,非 raw GET) |

3.是否引入测试
是(folder_test.go 渲染单测;convert_lib 全绿)

4.是否申请ACL
不需要
2026-09-04 18:34:52 +08:00
calendar-assistant 6956ac2eab feat(calendar): remove app_link from event outputs, emphasize share link (#2618)
Drop the app_link field from +get and +search-event outputs so calendar
commands no longer surface applink URLs. Emphasize in the lark-calendar
SKILL that sharing an event to a person, chat, or document requires the
event share link (via events share_info), not an applink.
2026-09-04 13:48:32 +08:00
bubbmon233 0cf8ae81c1 feat: add mail rule shortcuts (#2327)
* feat: add mail rule shortcuts

* fix: suggest mail rule alias corrections

* Fix rule update name flag

* fix: harden mail rule update handling

Change-Type: ci-fix

* fix: preserve mail rule update raw condition fields

Change-Type: ci-fix

* test: cover mail rule shortcut branches

Add regression coverage for mail rule JSON inputs, update preservation, toggles, reorder, and parser errors.

Change-Type: ci-fix

* fix: address mail rule review feedback

* fix: align mail rule delete confirmation

* fix: harden mail rule shortcut writes

Change-Type: ci-fix

* fix: align mail rule confirmation risk

* test: remove stale rule create yes flags

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix: harden mail rule shortcut review handling

Address review feedback for rule action parameter whitelisting, raw update body construction, bot mailbox validation, delete confirmation summaries, and parser limits.

Change-Type: ci-fix

* fix: align mail rule shortcuts with final design

* docs: fix mail rule shortcut summary

---------

Co-authored-by: bubbmon233 <272202079+bubbmon233@users.noreply.github.com>
Co-authored-by: TRAE CLI <traecli@bytedance.com>
2026-09-03 20:01:50 +08:00
bytedance-zhangbinkai ac0f243e5b feat(base): support AI classification and AI Analysis Action (#2590)
* docs(base): sync workflow guide to current branch

* docs(base): sync workflow schema to current branch

* feat(base): support AI classification workflow validation

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* docs(base): document AI classification workflow schema

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* feat(base): validate AI classification agent data

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(base): relax ai classification optional fields

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(base): validate workflow ai analysis json

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(base): validate workflow ai analysis json

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(base): default ai classification no match action

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(base): keep ai analysis validation scoped

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(base): normalize workflow empty steps

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(base): reject ai classification mode input

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix: polish skill

* fix: 还原 step 判断逻辑

* fix: 调整校验逻辑组织形式

* fix: polish skill

* fix: CR Comment

* feat: support development environment overrides

* fix: CR Comment

* Revert "feat: support development environment overrides"

This reverts commit cef8f78dc6.

* fix: polish skill

* fix: compress skill

* feat: support development environment overrides

* Revert "feat: support development environment overrides"

This reverts commit cef8f78dc6.

* fix: polish skill

* feat: support development environment overrides

* fix(base): preserve omitted AI classification strategy

* Revert "feat: support development environment overrides"

This reverts commit cef8f78dc6.

* fix: polish skill

* fix: ut

* feat: support development environment overrides

* fix: 沿用全量更新逻辑

* Revert "feat: support development environment overrides"

This reverts commit f805081e89.

---------

Co-authored-by: TRAE CLI <traecli@bytedance.com>
2026-09-03 19:34:14 +08:00
yangr-happy 30e95091ff feat: validate generated API parameter constraints (#2514)
Co-authored-by: yangr-happy <301323675+yangr-happy@users.noreply.github.com>
2026-09-03 17:18:46 +08:00
R0bynZhu 7690ba4446 feat(slides): lint slide writes server-side, add --no-lint to opt out (#2607)
+create, +add-slide, +update-slide and +replace-slide are the four
shortcuts that change slide content, so they are the four that can ask
the backend to check the page before accepting the write. All four now
send lint_xml=true by default and lint_xml=false when --no-lint is
passed. The subject of the check is the page the write produces, not the
payload it was handed: +replace-slide submits fragments, and a fragment
that is correct on its own can still push a neighbour off the canvas.

The switch travels in the request body rather than the query string. A
query parameter has to be declared in the gateway's own api meta before
it is bound to a field, and the published definition of these endpoints
does not list one — so an undeclared parameter is dropped, the field
arrives unset, and the server reads it as "not requested". Verified
against a live backend: pages that asked to be linted were written
unlinted, with nothing anywhere to say so. Body fields ride along with
the JSON already being sent and need no registration.

The value is sent explicitly in both directions rather than omitted when
on. The parameter is newer than the registry, so the server-side default
is not something this CLI can read anywhere, and a request that states
the value keeps meaning the same thing if that default ever moves.

A refusal is passed through verbatim. The message field carries the lint
report itself — the same document the lint tool writes when it is run by
hand — and the same refusal reaches callers through `lark-cli api` as
well, where nothing rewrites it. Rendering it to prose here would give
one refusal two formats depending on which command produced it. Each
finding carries the numbers behind its own rule, and which numbers those
are differs per rule, so nothing is decoded that is not used: the report
is what the caller reads.

The refusal is recognised by its error code, 4000153, which the engine
raises for nothing else and which reaches the CLI unchanged. Matching on
the shape of the message instead would mean claiming any JSON that
resembles a report, and a false positive there rewrites the hint of an
error this code does not understand.

What the backend cannot say goes in the hint instead: how many findings
refused the write, that the page did not land, and --no-lint, which is a
CLI flag the server has never heard of. The count is summary.error_count
rather than the number of findings, because errors are what refuse a
page — the same line the lint tool draws when it is run by hand, exiting
non-zero on error_count alone. A report can arrive with warnings beside
its errors, and counting those too would send the caller hunting for
blockers that are not there. A message that does not parse still gets
the hint: the escape hatch is the half of it they cannot get anywhere
else, and withholding it over a missing number helps nobody.

The hint names no page. Every write path submits exactly one page, so a
finding's slide_number is its position inside that submission and is
always 1 — which is not the page the caller is looking for. On +create
it is actively wrong: it would read "on slide 1" next to a progress line
saying "adding slide 2/3 failed". The page number has one source, and it
is that line.

Findings that did not refuse the write come back the other way. The
backend returns them in an issues field on a response that succeeded, and
all four shortcuts now pass that field through untouched rather than
dropping it. It only ever arrives on a page that was written: anything
serious enough to refuse the write left as the error above, carrying the
same report. Dropping it would leave the caller believing the deck says
exactly what they wrote, with no way to learn otherwise short of looking
at the rendered page. It is passed through rather than reformatted so
that one field reads the same however the page was written.

+create keeps adding its pages one at a time, so a refusal there can
arrive with the presentation and some of its pages already written. It
is reported as such: the error carries the lint report and, next to it,
which page was refused and how many landed before it, so the retry adds
the rest instead of building a second deck. +replace-pages does the same
for the items in its plan.

A batch that was told to keep going reports its failures only through the
per-item records, so those carry the report, the code and the flag hint
as well; a record built from the error's message alone would have named
neither the refusal nor the way past it. The position stays on the
returning path, where it is the only thing that says how far the batch
got — beside a per-item record it would describe a batch that did not
stop.

Tests assert on the wire — the body the stub actually received — rather
than on the builder's return value, so a command that stops calling its
own builder still fails.
2026-09-03 14:04:05 +08:00
max 688de5cda3 feat(vc): distinguish detected meeting share starts (#2541) 2026-09-03 13:37:51 +08:00
木杉 6606594068 fix(suggest): surface both halves of a welded compound flag name (#2604)
`suggest.Closest` ranks flag/command suggestions by shared prefix then edit
distance. A hallucinated name welded from two real names -- e.g. `--sql-file`
from the real `--sql` and `--file` of `apps +db-execute` -- defeats both
signals: the leading half wins on prefix, and the trailing half (`file`, 4
edits from `sql-file`, budget 2) is dropped. The hint then names the flag the
caller did not want and omits the one that does exactly what they asked for.

The cost is not the rejected call. Steered to `--sql`, callers inline SQL
through the shell, where quoting mangles `DEFAULT ''` and
`current_setting(...)` into syntax errors that read as SQL-authoring bugs.
`--file` passes file contents verbatim and avoids that class entirely.

Treat a candidate that exactly equals one hyphen-delimited segment of the typed
name as plausible, however far the whole string drifted. Ranking is unchanged --
segment hits are admitted, not promoted -- so the leading segment still ranks
first, and an unrelated candidate list still yields no suggestions.
2026-09-03 11:33:52 +08:00
sang-neo03 59f6ad4900 fix(output): preserve non-data payloads in the api success envelope (#2601)
* fix: preserve non-data payload keys in SuccessEnvelopeData

When an API response uses a non-"data" key (e.g., /bot/v3/info returns
payload under "bot"), the previous implementation discarded the payload
and returned an empty object. Fall back to the envelope minus transport
fields (code, msg, data) so the business payload is preserved.

Fixes #2428

(cherry picked from commit a82585718c)

* fix(output): pass non-object bodies through and pin api envelope at command level

Follow-up to the cherry-picked fix for #2428: return nil bodies as {} and
non-object bodies untouched instead of collapsing them, align the new test
with its neighbours, and add cmd/api regression tests through the httpmock
path so the user-visible envelope is pinned where the bug was reported.

* test(output): pin null data with sibling payload, drop SDK-pinned array test

TestApiCmd_NonObjectBody_FailsLoudly asserted the SDK's pre-decode rejection
of non-object bodies, not the output-layer branch this PR added; reverting
that branch left it green. The unit test already covers the branch, so the
command-level copy is removed. Add a unit case for {"data": null, "bot": {..}},
which the previous code collapsed to {} and now returns as {"bot": {..}}.

* fix(output): normalize legacy bot payloads

---------

Co-authored-by: Wu Shuwen <108231307+dajiaohuang@users.noreply.github.com>
Co-authored-by: sang-neo03 <266690410+sang-neo03@users.noreply.github.com>
2026-09-03 01:14:22 +08:00
kele498 515f9f5a4a feat: 支持会议搜索使用机器人身份 (#2445)
sa: safe
doc: skills/lark-meeting
cfg: none
test: unit test, dry-run e2e, live TAT smoke

Co-authored-by: search_zhuhao <zhuhao.517@bytedance.com>
2026-09-02 21:33:57 +08:00
dc-bytedance d5148a88df fix: honor requiredScopes conjunction in CollectScopesForProjects (#1878) 2026-09-02 18:11:22 +08:00
Wu Shuwen 59dcdf559b docs(skills): fix broken reference links (#2485) 2026-09-02 15:32:16 +08:00
huarenmin13 d66b1cac2e fix(base): improve search recovery and form deletion safety (#2422)
- 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>
2026-09-02 14:15:28 +08:00
bytedance-zhangbinkai 5c1aa633b6 fix(base): repair field schema template reference (#2575) 2026-09-02 10:41:26 +08:00
lark-cli-external-pr-digest[bot] 2aebe8970f chore: release v1.0.93 (#2597) v1.0.93 2026-09-01 22:40:41 +08:00
sang-neo03 d12b39cf46 feat(vfs): allow absolute paths under a built-in path policy (#2580)
* 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.

---------
2026-09-01 22:18:29 +08:00