lark-cli rejects absolute paths for --file, --output, --output-dir,
and @file with 'unsafe file path'. Document this in lark-shared so
agents know to use cwd-relative paths or stdin for data input.
Change-Id: I50cf801c2c5d0e3cbb98a76e1752d410518c8636
Same enforcement as the previous commit — require reading reference
docs (or -h) before calling shortcuts. These three skills use
non-standard section headers but still have shortcut tables with
reference links.
Change-Id: I5170cc763c15e3030c4117a36af36c9f1e94501e
Evaluation data shows AI call failure rate <1% when reference docs are
read vs ~29% when not. Add a CRITICAL line to the Shortcuts section of
14 SKILL.md files and the skill template, requiring agents to read the
linked reference doc (or run -h for commands without one) before
invoking any shortcut.
Change-Id: Ia4204518eb43a9f6c8295b95633ee5d9cf2f5352
* feat(api): add --json flag as no-op alias for --format json
* feat(service): add --json flag as no-op alias for --format json
* feat(shortcut): add --json flag as no-op alias for --format json
Skip registration when a custom --json flag already exists on the
command (e.g. base shortcuts use --json for body input).
Change-Id: If66236cadeea7fa81811061cce775deff51b92ce
* refactor: extract FetchTAT sharing the TAT-rejection classifier
doResolveTAT minted the tenant access token inline. Extract the HTTP call
into FetchTAT(ctx, httpClient, brand, appID, appSecret) so callers that
already hold plaintext credentials — notably the post-config-init probe —
can validate them without a second keychain round-trip.
FetchTAT routes a non-zero TAT body code through the same
classifyTATResponseCode the credential layer already uses, so a rejection is
the canonical CategoryConfig / SubtypeInvalidClient (10003 / 10014) typed
error — identical to what every token-resolving command returns. Transport,
HTTP-status and JSON-parse failures stay raw (untyped) so callers can use
errs.IsTyped to separate a deterministic credential rejection from upstream
noise. doResolveTAT now delegates to FetchTAT; observable behavior unchanged.
* feat: validate credentials after config init
After config init saves the App ID / App Secret, fire a best-effort probe:
mint a tenant access token with the just-saved credentials, then POST the
application probe endpoint. When the credentials are deterministically
rejected, FetchTAT returns a typed errs.* error and runProbe propagates it,
so config init exits non-zero with the canonical ConfigError / invalid_client
envelope (the same one every other command shows for the same bad creds)
instead of letting the user discover the mistake on a later request.
Ambiguous failures (transport, HTTP non-200, JSON parse, timeout,
http-client init) come back untyped and are swallowed (errs.IsTyped is the
discriminator), so a valid configuration is never blocked by upstream noise.
The probe is wired into all four init paths and skipped when the user reused
an existing secret. The saved config is not rolled back on rejection: stdout
still records what was saved, stderr carries the typed error envelope.
Drive-domain errors now leave the CLI as typed, machine-branchable
envelopes — a stable `type` plus `subtype` and named fields (param,
params, retryable, log_id, hint) — so scripts and AI agents can branch on
structure and act on a recovery hint instead of parsing prose.
Changes:
- Every error produced in the drive domain — validation, file I/O, and the
failures returned from its Lark API calls — is emitted as a typed errs.*
error; the exit code is derived from the error category. Drive's API calls
now go through a shared typed classifier, so failures carry subtype,
troubleshooter, a recovery hint, and the request's log_id whether the
server returns it in the response body or the x-tt-logid header; an
already-typed network/auth error is never downgraded into a generic API
error.
- Known API conditions (resource conflict, cross-tenant, cross-brand, ...)
carry a recovery hint keyed by their error class; a command can refine
that hint with command-specific guidance.
- Batch partial failures (+push / +pull / +sync, where some items succeed
and some fail) now report an honest ok:false multi-status result on
stdout — the summary and every per-item outcome stay machine-readable —
and exit non-zero, instead of a misleading ok:true success envelope.
- Duplicate rel_path conflicts report each colliding path as a structured
params entry (RFC 7807 invalid-params style).
- Static guards lock the drive path so legacy error construction — direct
envelopes or the auto-classifying API helpers — cannot be reintroduced,
making drive the template for the remaining domains.
Output changes worth noting for consumers:
- Error envelopes now carry typed type/subtype and named fields; exit
codes follow the error category (malformed or incomplete API responses
are reported as internal errors rather than generic API errors).
- Batch partial failures (+push / +pull / +sync) emit an ok:false result
envelope on stdout (summary + per-item items[]) and exit non-zero; the
per-item results stay on stdout rather than in a stderr error envelope.
Errors surfaced through shared cross-domain helpers (scope precheck, media
import upload, metadata lookup, save-path resolution) are not yet typed;
they migrate with the shared layer in a follow-up change.
Interactive card messages (msg_type: interactive) can contain @user elements in their card
body. The json_attachment.at_users field stores resolved user info, but the user_id there is
the sender-side platform user_id — not the reading app's canonical open_id. When the backend
populates a mention_key on each at_users entry, it signals that the API-level mentions[]
array carries a more authoritative open_id and display name for the reading context. This PR adds
support for this two-level lookup: it threads the raw mentions[] array into the card converter,
indexes it by mention_key for O(1) access, and renders the canonical open_id + display name
whenever the link is resolvable. All existing fallback paths (no mention_key, nil mentions) are
preserved without behavioral change.
Change-Id: I00f846d76482adba315d07361c35909b71ca74c7
Follow-up to #1223. The hand-written FLAGS block in `lark-cli --help`
restated leaf-command flags at the root level — flags that are not
registered on the root command (they error "unknown flag" there). Even
trimmed to an illustrative example list, it duplicated information Cobra's
per-command `--help` already renders authoritatively, and any static list
in root help drifts from the real per-command flag sets over time.
Drop the section entirely: Cobra's per-command `Flags:` output is the
single source of truth. `USAGE:`/`EXAMPLES:` still show flags in context,
and the `Flags:` block at the bottom of root help lists the actual root
flags. Also removes the now-obsolete TestRootLong_FlagsSectionPointsToCommandHelp.
The hand-written FLAGS block in `lark-cli --help` listed --params, --data,
--as, --format, --page-all, --page-size, --page-limit, --page-delay, -o,
--jq, -q and --dry-run as if they were global flags. None are registered
on the root command — they all error "unknown flag" at the top level and
exist only on leaf commands (api, service). The block also contradicted
the Cobra-generated "Flags:" section rendered directly below it, which
shows only -h/--help, --profile, -v/--version.
Replace it with a short illustrative example list (common flags first) and
a pointer to `lark-cli <command> --help` for the full per-command set.
Root help stays a discovery signpost without claiming the flags are global
or restating defaults/descriptions that drift from the real flag sets.
Change-Id: Ia1cab889dd70b6b49a61dac468dedfd7fe39043f
Simplifies the markdown-to-post rendering pipeline in the IM shortcut. The previous
implementation split markdown at blank-line boundaries into multiple post paragraphs,
using zero-width space (\u200B) sentinel characters to preserve visual spacing.
While well-intentioned, this approach introduced fragility around edge cases such as
blank lines inside fenced code blocks, messages with only blank lines, and interactions
with the heading-normalization pass. This change consolidates rendering back into a
single {"tag":"md"} segment, making the output more predictable, the code significantly
easier to follow, and the test surface easier to maintain.
Change-Id: Ic2870ecbcb31ae7d36121f120102f2ff964f5169
* feat: unconditionally inject --format flag for all shortcuts
Removes three HasFormat guards in runner.go so every shortcut
gets --format regardless of the Shortcut.HasFormat field value.
Shortcuts that already define a custom 'format' flag in Flags[]
are skipped to avoid redefinition panics (e.g. mail +triage, +watch).
HasFormat is retained in the struct but marked deprecated.
Change-Id: I5e8fe07e839d5aed4cefaf7d753dabbaee68fb6e
* test: isolate config dir in format-universal test
Change-Id: I3a59942aa8a6753cd949ca42f2a19a72f032ff55
* test: revert unnecessary config-dir isolation (mount-only test)
Change-Id: I0146e5a2f57f5419863bdeeaa1a662fd8f70bddf
internal/util imported internal/proxyplugin (SharedTransport, FallbackTransport,
NewHTTPClient, and WarnIfProxied via proxyPluginStatus), so a foundational util
package depended up into a feature package, pulling binding/core/vfs into the
transitive cone of every util importer.
Move internal/proxyplugin -> internal/transport and make it the single owner of
outbound transport: fold the two SharedTransport functions into one Shared()
(proxy-plugin override -> LARK_CLI_NO_PROXY -> http.DefaultTransport), and move
Fallback/NewHTTPClient/WarnIfProxied/DetectProxyEnv/noProxyTransport out of the
now-deleted internal/util/proxy.go into the new package. The proxy-plugin probe
is demoted to a private pluginTransport(); the duplicate redactProxyURL collapses
to one. internal/util keeps no proxy code and is a leaf again.
Re-point all consumers (registry, doctor, config, auth, cmdutil, update) to
internal/transport. Behavior-preserving: package move + symbol rename + dedup.
Two new tests lock the fail-closed contract (plugin overrides NO_PROXY; malformed
config never falls through to direct egress).
Every failure on the authentication, authorization, and configuration
path now surfaces as a typed structured error instead of an ad-hoc
envelope. Users and scripts that consume CLI output get:
- a fixed nine-category taxonomy on the wire, each mapped to a
stable shell exit code (authentication/authorization/config = 3,
network = 4, internal = 5, policy = 6, confirmation = 10)
- identity-aware detail fields (missing_scopes, requested_scopes,
granted_scopes, console_url, log_id, retryable, hint) carried
uniformly on the envelope
- a single canonical policy envelope at exit 6; the legacy
auth_error carve-out is retired
- per-subtype canonical message + hint that preserves Lark's
diagnostic phrasing and routes recovery to the right actor:
app developer (app_scope_not_applied), user (missing_scope,
token_scope_insufficient, user_unauthorized), or tenant admin
(app_unavailable, app_disabled)
- wrong app credentials classify as config/invalid_client whether
surfaced by the Open API endpoint (99991543) or the tenant
access-token mint endpoint (10003 / 10014), instead of
collapsing to a transport error or api/unknown
- local shortcut scope preflight emits the same
authorization/missing_scope envelope (identity + deterministic
missing-scope set) used by the post-call permission path, so AI
consumers read the same structured shape from precheck and from
server-returned permission denial
- streaming download/upload failures keep the same network subtype
split (timeout / TLS / DNS / transport) as the non-stream path
instead of collapsing every cause to a generic transport failure
- console_url is carried only on the bot-perspective
app_scope_not_applied envelope (where the recovery action is
"developer applies the scope at the developer console"); the
user-perspective missing_scope envelope drops the field, since
the only actionable user recovery is `lark-cli auth login --scope`
and pointing an end user at a console they cannot modify is
misleading
- bind workflows (Hermes / OpenClaw / lark-channel) flatten dynamic
Type tags to wire 'config' with the original module name kept
as a metric label
All 10 typed errors are cause-bearing, nil-safe on .Error() and
.Unwrap(), and defensively clone slice setter inputs. Four lint
rules (CheckNilSafeError / CheckBuilderImmutable / CheckUnwrapSymmetry
/ CheckBuildAPIErrorArms) lock these invariants on migrated paths.
* feat(platform): support multiple policy rules per plugin
Extend the command policy framework from single-Rule to multi-Rule
semantics. A plugin (or policy.yml) may now contribute several scoped
Rules; the engine combines them with OR -- a command is allowed when it
satisfies every axis of at least one rule. This lets one integration
apply different risk ceilings and identity restrictions to different
command groups.
The cross-plugin fail-closed boundary is preserved: two distinct plugins
both calling Restrict still aborts startup (multiple_restrict_plugins).
Single-Rule behaviour is fully backward compatible -- the rejection
reason_code / rule_name / envelope shape are byte-for-byte unchanged;
multi-rule rejection surfaces the aggregate reason_code no_matching_rule.
- engine: New keeps single-rule compat, add NewSet for OR over rules
- resolver: dedupe by owner (one plugin may contribute many rules),
return []*Rule; yaml gains a top-level rules: list
- registrar/builder/staging: Restrict may be called more than once;
retire the double_restrict error
- config policy show / config plugins show: emit a rules array
- inventory: PluginEntry.Rules is now a slice (fixes last-rule-wins
overwrite when a plugin contributes multiple rules)
* fix(platform): clone rules in Builder.Restrict and inventory snapshot
Address review feedback. Builder.Restrict stored the caller's *Rule
directly, so reusing and mutating one Rule object across multiple
Restrict calls collapsed entries to the last mutation; clone the rule and
its slices on append, mirroring the staging registrar.
BuildInventory likewise reused the source Allow/Deny/Identities slices;
copy them when building the RuleView snapshot instead of relying on
cloneInventory downstream.
Add a regression test: reusing and mutating one Rule across two Restrict
calls now yields two independent rules.
* fix(platform): skip yaml when a plugin owns policy; reject empty rules list
Two policy-config robustness fixes from review:
- A malformed ~/.lark-cli/policy.yml could abort a plugin-governed
binary. applyUserPolicyPruning read yaml before resolving, and
build.go fail-closes on any policy error when a plugin is present.
Plugin rules shadow yaml anyway, so skip reading yaml entirely when a
plugin contributed rules -- an unrelated broken file on the user's
machine can no longer lock the CLI.
- A present-but-empty "rules: []" collapsed to a single all-zero Rule
that allows every annotated command ("looks like policy, enforces
almost nothing"). yaml.Parse now distinguishes absent from
present-but-empty (Rules is a pointer) and rejects the empty list.
Add regression tests for both.
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.