Fix 3 occurrences of --minute-token (singular) to --minute-tokens
(plural) in lark-vc-recording.md to match the actual CLI flag
definition in minutes_download.go.
The size==1 (64-bit "largesize") branch of all three MP4 box walkers
(findMP4Box, readMp4DurationBytes, readMp4Duration) set boxEnd to the raw
largesize instead of offset+largesize — even though the 32-bit branch right
below correctly uses offset+size. Two consequences:
- Correctness: for any MP4 that carries a 64-bit box size at a non-zero
offset, the box walk is computed from the wrong end, so the moov/mvhd
lookup is truncated and the media duration is silently lost.
- Robustness/security (CWE-190): the unguarded uint64->int(64) conversion of
a largesize with the high bit set yields a negative boxEnd. The in-memory
walkers then assign it to offset and feed it back as a slice index
(data[offset:]), panicking with "slice bounds out of range" and crashing
the CLI on a crafted or corrupt MP4. This is reachable via URL-sourced IM
media, whose bytes the caller does not control.
Fix: compute boxEnd as offset+largesize (matching the 32-bit branch) and
reject largesize values smaller than the 16-byte header or larger than the
remaining input. Malformed media now honours the parsers' best-effort
contract by returning 0/-1 instead of panicking, and the bounds guarantee
the conversion can no longer overflow.
Add regression tests covering both the overflow (must not panic) and a
64-bit box at a non-zero offset (must walk correctly).
Add a new --types flag (string_slice; values from {group, p2p}) to
+chat-list, backed by the new GET /open-apis/im/v1/chats `types` query
parameter. Accepts CSV (--types group,p2p) and repeated-flag forms
(--types group --types p2p).
Defaults to groups-only (backward compatible). Under user identity,
p2p single chats appear with chat_mode="p2p" plus p2p_target_type /
p2p_target_id fields. Under bot identity:
- --types=p2p alone is rejected at validation
- --types=p2p,group is silently downgraded to types=group (no runtime
notice; skill docs document this contract)
Updates Shortcut.Description, lark-im SKILL.md (frontmatter trigger
+ shortcut table row), and the chat-list reference doc with command
examples, the new parameter, output field documentation, and a
dedicated "Bot identity and p2p" section.
Change-Id: I637ce23b3c6ce4ec350f0ac26dbac8120761bb71
* fix(install): detect curl version before using --ssl-revoke-best-effort
(cherry picked from commit da14737702)
* test(install): cover curl version gate and refactor for testability
Extract the version comparison out of curlSupportsSslRevokeBestEffort()
into a pure isCurlVersionSupported(output), so the >= 7.70.0 logic is unit
testable without spawning curl. Add cases for 7.55.1 / 7.69.0 / 7.70.0 /
8.x plus the unparseable and libcurl-token edge cases (the regex must read
the leading "curl X.Y.Z", not the trailing "libcurl/X.Y.Z").
Memoize the `curl --version` probe: curl's version is invariant for the
install's lifetime while download() runs once per mirror URL, so probe at
most once instead of re-spawning curl on every attempt.
---------
Co-authored-by: EllienTang <146210093+Ellien-Tang@users.noreply.github.com>
Co-authored-by: liangshuo-1 <266696938+liangshuo-1@users.noreply.github.com>
Two issues caught in review of #1132 that the existing tests missed because
they constructed RuntimeContext/CliConfig directly, bypassing the credential
edge where the bug lives.
P1 — Lang dropped at credential boundary
credential.Account had no Lang field, so AccountFromCliConfig and
ToCliConfig silently dropped cfg.Lang. The production Factory builds
CliConfig via acct.ToCliConfig() (factory_default.go Phase 3), which
meant RuntimeContext.Lang() always returned "" in production and
shortcuts/mail/mail_signature.go always fell back to zh_cn — defeating
the whole point of persisting --lang.
Fix: add Lang i18n.Lang to Account and copy it in both directions.
Regression test: TestFullChain_LangSurvivesProductionPath walks the
real path (SaveMultiAppConfig -> DefaultAccountProvider.ResolveAccount
-> ToCliConfig) and asserts Lang survives, so any future field added
to CliConfig forces the same audit.
P2 — priorLang ignored CurrentApp in multi-profile workspaces
priorLang scanned all Apps and returned the first non-empty Lang. If a
user had multiple profiles and the active one disagreed with Apps[0],
a re-bind without --lang would silently inherit the wrong profile's
preference.
Fix: read multi.CurrentAppConfig("").Lang instead.
Regression tests cover CurrentApp wins over Apps[0], single-app
fallback, and malformed bytes.
Change-Id: If7a276605f84f398cec329c2c942b471b4c32749
Follow-up to #1095. The reactions auto-enrichment shipped, but on busy chats the strictly-serial per-resource fetches in EnrichReactions, ExpandThreadReplies, and merge_forward expansion stretched the command's wall time above 14s — enough that wrapper agents (30–60s wall-clock budgets) saw timeouts even though the CLI itself never errored. This PR parallelizes all three with the same bounded-concurrency pattern, batches the follow-up contact-API sender resolution so it doesn't fan back out into a serial stall, and fixes two correctness bugs that surfaced during review. Scoped to convert_lib/{reactions,thread,merge,content_convert}.go + tests + the 4 shortcut Execute hooks + the reference doc.
Change-Id: I0206d10ad204382170bd42aec67f82578923736e
The six TestDriveInspectExecute_* tests set
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) but build the CLI via
cmdutil.TestFactory(t, cfg), which provides an in-memory config closure
(func() (*core.CliConfig, error) { return config, nil }) and never reads the
filesystem. Per the repo learning from PR #343, this env var should only be
set for tests exercising the real NewDefault() factory path. None of these
tests use NewDefault(), so the calls are dead and removed.
No behavior change; all TestDriveInspect* tests still pass.
Co-authored-by: kyalpha313 <kyalpha313@users.noreply.github.com>
妙搭/spark consolidated the apps domain onto spark:app:read / spark:app:write.
The standalone spark:app:publish and spark:app.access_scope:* scopes are retired.
- +html-publish: spark:app:publish -> spark:app:write
- +access-scope-get: spark:app.access_scope:read -> spark:app:read
- +access-scope-set: spark:app.access_scope:write -> spark:app:write
Verified against the official docs for upload_html_code_and_release,
get_app_visibility and update_app_visibility. +create/+update/+list were
already correct (spark:app:write / spark:app:read).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `lark-cli mail +draft-send` shortcut that takes one or more existing
draft IDs and sends each via POST /drafts/:draft_id/send sequentially.
Per-draft failures are isolated and aggregated into a structured output;
fatal failures (auth, permission, network, mailbox quota) abort the
entire batch immediately while recoverable failures honor --stop-on-error.
Also extend internal/output with six mail-send-specific errno constants
(LarkErrMailboxNotFound=4013, LarkErrMailSendQuota{User,UserExt,TenantExt},
LarkErrMailQuota, LarkErrTenantStorageLimit) consumed by isFatalSendErr.
Risk is "high-risk-write" so the framework's --yes gate applies; the
shortcut declares only the minimal mail:user_mailbox.message:send scope
to avoid asking users for permissions it does not need.
- Pull messages now auto-call im.reactions.batch_query and attach a
reactions block (counts + details) to each message. Stops AI from
misjudging "user already reacted" as "no response yet" and
re-sending duplicate reactions. Server caps queries[] at 20 per
call, so messages are split into batches of size <= 20.
- Edited messages additionally surface update_time. The server echoes
update_time == create_time for unedited messages too, so the field
is only emitted when updated == true; otherwise every message
output would look "edited". The value is read via an explicit
string assertion + TrimSpace so empty strings are filtered properly
(the previous `v != ""` was a no-op for non-string types).
- All four message-pulling shortcuts (+messages-mget,
+chat-messages-list, +messages-search, +threads-messages-list) get
a --no-reactions opt-out flag for callers that want to skip the
extra round-trip.
- Each shortcut declares im:message.reactions:read on its
UserScopes/BotScopes (or Scopes for the user-only search command) so
the auth flow covers the new dependency.
- Each shortcut's --dry-run output now lists the
reactions/batch_query call (or omits it when --no-reactions is set),
so callers can audit the full set of API calls before execution.
- Warnings go through runtime.IO().ErrOut (forbidigo lint requires
IOStreams over os.Stderr in shortcut code).
- Duplicate message_id inputs (e.g. mget --message-ids om_a,om_a)
attach the reactions block to every entry while still querying the
API only once per distinct id.
- EnrichReactions walks msg["thread_replies"] recursively, and mget/
chat-messages-list call it after ExpandThreadReplies, so replies
receive reactions in the same batched call as their parent message.
- When the batch_query call fails or returns per-message failures,
the affected messages get reactions_error=true (mirroring the
thread_replies_error flag from thread.go) so consumers can
distinguish "fetch failed" from "no reactions exist" by reading
stdout alone, without depending on the stderr warning channel.
- lark-im skill docs: the default-enrichment contract lives in a
standalone references/lark-im-message-enrichment.md so the generated
SKILL.md can't strand it on regeneration. The four read references
and the raw reactions API reference link to it, and the template
source skill-template/domains/im.md carries a durable pointer.
Change-Id: Ia9ea74b11945644262bb25c6503fb9b2003c6c98
Affordance examples previously carried a title plus a structured input
object mirroring the inputSchema. Replace that with a description plus a
command string holding a ready-to-run lark-cli invocation, which is what
an AI agent driving the CLI actually consumes.
No affordance data exists in the registry yet, so this only reshapes the
consuming AffordanceCase type and its tests; the data pipeline
(registry-config.yaml -> gen-registry.py -> meta_data.json) forwards the
new keys verbatim.
* feat(schema): add envelope types and ordered properties container
* feat(schema): build meta_data.json key-order index for property ordering
* feat(schema): implement convertProperty with file/enum/range/nested handling
* feat(schema): build inputSchema with x-in / file binary / yes injection
* feat(schema): build outputSchema wrapping responseBody
* feat(schema): build _meta with scopes/risk/access_tokens normalization
* feat(schema): scaffold affordance overlay loader (PR-1 stub)
* feat(schema): wire up AssembleEnvelope main entry point
* feat(schema): parse dotted and space-separated path arguments
* feat(schema): batch envelope assembly with optional method filter
* feat(schema): implement L1-L3 envelope lint (structure/type/cross-field)
* feat(schema): measure L4 coverage and gate all envelopes through L1-L3
* feat(schema): add golden test harness with UPDATE_GOLDEN refresh
* test(schema): seed 20 golden envelopes covering edge cases
* feat(schema): output MCP envelope as default JSON, preserve pretty mode
Rewrites cmd/schema/schema.go so the default --format json branch emits
MCP-spec envelopes via schema.AssembleAll/AssembleService/AssembleEnvelope.
The legacy --format pretty branch is preserved verbatim and still uses
printServices / printResourceList / printMethodDetail.
Args max raised from 1 to 8 so the path can be supplied either as a single
dotted argument (im.reactions.list) or as space-separated segments
(im reactions list); both forms route through schema.ParsePath and produce
byte-identical output.
The completeSchemaPath function is extended to drive tab-completion for
both forms: legacy dotted prefix when len(args) == 0, and per-segment
resource/method completion when args already contains earlier segments.
BREAKING CHANGE: default JSON output shape changes from the raw meta_data
structure to an MCP envelope array/object. Existing scripts parsing the
old shape must either pin --format pretty or migrate to the new envelope
fields (name, description, inputSchema, outputSchema, _meta).
* test(schema): cover envelope JSON output, space-form path, yes injection
Replaces TestSchemaCmd_NoArgs with two variants reflecting the new default
shape: TestSchemaCmd_NoArgs_Pretty asserts the legacy "Available services"
text appears only under --format pretty, and TestSchemaCmd_NoArgs_JSON_IsArray
asserts the default JSON output parses as an envelope array with at least 180
entries.
Adds six new tests:
- TestSchemaCmd_JSONIsEnvelope: single-method output has name / description
/ inputSchema / outputSchema / _meta keys and envelope_version "1.0".
- TestSchemaCmd_SpaceSeparatedPath_EqualsDotted: dotted and space forms
produce identical output bytes for the same command path.
- TestSchemaCmd_ServiceListIsArray: schema <service> returns a JSON array
whose every entry's name starts with "<service> ".
- TestSchemaCmd_HighRiskYesInjection: high-risk-write commands inject
inputSchema.properties.yes.
- TestSchemaCmd_NoYesForReadRisk: read-risk commands do not inject yes.
- TestSchemaCmd_PrettyUnchanged_KeyTextPresent: --format pretty still
surfaces the legacy section markers (Parameters:, Response:, Identity:,
Scopes:, CLI:).
* feat(schema): assemble envelope from embedded data only for stability
* chore(schema): lint cleanup
* fix(schema): preserve dotted resource segments in envelope name
Nested resources whose meta_data key contains a dot (e.g. chat.members,
user_mailbox.templates) were previously split on '.' and rejoined with
spaces, producing envelope names like 'im chat members bots'. AI
consumers doing name.split(' ') and feeding the result back as argv
got 'lark-cli im chat members bots' which the CLI rejects — the actual
invocation form is 'lark-cli im chat.members bots'.
Pass the dotted resource key as a single argv segment so the envelope
name 'im chat.members bots' round-trips through name.split(' ') back
to the CLI. Mirror the same convention in the golden harness so its
single-method assembly matches the live AssembleService walk.
* fix(schema): align MCP envelope output with JSON Schema 2020-12 contract
- coerce enum literals to typed JSON values (integer to int64,
number to float64, boolean to bool) so type:"integer" fields no
longer emit string enums; sort numeric/boolean enums while
preserving meta_data order for string enums that carry semantic
priority
- translate non-standard meta_data type:"list" to JSON Schema
type:"array" with items:{} fallback when element shape is absent
(covers the two mail attachment_ids fields)
- render inputSchema.required even when empty so consumers see a
stable envelope shape ("[]" means no required fields, not "field
is missing")
- reject trailing path segments in both JSON and pretty modes so
schema im.messages.delete.foo errors instead of silently
returning the delete method
- drop dead "list type" entry from lint_test isKnownDataInconsistency
whitelist now that list values are translated upstream
* fix(schema): address CodeRabbit findings and stabilize CI tests
CI fix
- Replace hard-coded absolute key-order assertions in TestKeyOrderIndex_*
and TestBuildInputSchema_* with set-membership and propagation invariants;
the upstream meta_data API does not guarantee stable JSON key order across
fetches, so the old tests were flaky on CI by design.
- Skip byte-level TestGoldenEnvelopes when CI=true; golden snapshots are a
manual refresh artefact tied to a specific meta_data fetch, not a CI gate.
- Add TestMain to isolate registry-backed tests from any host ~/.lark-cli
cache (LARKSUITE_CLI_CONFIG_DIR + LARKSUITE_CLI_REMOTE_META=off) so the
suite gives the same answer on every machine.
CodeRabbit review actionables
- EmbeddedServiceNames returns a defensive copy so callers cannot mutate
the package-level slice and affect subsequent assembly determinism.
- coerceEnumValue is now also applied to default literals: integer fields
no longer ship default: "500" — they ship default: 500 (same idea as the
earlier enum coercion fix).
- options-branch string enums preserve meta_data source order, matching the
enum-branch policy; only numeric/boolean enums get sorted.
- validatePropertyTypes now validates the array element schema itself
(type, nested items), not only items.properties — previously a primitive
element with an invalid type (e.g. items.type="list") slipped past lint.
- OrderedProps.MarshalJSON falls back to alphabetical key order when Map
has entries but Order is empty, instead of silently emitting {}.
Tests pass locally and with CI=true env (simulating GitHub Actions).
* chore(schema): refresh golden envelopes after meta_data drift
Re-generated with UPDATE_GOLDEN=1 against the current meta_data.json
snapshot. The bulk of the diff is upstream noise (description wording,
enum entries, field order) which the CI snapshot diff can no longer
reasonably gate (see previous commit). Side-effects of the code fixes
in the parent commit are also captured:
- integer-typed defaults now emit numeric literals (e.g. page_size
default 500, not "500") thanks to coerceEnumValue
- mail.user_mailbox.templates.create _meta.risk corrects to "write"
(assembler already emitted "write"; the old golden was stale)
* fix(schema): address CodeRabbit round-3 review findings
- TestMain: cleanup now runs reliably. os.Exit skips deferred functions,
so the previous defer os.RemoveAll(dir) never executed. Replace defer
with explicit cleanup, and fail fast if MkdirTemp errors instead of
silently running against the host cache (which defeats isolation).
- convertProperty default coercion: when the literal cannot be coerced to
the declared type (e.g. default:"" on integer field, used by meta_data
to mean "no default"), omit the field entirely rather than emit a
type-mismatched default. Removes a contract violation flagged on
im.reactions.list.json#page_size.
* feat(schema): wire affordance overlay into envelope _meta
Replace the loadAffordance stub (which always returned nil and read
from an empty embedded annotations/ directory) with parseAffordance,
which lifts the affordance block from method["affordance"]. The block
is authored under larksuite-cli-registry's registry-config.yaml in the
overrides: section and flows through gen-registry.py's deep_merge into
the embedded meta_data.json.
Simplify buildMeta signature: the service/resourcePath/method args
existed only to feed the old dotted-path lookup.
Refresh 9 golden envelopes for unrelated upstream meta_data.json drift.
* refactor(schema): drop x-in extension from inputSchema
x-in (path/query/body) was an HTTP-shape leak in a CLI-facing tool spec.
AI consumers call the CLI by name with named args — they never construct
HTTP requests directly, so the path-vs-body-vs-query distinction is the
CLI's internal concern, not part of the contract.
Execution path (cmd/service/service.go) already reads location from
meta_data.json directly, so removing x-in does not affect routing.
Drop:
- Property.XIn field
- validXIn map and the two lint rules that depend on x-in
(L1 "top-level missing x-in" and L2 "path field must be in required")
- contains() helper, no longer referenced after the path-required rule
went away
Refresh 20 goldens for the now-absent x-in lines.
* refactor(schema): wrap inputSchema into params/data/flags sub-objects
Replace the flat inputSchema with a 3-bucket nested structure that mirrors
the CLI's actual flag layout, so AI consumers can directly map envelope
fields to lark-cli invocation:
inputSchema:
properties:
params: { ...path + query fields } → CLI --params JSON
data: { ...body fields } → CLI --data JSON
flags: { yes: ... } → CLI --yes (only for high-risk-write)
Each sub-object only appears when the method has the corresponding source,
so read-only GETs have a single `params` block, body-only POSTs have a
single `data` block, etc.
The `flags` wrapper carries an explicit description marking it as a CLI
control bucket (not API fields), so AI does not confuse `yes` with a
backend parameter.
Lint:
- L2 walkForL2 helper recurses into params/data sub-objects so leaf
invariants (format:binary on non-string, min<max, required-in-properties)
still apply.
- L3 yes-presence check now navigates flags.properties.yes.
Refresh all 20 goldens for the new shape.
* refactor(schema): drop flags wrapper, put yes at top level alongside params/data
The flags wrapper added one extra layer for a single field. Flatten so
inputSchema.properties has three siblings:
inputSchema:
properties:
params: { ...path + query } → CLI --params
data: { ...body } → CLI --data
yes: { boolean, default:false } → CLI --yes (only when risk == high-risk-write)
`yes` description strengthened to mark it as a CLI confirmation gate
(consumed by lark-cli, not sent to the backend), so AI can still
distinguish it from API fields without needing a wrapper.
Lint L3 yes-presence check goes back to top-level Properties.Map["yes"].
Refresh 20 goldens.
* feat(schema): add `file` top-level sub-object for binary upload fields
Splits file fields out of `data` into their own sibling, so the four
top-level slots in inputSchema map 1:1 to CLI flag dispatch:
inputSchema.properties:
params { path + query fields } → --params JSON
data { non-file body fields } → --data JSON
file { type:file body fields, format:binary } → --file <key>=<path>
yes boolean → --yes (only when risk == high-risk-write)
Each slot is conditional: only registered when the method actually has
fields for that source. This matches the CLI's own conditional flag
registration (cmd/service/service.go:170-195), so what AI sees in the
schema is exactly what flags exist for that method.
The file sub-object carries a description explaining its semantics so AI
knows to use --file for those fields rather than embedding the binary
in --data JSON.
Refresh im.images.create golden (the only file-upload method in the
golden set).
* test(schema): cover L2 lint recursion into params/data sub-objects
Add two negative test cases that stuff bad values inside the wrapped
inputSchema sub-objects (rather than at top-level), to lock in
walkForL2's recursive coverage:
- format:binary on a non-string field nested under params
- sub-object Required referencing a key not in its Properties
Regression guard so future walkForL2 refactors do not silently lose
recursion and let leaf-field violations slip past lint.
* fix(schema): coerce example, aggregate nested required, fix path hint
- coerce `example` literal to the declared JSON Schema type (rename
coerceEnumValue -> coerceLiteral, drop on coerce failure to match the
`default` policy). Without this, integer/boolean/number fields emitted
string examples and failed strict validators.
- aggregate child field `required:true` into the enclosing nested
object's `required[]` (both object and array-items shapes). Previously
only the top-level params/data sub-objects scanned `required`, so
envelopes silently under-reported the real call contract.
- check method existence before reporting trailing-segment failure in
both JSON and pretty `schema` paths. A typo like `schema im messages
typo extra` now reports "Unknown method: im.messages.typo" instead of
the misleading "Method 'typo' exists but trailing segments ..." hint.
- extract risk level constants (RiskRead / RiskWrite / RiskHighRiskWrite)
in internal/cmdutil/risk.go; replace literal usages in schema, lint,
and confirm helpers so the typo radius is one file.
- reconcile AssembleEnvelope docstring with implementation reality (the
package-level currentMethodOrder + assembleMu serialize concurrent
callers; output is deterministic per inputs).
- drop testdata/golden/ and golden_test harness. End-to-end envelope
shape regression now relies on real CLI invocations and the existing
property-level unit + lint coverage.
* fix(schema): emit items:{} for all typeless arrays, restore lint gate
The list→array fallback only added items:{} when the source type was
"list", leaving ~64 natively-typed array fields (e.g.
approval.instances.cc.cc_user_ids) as {type:"array"} with no items.
These violated the L1 lint rule, but TestAllEnvelopesPass skipped the
"array missing items" error as a known data inconsistency, so the MCP
tool contract was not actually lint-clean.
Relax the fallback to cover every array lacking element shape regardless
of source type, and drop the lint-test skip so the gate is hard again.
Parse keywords from minutes artifacts API in vc +notes and document
the field in lark-vc skill references.
Co-authored-by: Cursor <cursoragent@cursor.com>
The standup workflow and the +get-my-tasks reference both implied a
"pending todo summary" use case but did not pass --complete=false in
the example commands. As a result, completed tasks were surfaced into
standup/daily summaries as if they were still pending.
This change updates the workflow and reference docs only — the
underlying command behavior is unchanged.
Closes#993
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.
* feat(apps): replace +html-publish cwd hard-reject with credential-file scan
The previous --path == "." block was a coarse heuristic: it caught the
common foot-gun of publishing a repo root, but also rejected legitimate
clean cwds, and let a ./dist with a forgotten .env ship the secret
through anyway (the sensitive-paths scanner was advisory and never ran
on the Execute path).
Move the gate from path shape to path content:
- Validate now walks --path candidates and rejects publishes that
include well-known credential files (.env / .env.* / .npmrc / .netrc
/ .git-credentials / .aws/credentials / .gcloud/credentials* /
.docker/config.json / .kube/config). Living in Validate (not DryRun)
means dry-run returns non-zero on hit too, so the dry-run preview
matches Execute.
- Narrow the credential pattern set. .git/, SSH private keys, *.pem
and *.key are out of scope -- they're not env-token files and the
false-positive rate (public certs, docs about key formats) is high.
- Add --allow-sensitive as the escape hatch for legitimate cases
(e.g. a docs site shipping .env.example on purpose). DryRun surfaces
the waived list in sensitive_waived so the caller can relay it.
- Drop the cwd defense-in-depth in runHTMLPublish. A clean cwd is now
a valid publish target.
The lark-apps skill and the html-publish reference are updated to
describe the new gate, the override flag, and the patterns now
explicitly out of scope.
* feat(apps): drop .gcloud/* from credential-file scan
The .gcloud/credentials pattern matched a non-existent path: gcloud's
actual config dir is ~/.config/gcloud/ (XDG-based), and the real
credential files there are credentials.db / access_tokens.db /
application_default_credentials.json -- none of which would land under
a .gcloud/ segment in a publish payload.
Drop the rule rather than fix it: the realistic gcloud foot-gun would
require recognizing the .config/gcloud/* tree by file basename, which
is a broader change than the targeted env/cred scan in this PR. The
remaining 7 patterns (.env / .env.* / .npmrc / .netrc /
.git-credentials / .aws/credentials / .docker/config.json /
.kube/config) cover the common Node/Python/CLI-tooling foot-guns.
* fix(apps): close credential-scan bypass when --path is the parent dir itself
isSensitiveRelPath anchors cloud-SDK matchers on adjacent parent/file
segments (.aws/credentials, .docker/config.json, .kube/config), but
walker strips that parent via filepath.Rel when --path is the conventional
parent dir (e.g. ./.aws), yielding a bare RelPath="credentials" that
slipped through silently. Same bypass for the single-file form
--path ./.aws/credentials (walker sets RelPath = Base(rootPath)).
Wrap the scan in isSensitiveCandidate: keep the fast RelPath scan, and
on miss fall back to filepath.Abs(AbsPath) so the parent segment is
visible again. isSensitiveRelPath itself is unchanged; existing tests
still pin its pure-function contract.
* fix(apps): drop filepath.Abs from sensitive scan to satisfy forbidigo lint
The previous fix called filepath.Abs(c.AbsPath) — banned by the repo's
forbidigo rule because shortcuts must not reach into the filesystem for
path resolution.
Reframe the same fix without fs access: re-prepend the root's basename
(or, for the single-file form, the parent dir's basename of rootPath)
to RelPath and re-scan only the parent-anchored credential pairs
(.aws/credentials, .docker/config.json, .kube/config). Leaf matchers
(.env / .npmrc / ...) stay scoped to RelPath — incidentally closing a
latent false-positive where --path /home/alice/.env/dist would have
flagged every file under it just because .env appeared in the
absolute path.
* fix(apps): read app object from data.app for +create and +update
The Miaoda OpenAPI returns the application object nested under
data.app for both POST /apps and PATCH /apps/{appId}. The CLI text
helper was reading common.GetString(data, "app_id"), which yields an
empty string against the wire format -- so `lark-cli apps +create
--format pretty` printed `created: ` with no ID.
Navigate the new nested path via GetString(data, "app", "app_id") for
both create and update. Update unit-test mocks to wrap the response
under `app`. Refresh the lark-apps skill references (example response
shape + jq paths) so agents reading them follow the right path.
Wire format is passed through to the user's JSON envelope untouched
-- no unwrapping in CLI. Consumers reading the response should use
.data.app.app_id.
The GET /apps list endpoint is unchanged: per the design doc its
items[] are flat objects, no wrapper.
* docs(apps): add required --app-type HTML to scenario 2 snippet
The "用户没有 app_id" snippet in lark-apps-html-publish.md was missing
the required --app-type flag, so copy-pasting it triggered Validate
("--app-type is required") and left $APP empty -- the following
+html-publish then failed with --app-id "". Bring the snippet in line
with every other apps +create example in the skill.
* docs(apps): simplify auth-recovery rule to error.type == missing_scope
Every apps shortcut declares Scopes, so the precheck path in
shortcuts/common/runner.go:825 is always the one that fires on scope
violations and the envelope's error.type is the stable discriminator.
Drop the keyword-sniffing of error.hint, the chain explanation, and the
bot caveat — they all reduce to one boolean: error.type == "missing_scope"
→ run `lark-cli auth login --domain apps`.
Also collapse the corresponding bullet in 快速决策 to point at this rule.
* fix(common): escape special chars in multipart form filenames
MultipartWriter.CreateFormFile concatenated the fieldname and filename
into the Content-Disposition header without escaping, so a filename
containing a double-quote, backslash, CR, or LF produced a malformed
header. For example, uploading `report "draft" v2.pdf` via
`task +upload-attachment` made the server see `filename="report "`
(truncated at the first internal quote) and drop the rest.
Drop the custom override and let CreateFormFile be promoted from the
embedded *multipart.Writer, which applies the stdlib's quoteEscaper
(backslash and double-quote get a backslash prefix; CR and LF get
percent-encoded). The Content-Type ("application/octet-stream") and
the wrapper API are unchanged, so the existing `task +upload-attachment`
call site is unaffected -- filenames with special characters just now
round-trip correctly.
Add helpers_test.go covering plain, quoted, backslashed, mixed, and
unicode filenames. The test asserts both the on-wire encoding and a
round-trip through mime.ParseMediaType (bypassing Part.FileName, whose
filepath.Base is platform-dependent for backslash on Windows).
* test(common): cover CR/LF/CRLF in multipart filename escaping
Per code-review feedback, extend the helpers_test.go cases table with
CR, LF, and CRLF filenames so the test exercises both legs of the
stdlib's quoteEscaper:
- backslash and double-quote use backslash escaping (quoted-pair);
these round-trip exactly through mime.ParseMediaType.
- CR and LF use percent encoding to prevent header injection; the
MIME parser does not decode percent escapes, so the read-side
filename param contains literal "%0D"/"%0A".
The cases table grows a wantParsed column so each case can declare its
expected post-parse value (same as filename for backslash-escaped chars,
percent-encoded for CR/LF).
* refactor(common): polish doc comments and regroup test cases
Two follow-up tweaks suggested by a re-read of the PR:
- helpers.go: stop naming the stdlib's internal `quoteEscaper` in the
doc comment. Describe the observable behaviour ("escapes special
characters") instead, so the comment stays valid if the stdlib ever
renames or reimplements its escaping.
- helpers_test.go: rename the vague `with both` case to
`backslash and quote`; split the table-driven cases into three
visually-separated groups (happy path / backslash escaping /
percent encoding) so it is obvious why two cases have a different
wantParsed than filename.
No behaviour change; tests still pass 8/8.
* test(common): drop CR/LF filename cases that depend on Go 1.24+ stdlib
CI runs against the toolchain pinned in go.mod (1.23.0), whose
multipart/Writer.quoteEscaper escapes only backslash and double-quote.
Percent-encoding of CR and LF was added to the stdlib later, so the
three CR / LF / CRLF cases I added on review feedback fail on CI: the
literal CR/LF lands in the Content-Disposition header and the parser
reports `malformed MIME header: missing colon`.
Drop those three cases. The fix in the prior commits still covers the
real-world bug — backslash and double-quote in filenames — which is
what the original `report "draft".pdf` example demonstrates. CR or LF
in a filename is essentially never legal on any supported OS, so
leaving that edge case to a future stdlib upgrade keeps the test
stable across toolchains.
Also dropped the now-unused wantParsed column from the cases table:
with only round-trippable characters left, mime.ParseMediaType returns
the original filename byte-for-byte, so a single tc.filename comparison
suffices.
---------
Co-authored-by: Wang-Yeah623 <Wang-Yeah623@users.noreply.github.com>
When creating wiki nodes under the same parent concurrently, the API
returns error code 131009 (lock contention) ~5-15% of the time. This
adds automatic retry with exponential backoff (250ms, 500ms; max 2
retries) so callers no longer need to implement retry logic themselves.
- Retry loop in runWikiNodeCreate: only retries on code 131009, respects
context cancellation, prints progress to stderr
- wrapWikiNodeCreateRetryError preserves Err/Raw/Detail.Code in ExitError
- 6 unit tests covering retry success, exhaustion, non-contention error,
single-retry success, context cancellation, no-retry on success
- 8 dry-run E2E tests for wiki +node-create request shape and validation