19 Commits

Author SHA1 Message Date
liangshuo-1
ebb0b6fe73 feat(affordance): per-command usage guidance system (markdown source) (#1565) 2026-06-29 19:33:27 +08:00
evandance
c5b5aece33 refactor: retire legacy error envelopes and enforce typed contract (#1449)
* refactor: retire legacy error envelopes and enforce typed contract

Consolidate all command error reporting onto the typed errs.* contract, remove
the legacy error surface that predated it, and tighten the lint guards so the
contract holds across the whole repository going forward.

Every failure now reaches stderr as one envelope shape: a category, an
optional subtype, a human- and agent-readable message, and a recovery hint,
with invalid parameters listed under `params`. The legacy ExitError envelope,
its constructors, and the boundary bridge that promoted untyped config and
authorization errors are deleted, leaving a single path from error to wire.
Predicate commands keep their silent-exit behavior through a dedicated signal
that carries only an exit code.

Infrastructure paths that still emitted ad-hoc envelopes — flag parsing,
unknown commands and subcommands, plugin and policy guards, confirmation
prompts, and auth/config failures — now classify into the same taxonomy.
Business, API, auth, and config exit codes are preserved; the one behavioral
change is that Cobra usage failures (missing required flag, unknown command,
bad arguments) now emit the typed validation envelope and exit 2, matching the
explicit flag and subcommand guards, instead of Cobra's plain-text exit 1.

Enforcement is repo-wide rather than per-path:
- The errscontract guards run by default everywhere instead of through a
  migration allowlist, so legacy envelopes cannot be reintroduced anywhere.
- errorlint runs across the whole repository: every error wrap must use %w and
  every comparison must use errors.Is/errors.As, so interior wraps stay legal
  but can no longer break the chain the typed boundary relies on.
- The errs-no-bare-wrap guard is keyed by structural prefix instead of an
  explicit per-domain allowlist, so new shortcut domains are covered without
  editing a list. It runs where forbidigo is enabled (the shortcut domains and
  the auth/config/service command groups); repo-wide chain integrity for the
  remaining command paths is carried by errorlint above.

* test: align cli_e2e success assertions to the ok envelope

The api and service success path now emits the {"ok":true} envelope, so the
cli_e2e workflow assertions that still expected the old {"code":0} shape via
AssertStdoutStatus(t, 0) fail once they run with live credentials. Switch those
workflow assertions to AssertStdoutStatus(t, true); the fake-payload helper test
in core_test.go keeps its code-shape assertion.
2026-06-17 19:42:38 +08:00
guokexin.02
4a4c3344c8 fix: align api success envelopes (#1489) 2026-06-17 17:41:48 +08:00
MaxHuang22
24ce3ec151 feat: add --json flag as no-op alias for --format json (#1104)
* 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
2026-06-03 13:58:14 +08:00
evandance
99e314fe0b feat(errs): typed envelope contract for auth-domain errors (#1135)
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.
2026-05-30 19:08:41 +08:00
evandance
fe72e41fb2 feat(errs): add structured CLI error contract (#984)
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.
2026-05-26 11:42:33 +08:00
sang-neo03
33c292c05e feat(extension): Plugin / Hook framework with command pruning (#910)
* feat(extension): introduce Plugin / Hook framework with command pruning

Add a single public extension contract under extension/platform: integrators
implement the Plugin interface and register Observers, Wrappers, Lifecycle
handlers, and pruning Rules through the Registrar in one Install call.

Command pruning:
  - Rule (Allow / Deny / MaxRisk / Identities) with doublestar globs
  - 4-axis AND evaluation, parent-group aggregation, unknown-risk allow
  - Sources: Plugin.Restrict (single-rule) and ~/.lark-cli/policy.yml
  - Plugin path is fail-closed (envelope on rule error / multiple Restrict);
    yaml path is fail-open (warning, CLI continues)
  - strict-mode stubs now also write the denial annotation so the hook
    layer's denial guard physically isolates Wrap chains on them
  - HOME path never leaked through policy_source label

Hook framework:
  - Observer (panic-safe, Before/After), Wrapper (middleware, may short-circuit
    via AbortError), Lifecycle (Startup + Shutdown only)
  - Recover guards every plugin entry point: Capabilities(), Install(),
    Wrapper factory composition AND inner Handler, Lifecycle handlers
  - namespacedWrap copies AbortError so a plugin's package-level sentinel
    is never mutated across concurrent invocations
  - Selector unknown-risk uniform: ByExactRisk / ByWrite / ByReadOnly never
    match unannotated commands; safety-side hooks opt in via
    ByWrite().Or(ByUnknownRisk())

Bootstrap orchestration (cmd/build.go + cmd/policy.go):
  - InstallAll uses a staging Registrar + atomic commit
  - FailClosed plugin install / Plugin.Restrict conflict / Startup handler
    failure each install a structured envelope guard at every dispatch path
  - walkGuard neutralises every cobra bypass we know of (PersistentPreRunE
    first-wins, ValidateArgs, ParseFlags, legacyArgs, __complete /
    __completeNoDesc, non-runnable groups, required-arg subcommands)
  - cmd/root.go::Execute calls hook.Emit(Shutdown, runErr) after
    rootCmd.Execute; isCompletionCommand skips both __complete and
    __completeNoDesc so Tab completion never triggers Shutdown handlers

Capabilities consistency:
  - Restricts=true must declare FailurePolicy=FailClosed
  - RequiredCLIVersion (semver constraint) is validated against build.Version;
    a malformed constraint is treated as untrusted-config and aborts
    unconditionally, regardless of FailurePolicy (DEV builds included)

JSON envelope contract:
  - error.type closed enum: pruning / strict_mode / hook / plugin_install /
    plugin_conflict / plugin_lifecycle
  - reason_code closed enums per type, all referenced by structured tests

Bootstrap surfaces (new user commands):
  - lark-cli config policy show     -- JSON view of the active Rule + source
  - lark-cli config policy validate -- parse + schema + glob check, no apply

Coverage:
  - extension/platform: every public type has a unit test
  - internal/{pruning,hook,platformhost,policydecision,cmdmeta}: full coverage
    of denial guard isolation, AbortError sentinel safety, observer panic
    safety, lifecycle error/panic typing, staging atomic rollback
  - cmd/plugin_integration_test.go: end-to-end through buildInternal with
    synthetic and real command trees
  - cmd/install_guard_test.go: walkGuard covers auth / config / __complete /
    __completeNoDesc / non-runnable parents

* fix(pruning): deny stub must override Args + PersistentPreRunE

The pruning denyStub and the strict-mode stub previously only swapped
RunE plus Hidden + DisableFlagParsing. Cobra's dispatch order means
several pre-RunE gates can fire BEFORE the stub's RunE ever runs:

  1. Args validator: shortcut commands often declare cobra.NoArgs.
     With DisableFlagParsing=true the user's `--doc xxx --mode append`
     looks like positional args, so ValidateArgs surfaces a usage
     error instead of the pruning / strict_mode envelope. Observer
     hooks also miss the dispatch entirely.

  2. Parent PersistentPreRunE: cmd/auth/auth.go declares a
     PersistentPreRunE that returns external_provider when env
     credentials are set. Cobra's "first PersistentPreRunE wins
     walking up from the leaf" then short-circuits with
     external_provider instead of the leaf's denial envelope.

Both stubs now also set:

  - Args               = cobra.ArbitraryArgs   (bypass gate 1)
  - PersistentPreRunE  = no-op leaf hook       (bypass gate 2)
  - PreRunE / PreRun / PersistentPreRun = nil  (defensive)

Effect: dispatch reaches the wrapped RunE, observers fire, the real
pruning / strict_mode envelope is emitted regardless of credential
provider or flag count.

Adds regression tests covering both gates on both stub paths.

* fix(config): policy subcommand bypasses parent's credential check

cmd/config/config.go::NewCmdConfig declares a PersistentPreRunE that
calls f.RequireBuiltinCredentialProvider; with env credentials set,
it returns external_provider for every config subcommand.

`config policy show` and `config policy validate` are READ-ONLY
diagnostic commands -- they inspect or parse the user-layer rule
without touching credentials. They MUST work regardless of which
credential provider is active, otherwise users on env-credential
deployments cannot debug their policy.

Same shape as the codex C11/C13 fix: install a no-op leaf-level
PersistentPreRunE on the `policy` group so cobra's "first walking up
from leaf" rule picks ours over the config parent's.

Regression caught by divergent e2e (F1-F6 all returned external_provider
before this fix; all pass after). Adds a unit test pinning the
PersistentPreRunE override.

* feat(shortcuts): tag service groups with cmdmeta.Domain

RegisterShortcutsWithContext now calls cmdmeta.SetDomain on each
service-level cobra.Command (im, docs, drive, calendar, ...) so the
business-domain axis is actually populated on every shortcut leaf via
parent-chain inheritance.

Before this change, platform.ByDomain("docs") never matched any
command: the domain annotation was unset across the entire shortcut
tree, so the selector's d != "" guard always failed and risk-style
selectors silently degraded to no-op.

The SetDomain call is placed AFTER the create-or-reuse branch so it
fires whether the service command was freshly created here or had
already been added by cmd/service/service.go's OpenAPI auto-
registration (which runs first and creates im, drive, calendar, etc.).
Without this placement only pure-shortcut services like docs would
have been tagged.

Adds a regression test asserting:
  - service-group cobra.Command carries the cmdmeta.domain annotation
  - leaf shortcuts inherit the domain via parent-chain walk

* feat(diagnostic): add unconditionally allowed command paths for introspection

* feat(plugins): add diagnostic command to inspect installed plugins and their contributions

* fix(cli): surface unknown_subcommand error instead of silent help fallback

When a user passed an unknown subcommand or shortcut (e.g. `lark-cli drive
+bogus`), cobra returned `flag.ErrHelp` for the non-runnable group command,
printed the parent help, and exited 0. AI agents couldn't distinguish a
typo from an intentional help request.

Install a tree-wide guard that attaches a RunE to every group command
without its own Run/RunE. The RunE forwards no-args invocations to help
(preserving prior behavior) and emits a structured unknown_subcommand
ExitError (exit 2) listing available subcommands when args are present.

* refactor(envelope): rename error.type pruning/strict_mode to command_denied

The envelope's `type` field was leaking implementation terms ("pruning",
"strict_mode") that describe enforcement mechanism rather than the user-
facing semantic. It also duplicated `detail.layer`, and forced consumers
to branch on two values for the same conceptual error ("a command was
denied by policy").

Collapse both into a single semantic type "command_denied". The
enforcement layer ("pruning" / "strict_mode") is preserved in
`detail.layer` so debugging and per-layer diagnostics still work.

* feat(platform): fail closed on unannotated/invalid risk when a Rule is active

The pruning engine used to treat any command without a risk annotation as
ALLOW even when a Rule with MaxRisk was set, and would silently skip the
MaxRisk comparison whenever the command's risk string was outside the
closed taxonomy. Both gaps let an unannotated or typo'd write command
slip past an "agent read-only" pruning rule.

Engine now denies before any other axis when a Rule is registered:
  - reason_code "risk_not_annotated" for commands with no risk
  - reason_code "risk_invalid"        for commands whose risk is outside
                                      the read | write | high-risk-write
                                      taxonomy (e.g. typo "wrtie")

Main-flow is preserved: a nil Rule still returns Allowed=true
unconditionally, so a CLI with no pruning plugin behaves identically to
before. ByUnknownRisk() is removed from the public surface since the
Unknown state is no longer reachable through risk-based selectors when
any Rule is active; safety-side widening composition is no longer needed.

* chore(config): hide diagnostic policy/plugins commands from --help

`config policy show`, `config policy validate`, and `config plugins show`
are local-introspection-only commands kept behind the pruning
diagnostic whitelist so operators can always inspect why a command was
denied. They do not need to surface in `--help` for AI agents and were
contributing to help noise.

Hide the `policy` and `plugins` parent groups and both `show` /
`validate` leaves. Commands remain callable by exact name and continue
to bypass user-layer pruning via diagnosticPaths.

* style: gofmt

* fix(platform): nil Selector honours None contract; reject multi-doc policy yaml

- selector.go: And/Or/Not now treat nil Selector as None() per godoc,
  preventing runtime panic when composed selectors are invoked.
- schema.go: Parse rejects multi-document YAML input so a stray '---'
  separator can't silently drop trailing policy constraints.

* chore: go mod tidy

* feat(extension/platform): plugin SDK with policy engine, hooks, and Builder

Introduces extension/platform — the in-process plugin SDK external
Go forks of lark-cli use to extend or restrict the command surface.
Plugins compile in via blank import; there is no dynamic loading
and no RPC isolation.

Public SDK (extension/platform):

  - Plugin interface (Name / Version / Capabilities / Install).
  - Registrar verbs: Observe, Wrap, On, Restrict.
  - Hook types: Observer (side-effect, panic-safe, fires Before/After
    RunE), Wrapper (middleware, may short-circuit via AbortError),
    LifecycleHandler (Startup / Shutdown), Selector with nil-safe
    And/Or/Not composition.
  - Risk / Identity are defined string types with closed taxonomies;
    ParseRisk / ParseIdentity convert raw strings with the
    absent-vs-invalid distinction the engine relies on.
  - Builder ergonomic constructor (NewPlugin().Observer().Wrap()
    ...MustBuild()) that enforces name/hookName grammar, hookName
    uniqueness, and the Restrict ↔ FailClosed pairing regardless of
    call order.
  - Invocation is a read-only interface; the framework's concrete
    invocation type lives in internal/hook so plugins cannot
    fabricate denial / strict-mode / identity state. Args() returns
    a defensive copy on every call so hook mutation cannot leak
    into the original RunE.
  - CommandDeniedError + AbortError carry structured fields for the
    closed `command_denied` / `hook` envelope contract.
  - ResetForTesting gated behind //go:build testing.
  - README + godoc examples (Observer / Wrapper / Restrict) + two
    runnable example forks (audit-observer, readonly-policy).

Host (internal/platform, internal/hook, internal/cmdpolicy):

  - InstallAll: staged plugin registration with atomic commit, panic
    isolation, FailOpen / FailClosed semantics, RequiredCLIVersion
    semver check, single-Restrict invariant, duplicate-plugin-name
    detection.
  - hook.Install wraps every runnable cmd.RunE with:
    Before observers (panic-safe) → denial guard → composed Wrap
    chain → original RunE → After observers (always fire, even on
    err). Denied commands physically bypass the Wrap chain so a
    plugin Wrapper cannot suppress or rewrite a denial; observers
    still see the attempt for audit.
  - Recover shim around plugin Wrappers converts panics (including
    the factory call) into a structured `hook` envelope with
    reason_code=panic; namespacing shim attributes AbortError to
    the namespaced hook name.
  - cmdpolicy (renamed from internal/pruning) is the user-layer
    command policy engine: walks the cobra tree, evaluates each
    runnable command against a Rule's four-axis filter (Allow /
    Deny / MaxRisk / Identities), produces parent-group aggregate
    denials, and installs denyStubs. Rule.AllowUnannotated opts out
    of the unannotated-deny gate for gradual adoption; risk_invalid
    typos always deny with an edit-distance "did you mean"
    suggestion.
  - Strict-mode stub in cmd/prune.go composes the shared
    detail.* / wrapped CommandDeniedError shape via cmdpolicy
    helpers (BuildDenialError / CommandDeniedFromDenial /
    DenialDetailMap), so command_denied envelopes from strict-mode
    and user-layer policy carry the same closed-enum fields
    (detail.layer / reason_code / policy_source). The historical
    short Message + independent Hint are preserved unchanged.
  - cmdpolicy/yaml: structural parsing of ~/.lark-cli/policy.yml
    with KnownFields strict mode, including allow_unannotated.
  - `config policy show` / `config policy validate` and the plugin
    inventory diagnostic surface the resolved Rule (allow,
    deny, max_risk, identities, allow_unannotated) and the hook
    contributions per plugin.

Envelope contract (docs/extension/reason-codes.md):

  - error.type is a closed set: command_denied, hook, plugin_install,
    plugin_conflict, plugin_lifecycle.
  - reason_code is a closed enum per error.type, dispatched on by
    external agents and CI integrations.
  - detail.layer = "policy" | "strict_mode" attributes the rejection.

Build / CI:

  - Makefile unit-test / vet / coverage and ci.yml fast-gate +
    unit-test + coverage now pass -tags testing so register_testing.go
    is visible; ./extension/... is in the package list so the SDK's
    own tests actually run.
  - fmt-check and examples-build Makefile targets.
  - bmatcuk/doublestar/v4 added as a direct dependency for `**` glob
    matching in Rule.Allow / Rule.Deny.

Author-facing material:

  - docs/extension/ (quickstart, plugin-author-guide, reason-codes)
    is provided in the working tree but kept out of git tracking
    per repo convention (.gitignore covers docs/).

Change-Id: I3b8ecc2923bd54c2dff19e5dce8a0855a6f9e703

* feat(extension/platform): plugin SDK with policy engine, hooks, and Builder

Introduces extension/platform — the in-process plugin SDK external
Go forks of lark-cli use to extend or restrict the command surface.
Plugins compile in via blank import; there is no dynamic loading
and no RPC isolation.

Public SDK (extension/platform):

  - Plugin interface (Name / Version / Capabilities / Install).
  - Registrar verbs: Observe, Wrap, On, Restrict.
  - Hook types: Observer (side-effect, panic-safe, fires Before/After
    RunE), Wrapper (middleware, may short-circuit via AbortError),
    LifecycleHandler (Startup / Shutdown), Selector with nil-safe
    And/Or/Not composition.
  - Risk / Identity are defined string types with closed taxonomies;
    ParseRisk / ParseIdentity convert raw strings with the
    absent-vs-invalid distinction the engine relies on.
  - Builder ergonomic constructor (NewPlugin().Observer().Wrap()
    ...MustBuild()) that enforces name/hookName grammar, hookName
    uniqueness, and the Restrict ↔ FailClosed pairing regardless of
    call order.
  - Invocation is a read-only interface; the framework's concrete
    invocation type lives in internal/hook so plugins cannot
    fabricate denial / strict-mode / identity state. Args() returns
    a defensive copy on every call so hook mutation cannot leak
    into the original RunE.
  - CommandDeniedError + AbortError carry structured fields for the
    closed `command_denied` / `hook` envelope contract.
  - ResetForTesting gated behind //go:build testing.
  - README + godoc examples (Observer / Wrapper / Restrict) + two
    runnable example forks (audit-observer, readonly-policy).

Host (internal/platform, internal/hook, internal/cmdpolicy):

  - InstallAll: staged plugin registration with atomic commit, panic
    isolation, FailOpen / FailClosed semantics, RequiredCLIVersion
    semver check, single-Restrict invariant, duplicate-plugin-name
    detection.
  - hook.Install wraps every runnable cmd.RunE with:
    Before observers (panic-safe) → denial guard → composed Wrap
    chain → original RunE → After observers (always fire, even on
    err). Denied commands physically bypass the Wrap chain so a
    plugin Wrapper cannot suppress or rewrite a denial; observers
    still see the attempt for audit.
  - Recover shim around plugin Wrappers converts panics (including
    the factory call) into a structured `hook` envelope with
    reason_code=panic; namespacing shim attributes AbortError to
    the namespaced hook name.
  - cmdpolicy (renamed from internal/pruning) is the user-layer
    command policy engine: walks the cobra tree, evaluates each
    runnable command against a Rule's four-axis filter (Allow /
    Deny / MaxRisk / Identities), produces parent-group aggregate
    denials, and installs denyStubs. Rule.AllowUnannotated opts out
    of the unannotated-deny gate for gradual adoption; risk_invalid
    typos always deny with an edit-distance "did you mean"
    suggestion.
  - Strict-mode stub in cmd/prune.go composes the shared
    detail.* / wrapped CommandDeniedError shape via cmdpolicy
    helpers (BuildDenialError / CommandDeniedFromDenial /
    DenialDetailMap), so command_denied envelopes from strict-mode
    and user-layer policy carry the same closed-enum fields
    (detail.layer / reason_code / policy_source). The historical
    short Message + independent Hint are preserved unchanged.
  - cmdpolicy/yaml: structural parsing of ~/.lark-cli/policy.yml
    with KnownFields strict mode, including allow_unannotated.
  - `config policy show` / `config policy validate` and the plugin
    inventory diagnostic surface the resolved Rule (allow,
    deny, max_risk, identities, allow_unannotated) and the hook
    contributions per plugin.

Envelope contract (docs/extension/reason-codes.md):

  - error.type is a closed set: command_denied, hook, plugin_install,
    plugin_conflict, plugin_lifecycle.
  - reason_code is a closed enum per error.type, dispatched on by
    external agents and CI integrations.
  - detail.layer = "policy" | "strict_mode" attributes the rejection.

Build / CI:

  - Makefile unit-test / vet / coverage and ci.yml fast-gate +
    unit-test + coverage now pass -tags testing so register_testing.go
    is visible; ./extension/... is in the package list so the SDK's
    own tests actually run.
  - fmt-check and examples-build Makefile targets.
  - bmatcuk/doublestar/v4 added as a direct dependency for `**` glob
    matching in Rule.Allow / Rule.Deny.

Author-facing material:

  - docs/extension/ (quickstart, plugin-author-guide, reason-codes)
    is provided in the working tree but kept out of git tracking
    per repo convention (.gitignore covers docs/).

Change-Id: I3b8ecc2923bd54c2dff19e5dce8a0855a6f9e703

* refactor(policy): remove validate command and update diagnostics

* fix(extension/platform): address PR review must-fix items

- cmdpolicy: skip AnnotationPureGroup commands in EvaluateAll,
  aggregateParents, and hasRunnableDescendant so user-layer policy
  no longer blocks `<group> --help` after the unknown-subcommand
  guard attaches RunE to every parent
- cmd/root: tag guarded parent groups with AnnotationPureGroup
- extension/platform: drop `//go:build testing` from register_testing.go
  so `go test ./...` works without an extra build tag
- extension/platform/README: inline reason_code reference, fix plugin
  lifecycle diagram order (init/Register precede RegisteredPlugins)
- cmd/platform_bootstrap: route userPolicyPath through
  core.GetBaseConfigDir so LARKSUITE_CLI_CONFIG_DIR is honoured
- cmdpolicy: add RedactHomeDir helper, fold base config dir and
  $HOME prefixes for config policy show + resolver errors
- internal/platform: reject unrecognised FailurePolicy values with
  invalid_capability instead of silently fail-open
- cmd/config: surface diagnostic policy/plugins commands in
  `config --help` Long text
- CHANGELOG: document command_denied error.type rename and
  unknown_subcommand exit-2 behavior change

* fix(extension/platform): address CodeRabbit review comments + CI gofmt

- hook/install: propagate wrapper-injected ctx to invokeOriginal so
  RunE/Run see context values added by upstream Wrappers
- hook/testing: SetStderrForTesting returns a restore func; tests now
  defer it via t.Cleanup to avoid cross-test sink leakage
- cmdpolicy/active: deep-copy ActivePolicy.Rule on SetActive/GetActive
  so callers can't mutate the stored global through shared slices
- platform/inventory: deep-copy Inventory + nested Plugins / HookEntry
  / RuleView slices on SetActiveInventory / GetActiveInventory
- platform/staging: Restrict clones the plugin-supplied Rule before
  retaining it so the plugin can't mutate it after Install returns
- platform/version: reject RequiredCLIVersion with more than three
  numeric components instead of silently truncating 1.2.3.4 to 1.2.3
- cmd/platform_bootstrap: clear cmdpolicy.SetActive on yaml resolver
  error so config policy show doesn't surface a stale rule
- cmd/platform_bootstrap_test: tmpHome pins LARKSUITE_CLI_CONFIG_DIR
  so host env can't bleed into the policy test fixtures
- cmdpolicy/apply: installDenyStub returns bool; Apply count no longer
  over-reports when strict-mode short-circuits the install
- cmdpolicy/engine: aggregateParents now returns the runnable hybrid's
  own denial status when all children are placeholder branches
- cmdpolicy/resolver_test: use t.TempDir()-rooted missing path instead
  of hardcoded /nonexistent for hermetic missing-file assertion
- cmd/config/plugins: empty-inventory branch emits total: 0 so the
  JSON schema stays stable across populated/empty cases
- cmd/platform_guards_test: select leaf by RunE != nil (not Runnable)
  so the test doesn't nil-deref on Run-only commands
- gofmt run on previously committed cmdpolicy/path*.go (CI fast-gate)

* fix(cmdpolicy): replace filepath.Abs with filepath.Clean for lint policy

The depguard / forbidigo rule blocks filepath.Abs in internal/ on the
grounds that it accesses the filesystem (Getwd) directly. Switch
RedactHomeDir + foldPrefix to operate on filepath.Clean strings; real
callers pass already-absolute paths (resolver builds yamlPath via
filepath.Join on the absolute config root), so the redaction outcome
is unchanged for production inputs. Relative inputs fall through to
the unchanged branch — filepath.Rel rejects the mixed-absoluteness
case with an error, which the foldPrefix helper already treats as
"not a hit".

* refactor(cmdpolicy): pure Resolve + drop path redaction & verbose comments

- Resolve becomes a pure function; I/O moves to LoadYAMLPolicy so
  precedence selection can be unit-tested without vfs mocks
- ActivePolicy drops YAMLPath; config policy show JSON loses yaml_path
  and yaml_shadowed (and the TOCTOU stat that surfaced them)
- RedactHomeDir and path_test.go removed: the home-dir folding was only
  earning its keep through the now-deleted yaml_path field
- cmd/build.go bootstrap block trimmed from 71 to 39 lines by cutting
  PR-rationale comments; one note kept for the fail-CLOSED-vs-fail-OPEN
  business rule
- cmd/config/config.go: parent Long no longer hard-codes hidden command
  hints, matching their Hidden:true intent

Change-Id: Icfbb818ce3ef523c63286bfbed34c49be08ed6a2

* refactor(platform): drop StrictMode/Identity from Invocation interface

These two accessors were documented in the public SDK as "After observers
always see ok=true" but the framework never plumbed values to them, so they
always returned ("", false). Zero internal/example/test callers; a plugin
author trusting the doc would silently get wrong behaviour.

Identity is also fundamentally unsuited for Before observers (per-command
identity resolves inside RunE via f.AuthFor, after Before fires). StrictMode
is a global value better placed on a Framework/Environment interface than
per-Invocation. Removing is non-breaking now (no callers); adding later is
non-breaking too.

Change-Id: Ice200543e9bca3bda759ad98a6e34a56df69e915

* fix(prune): preserve original metadata on strict-mode denial stubs

strictModeStubFrom built a fresh *cobra.Command from scratch, dropping
the original command's annotations (risk_level, lark:supportedIdentities,
cmdmeta.domain) and help text. cobraCommandView is a live proxy walking
parent annotations, so after the Remove+Add replacement, audit observers
firing on a strict-mode-denied command saw Cmd().Risk()=("",false) and
Cmd().Identities()=nil -- breaking the first-class use case for
audit/compliance plugins.

Copy child.Annotations into the stub (stamping the denial annotations on
top) and propagate Short/Long for help-text parity with
cmdpolicy/apply.go::installDenyStub, which preserves these by virtue of
mutating in place.

Regression test asserts risk_level / supportedIdentities / Short / Long
all survive replacement, alongside the denial annotations.

Change-Id: I19810a34575996344b63e839066888c154d69335

* chore(platform): align docs with implementation; fold home in yaml warnings

Followup cleanup to the previous three refactor commits, addressing review
fallout where public docs / examples / contract notes still pointed at
deleted symbols or unimplemented designs:

- cmd/build.go: Build() docstring now mentions the plugin install + Startup
  emit side effects; Shutdown only fires on Execute path
- extension/platform/doc.go, lifecycle.go, invocation.go: drop references
  to the deleted StrictMode/Identity methods, restore minimal Godoc on
  Cmd/Args/Started
- extension/platform/view.go, cmd/platform_bootstrap.go,
  internal/hook/install.go: rewrite "snapshot before pruning" promise to
  match the actual contract (live view + strict-mode stub metadata
  preservation)
- cmd/platform_guards_test.go: stubInvocation drops the two old methods
- cmd/platform_bootstrap.go: redactHome() last-mile folds $HOME -> ~ in
  warnPolicyError so an os.PathError carrying the absolute policy path
  does not leak the user's home dir to stderr / agent / CI logs
- examples/readonly-policy/README.md: drop yaml_path from the sample
  `config policy show` envelope (the field was removed in 52cbb92)

Change-Id: I2874cc2cf9225dfa44a9c07b2449149181b387cb

* chore(build): drop vestigial -tags testing from Makefile and CI

The `testing` build tag was introduced in 461e3c6 to gate
extension/platform/register_testing.go (ResetForTesting); PR review
0efee93 then dropped the //go:build testing directive from that file
so downstream `go test ./...` would work without the tag, but never
cleaned the matching tag references out of Makefile and ci.yml.

The result: 8 places passing -tags testing for a tag that nothing in
the repo actually gates, plus a Makefile comment that confidently
claims a gate exists. Net behaviour is identical to omitting the flag;
the only effect is misleading developers into believing there is a
test-only surface separation.

Drop the flag from vet / unit-test / lint / coverage / deadcode (head
+ base worktree) and remove the misleading comment. ResetForTesting's
public-API exposure was the conscious trade-off taken in 0efee93 and
is left untouched.

Change-Id: If0cd78c87d4aec2a2533419fe75b01aae6b165fd

* feat(cmdpolicy): enrich denial Reason with attempted value + rule constraint

The envelope reason for command_denied previously told the caller WHAT
axis failed but not the concrete values on each side, so an AI agent
reading the envelope could not tell which command identity / risk /
path was attempted vs. which the rule permits. The natural temptation
was then to recommend modifying the rule -- exactly the wrong nudge,
since policy exists to prevent the agent from rewriting its own limits.

Each Reason now carries both the attempted value and the rule's
constraint:

  identity_mismatch:
    "command supports identities [user]; rule allows [bot]"
  domain_not_allowed:
    "command path \"drive/+upload\" not in allow list [docs/** contact/**]"
  command_denylisted:
    "command path \"docs/+delete-doc\" matched deny pattern \"docs/+delete-*\""
  risk_too_high / write_not_allowed:
    "command risk \"high-risk-write\" exceeds rule max_risk \"write\""
  risk_not_annotated:
    "command has no risk_level annotation; rule denies unannotated commands"
    (drops the prescriptive "set allow_unannotated=true" hint -- that
     belongs in docs, not in the engine's denial path)

Adds firstMatch() helper so command_denylisted can name the specific
glob that fired; matchesAny() now wraps firstMatch.

Regression test pins the substring contract per reason_code so future
"comment cleanup" cannot silently strip the values out again.

Change-Id: I17c7cc9411f58e3e43ade5e1ce875f3b7fe3e5ea

* fix(cmdpolicy): gofmt engine_test.go

CI fast-gate flagged the test added in 2eb0c2b as unformatted. Local
make unit-test had it cached; should have run `make vet` (which runs
gofmt-equivalent check via fmt-check) before pushing. Trivial 3-line
indent fix.

Change-Id: I42297ae59f607b97b32e976c9ec1c9ec4ab7de21

* feat(cmd): annotate risk_level on all hand-written cobra commands

Without this, any non-empty user-layer policy.yml (default
allow_unannotated=false) denies these commands with reason_code
risk_not_annotated -- bricking auth login, config init, profile use
etc. on first contact with a policy.

cmdpolicy/engine evaluation now resolves to the intended axis (deny
list / allow list / max_risk / identities) instead of failing closed
on the unannotated gate. Policy authors can write `max_risk: write`
or `allow: [auth/** config/** ...]` to express real intent.

Classification:
  read              auth status/check/list/scopes, config show /
                    policy show / plugins show, doctor, completion,
                    schema, profile list, event list/status/schema/
                    consume
  write             auth login/logout, config init/bind/remove/
                    default-as/strict-mode, profile add/remove/
                    rename/use, event stop/_bus, api (raw transit)
  high-risk-write   update (replaces the CLI binary; failure can
                    leave the install broken)

Notes:
- api standalone is conservatively `write`; per-call risk is unknown
  at parse time (raw transit), so static gating only enforces the
  write-class minimum.
- event _bus is the hidden IPC daemon forked by consume; standalone
  invocation by users is not expected, but the annotation keeps
  policy evaluation consistent with the other event subcommands.
- The two diagnostic-allowlisted commands (config policy show /
  plugins show) still bypass the engine via diagnosticPaths; the
  read annotation is for consistency with surrounding leaves.

---------

Co-authored-by: liangshuo-1 <266696938+liangshuo-1@users.noreply.github.com>
2026-05-18 15:25:02 +08:00
liangshuo-1
c100ca049e feat(cmdutil): support @file for params and data (#724)
* feat(cmdutil): support @file for --params/--data (issue #705)

Inline JSON values for --params/--data are mangled by Windows
PowerShell 5's CommandLineToArgvW. Stdin (-) was the only escape
hatch but supports just one flag at a time.

Extend ResolveInput to accept @<path> (read JSON from a file) and
@@... (escape for a literal @-prefixed value), mirroring the
shortcuts framework's resolveInputFlags semantics. With this, both
--params and --data can be sourced from files in the same call,
sidestepping shell quoting on every platform.

- internal/cmdutil/resolve.go: add @path / @@ handling, trim file
  content like stdin does, error on empty path or empty file
- internal/cmdutil/resolve_test.go: cover file read, whitespace
  trim, missing file, empty path, empty content, @@ escape, plus
  ParseJSONMap / ParseOptionalBody integration through @file
- cmd/api/api.go, cmd/service/service.go: update --params/--data
  help text to mention @file

Change-Id: I366aa0f5783fbec6f05403f7f542505098a98c82

* refactor(cmdutil): route @file through fileio.FileIO abstraction

The first cut of @file support called os.ReadFile directly inside
ResolveInput, bypassing the codebase's fileio.FileIO abstraction
(SafeInputPath validation, pluggable provider). That diverged from
how every other file-reading path works: BuildFormdata for --file
uploads and the shortcuts framework's resolveInputFlags both go
through fileio.FileIO.Open with explicit fileio.ErrPathValidation
handling.

Re-route @file through the same path:

- ResolveInput, ParseJSONMap, ParseOptionalBody now take a
  fileio.FileIO; @path uses fileIO.Open which goes through
  SafeInputPath (control-char rejection, abs-path rejection,
  symlink-escape check) — same security posture as --file
- cmd/api and cmd/service callsites pass
  Factory.ResolveFileIO(ctx); the upload path now reuses the
  resolved fileIO instead of resolving twice
- Path-validation errors surface as
  `--params: invalid file path "...": ...` distinct from
  `--params: cannot read file "...": ...` for genuine I/O errors
- Nil fileIO with an @path returns a clear
  "file input (@path) is not available" error
- Tests use localfileio.LocalFileIO with TestChdir(t, dir),
  matching the existing fileupload_test.go pattern; absolute-path
  rejection and nil-fileIO are covered

This makes the feature behave identically under any FileIO
provider (including server mode) instead of being silently bound
to the local filesystem.

Change-Id: I878c4e8fb03f43f1f19afad75ec3af9cdab7a7f9

* refactor(cmdutil): share at-file input handling

Change-Id: I92a6eb6ea8fd02054bf8f4925cd81807449d5e51
2026-04-30 15:34:45 +08:00
MaxHuang22
600fa50517 feat: add configurable content-safety scanning (#606)
* feat(contentsafety): add extension interface layer with Provider, Alert, and registry

Change-Id: Ibeac6366c7201293057bc3b063f75ac34565bcd5

* feat(contentsafety): add normalize utility for JSON type conversion

Change-Id: I7d4729a5ddcab2553abc110f8f6ecc88435ae921

* feat(contentsafety): add tree walker and regex scanner

Change-Id: I215dad7cf3072711d05e45f7d384162e1f8752d4

* feat(contentsafety): add config loading with lazy creation, default rules, and allowlist matching

Change-Id: I75e10df28f1f8d4f433cb2b469a0ff317af3bf70

* feat(contentsafety): add regex provider with config-driven scanning and allowlist

Change-Id: I658889b3647cbbbde6881e0c5f7c13887a1eb1d4

* feat(contentsafety): add output core with mode parsing, path normalization, and scan orchestration

Change-Id: I1cb9df75f1a4d176d660e2e7a9561314c3787191

* feat(contentsafety): add ScanForSafety entry point and Envelope alert field

Change-Id: I5fdb311e1c8d983a35a58667970b9fd3ac729a5c

* feat(contentsafety): integrate scanning into shortcut Out() and OutFormat()

Change-Id: I33eef1dba14c8a9bd1998857311bdd611f33b916

* feat(contentsafety): integrate scanning into API/service output paths and register provider

Change-Id: Ic3981db6c546a19eadea095d82175f92f4783bec

* fix(contentsafety): emit stderr notice when lazy-creating default config

Change-Id: Ia2491f7a17caceea3125ff9fb58d750dc196d7e7

* style: gofmt factory_default and exitcode

Change-Id: I86c5afdfbbdb68d8137f0ca09ef3b5a1139f4b4e

* fix(contentsafety): vfs for config I/O, mutex for lazy-create, sort matched rules, emit warn on --output path

Change-Id: Ib4982cd54e1bfe0580a0eb03368e6ca818304e1b

* fix(contentsafety): isolate scan goroutine errOut to prevent race on timeout

Change-Id: Ia5a770d7387ba6d3b7fa318fc5f1384214ea10b7

* fix(contentsafety): deep-normalize typed slices so scanner can walk shortcut data

Change-Id: I641e89113d1a2f2285ac6109bd3d7264f5845ea7

* fix(contentsafety): file perms 0600/0700, no result mutation, timeout test, scanTimeout comment

Change-Id: Ie45a2e365ee7098e214e94f8871026cc12029d83
2026-04-23 17:18:29 +08:00
tuxedomm
11191df703 fix: skip flag-completion registration outside completion path (#598)
* fix: skip flag-completion registration outside completion path

Cobra keeps completion callbacks in a package-global map keyed by
*pflag.Flag with no removal path, so registrations made during Build()
outlive the command itself. Route all seven call sites through
cmdutil.RegisterFlagCompletion and enable registration only when the
invocation actually serves a __complete request.

Measured over 30 dropped Builds: ~202 KB / 2180 retained objects per
Build before, ~0 after.

Change-Id: I734d598a4c91a92c33b02e0f292f640cc0e224c6
2026-04-22 11:55:11 +08:00
tuxedomm
fbed6beac3 refactor: split Execute into Build + Execute with explicit IO and keychain injection (#371)
* refactor(cmd): split Execute into Build with IO/Keychain injection

Introduce a public cmd.Build entry point so external consumers (cli-server,
MCP server, other embedders) can assemble the full CLI command tree without
going through os.Args or the platform keychain. Build takes an
InvocationContext plus functional BuildOptions:

  * WithIO(in, out, errOut) — inject custom streams; terminal detection
    is derived from the input's underlying *os.File when present.
  * WithKeychain(kc)        — swap the credential store.
  * HideProfile(bool)       — registered later in cmd.HideProfile.

The existing Execute() keeps using the internal buildInternal (which
still returns the Factory so error handling can attribute exit codes),
and SetDefaultFS replaces the global VFS implementation at startup.

Hardening applied up front:

  * cmdutil.NewIOStreams(in, out, errOut) centralizes terminal detection
    so SystemIO() and WithIO share one path.
  * cmdutil.NewDefault normalizes partial IOStreams — callers may pass
    &IOStreams{Out: buf} without tripping nil-writer panics in the
    RoundTripper warnings, Cobra, or the credential provider.
  * Build guards against nil functional options.
  * An API contract test (cmd/build_api_test.go) exercises Build +
    WithIO + WithKeychain + HideProfile + SetDefaultFS so the public
    surface is reachable by deadcode analysis.

Change-Id: I7c895e6019817401accbde2db3ef800da40ad319

* feat(schema): filter methods by strict mode in schema output

When strict mode is active, schema output now excludes methods that
are incompatible with the forced identity. This applies to both
pretty and JSON output formats at the resource and method levels.

Change-Id: I39647d5578466c3e23dc545bfb917ae075203ad7

* refactor: centralize strict-mode as flag registration

Change-Id: Iec11151c5002c2f58a8aa067d08747db2e4d2d8c

* fix(cmd): align strict-mode completion and build context; drop dead register shims

Thread a context.Context through RegisterShortcuts, RegisterServiceCommands,
and service.registerService/Resource/Method by introducing explicit
*WithContext variants. Pass that context into NewCmdServiceMethodWithContext
so shortcut and service command construction can honor cancellation and
strict-mode pruning consistently.

Also drop the context-less registerMethod and registerResource shims —
they became unreachable once the WithContext variants took over, and
were the source of new deadcode warnings. registerService is retained
because service_test.go still calls it directly.

Change-Id: I3fe5673aed663c7383bbbc5b0ae94d1f3491f22d

* refactor(cmd): hide --profile in single-app mode via build option

- GlobalOptions gains HideProfile; RegisterGlobalFlags stays pure and reads
  the policy off the struct. No boolean-trap parameter, one call per site.
- buildConfig holds GlobalOptions inline so HideProfile(bool) BuildOption
  mutates it directly. buildInternal stays a pure assembly function and
  requires callers to supply WithIO — no implicit os.Std* fallback.
- Add WithIO BuildOption (wrapping raw io.Reader/Writer with automatic
  *os.File TTY detection); Execute injects streams explicitly and decides
  profile visibility via HideProfile(isSingleAppMode()).
- installTipsHelpFunc force-shows hidden root flags while rendering the
  root command's own help, so single-app users still discover --profile
  via lark-cli --help without it polluting subcommand helps.

Change-Id: I7755387e993992ca969e0a4a6f54441cc1993eef

* feat(transport): extension abort hook and shared base transport

Two transport-layer changes bundled because both reshape the base
round-tripper contract used by the HTTP client, the Lark SDK client,
and the in-process updater.

1. Extension abort hook (PreRoundTripE).

   Extensions implementing exttransport.AbortableInterceptor can now
   return an error from PreRoundTripE to skip the built-in chain. The
   post hook still fires with (nil, reason) so extensions can unwind
   resources. extensionMiddleware captures the provider name so the
   returned *AbortError carries attribution.

2. Shared base transport to stop RPC leak.

   util.NewBaseTransport cloned http.DefaultTransport on every call, so
   each cmdutil.Factory produced a fresh *http.Transport whose
   persistConn readLoop/writeLoop goroutines lingered until
   IdleConnTimeout (~90s). Invisible in a single-process CLI, but the
   fork is consumed by cli-server where each RPC request constructs a
   new Factory, causing linear memory + goroutine growth under load.

   Replace NewBaseTransport with SharedTransport — returns
   http.DefaultTransport (the stdlib-wide singleton) by default, and
   a cached proxy-disabled clone only when LARK_CLI_NO_PROXY is set.
   Return type is http.RoundTripper to discourage in-place mutation of
   the shared instance. FallbackTransport is kept as a thin
   *http.Transport wrapper so existing callers in internal/auth and
   internal/cmdutil transport decorators (which were already on the
   singleton path) do not have to migrate.

   Leak-site migrations: factory_default.go (HTTP + SDK base) and
   update.go now call SharedTransport directly.

Change-Id: Ia82462134c5c5ee838be878b887860f41446a235

* fix: unblock Build() zero-opts path and sidecar demo build

Two regressions surfaced on refactor/build-execute-split:

1. cmd.Build(ctx, inv) without WithIO panicked at rootCmd.SetIn/Out/Err
   because cfg.streams stayed nil — NewDefault normalized internally
   but cmd/build.go never saw the normalized value. Default cfg.streams
   to cmdutil.SystemIO() before the root command wires them, and add a
   TestBuild_NoOptions regression guard.

2. sidecar/server-demo/main.go still called cmdutil.NewDefault(inv),
   so `go build -tags authsidecar_demo ./sidecar/server-demo` failed
   with "not enough arguments". Pass nil for the new streams parameter
   to preserve the prior behavior (NewDefault substitutes SystemIO).

Change-Id: I20227b2355cde7d19e22eba3eb841c6d8611e8a7
2026-04-21 14:48:40 +08:00
MaxHuang22
344ff88701 feat: add --file flag for multipart/form-data file uploads (#395)
* feat(cmdutil): add shared file upload helpers

Add ParseFileFlag, ValidateFileFlag, and BuildFormdata to support
multipart file upload via --file flag across raw API and meta API commands.

Change-Id: Ib724cf8b055b0b314af11d8d830f38559dac60eb

* feat(api): add --file flag for multipart/form-data file uploads

Add --file flag to `lark-cli api` command enabling file upload via
multipart/form-data. The flag accepts [field=]path format and supports
stdin (-). Includes mutual exclusion validation with --output,
--page-all, and GET method. Dry-run mode shows file metadata instead
of building actual formdata.

Change-Id: Icf34aba5da3a558219a97a583e8f6aa951ded199

* feat(service): add --file flag with auto-detection from metadata

Add file upload support to meta API service method commands. The --file
flag is conditionally registered only for methods whose metadata declares
file-type fields (POST/PUT/PATCH/DELETE). The default field name is
auto-detected from metadata when exactly one file field exists.

Change-Id: Ibbf04eb42341ba11bb1fd9750e63bc1d0eacd08d

* feat(schema): show file upload indicators in method detail display

Add hasFileFields helper to detect file-type fields in requestBody
metadata. Modify printMethodDetail to display [file upload] tag on
--data line, --file flag description with default field name, and
--file <path> in CLI example for methods that accept file uploads.

Change-Id: Iae3bc14fe07e16a8b5f6a50a2b3592d6d8490ed9

* fix: address code review findings for file upload feature

- ParseFileFlag: change idx >= 0 to idx > 0 to prevent empty field name
  when input like "=photo.jpg" is passed
- BuildFormdata: read file into bytes.Reader with defer Close to prevent
  file handle leak on later errors
- BuildFormdata: remove unused ctx parameter from signature and callers
- Eliminate duplicated dry-run logic by having buildAPIRequest and
  buildServiceRequest return FileUploadMeta when in dry-run mode,
  removing ~60 lines of copy-pasted URL building and validation code

Change-Id: I27b9534fd0eaefce40390f6e723dd0c04a2cdf80

* fix: address PR review findings

- Remove opts.File=="" guard on dual-stdin check so --file photo.jpg
  --params - --data - correctly reports an error instead of silently
  dropping --data content (P1 bug in both api.go and service.go)
- Extract shared DetectFileFields into cmdutil, deduplicate
  detectFileFields (service.go) and hasFileFields (schema.go)
- Show "<stdin>" instead of empty path in dry-run output for --file -

Change-Id: Iccc5d879165ea6a3d04f0425ec6a5018a10e72e1

* fix: reject non-object --data with --file and improve multi-file schema

- --data with --file now requires a JSON object; arrays/strings/numbers
  are rejected with a clear error instead of being silently dropped
- Schema display for multi-file methods shows explicit field=path syntax
  and lists valid field names instead of advertising a false default

Change-Id: I0facdb3ad86f68cb125c7ea109a33714fd91dba0
2026-04-10 17:49:41 +08:00
liangshuo-1
619ec8c2cb fix(api): support stdin and quoted JSON inputs on Windows (#367)
* fix(api): add stdin and single-quote support for --params/--data on Windows (#64)

Windows PowerShell 5.x mangles JSON double-quotes when passing arguments
to native executables, causing --params and --data to fail with
"invalid JSON format". This commit adds two mitigations at the framework
level:

- stdin piping: `echo '{"k":"v"}' | lark-cli --params -` bypasses
  shell argument parsing entirely and works on all platforms/shells.
- single-quote stripping: cmd.exe passes literal single quotes which
  are now transparently removed before JSON parsing.

Implementation:
- New `cmdutil.ResolveInput(raw, stdin)` handles `-` (stdin), strip
  surrounding `'...'`, and plain passthrough.
- `ParseJSONMap` and `ParseOptionalBody` now accept an `io.Reader` and
  delegate to `ResolveInput` before JSON unmarshalling.
- `cmd/api` and `cmd/service` pass `IOStreams.In` and guard against
  simultaneous stdin usage by --params and --data.
- Empty stdin is rejected with a clear error message.

Closes #64

Change-Id: If21e735d0aed5c6a2d6674c1e6c898186fca3aba

* test: add stdin e2e regression coverage

Change-Id: I4e00bf1c6b6f3259f503e3414cae10fa4b34ba75
2026-04-09 19:10:50 +08:00
tuxedomm
f5a8fbf8f1 refactor: migrate common/client/im to FileIO and add localfileio tests (#322)
* refactor: migrate common/client/im to FileIO and add localfileio tests

- runner resolveInputFlags: replace validate.SafeInputPath + vfs.ReadFile
  with FileIO.Open + io.ReadAll
- SaveResponse: delegate to FileIO.Save + ResolvePath
- cmd/api, cmd/service: pass FileIO to ResponseOptions
- im: replace validate.SafeLocalFlagPath with RuntimeContext.ValidatePath,
  migrate download/upload to FileIO.Save/Open/Stat
- Add path_test.go and atomicwrite_test.go for localfileio
- Add validate_media_test.go for im media flag validation
- Adapt test mocks to fileio.FileInfo interface
2026-04-08 17:31:21 +08:00
Vux
a73c9ae27e fix: improve raw API diagnostics for invalid or empty JSON responses (#257)
- Add internal/client/api_errors.go with WrapDoAPIError and WrapJSONResponseParseError to classify JSON decode issues vs generic network errors
- Route cmd/api DoAPI errors and HandleResponse JSON parse errors through the new helpers
- Add regression tests in cmd/api and internal/client

Related: https://github.com/larksuite/cli/issues/215
2026-04-08 14:28:02 +08:00
liangshuo-1
8db4528269 feat: add strict mode identity filter, profile management and credential extension (#252)
* feat: add strict mode identity filter, profile management and credential extension

Port changes from feat/strict-mode-identity-filter_3 branch:
- Add strict mode for identity filtering and configuration
- Add profile management commands (add/list/remove/rename/use)
- Add credential extension framework (registry, env provider)
- Add VFS abstraction layer
- Refactor factory default and client options
- Update shortcuts to use new credential and validation patterns

Change-Id: I8c104c6b147e1901d94aefcefe35a174932c742b
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: go mod tidy

Change-Id: I0f610ccea6bc874248e84c24770944a3071dcc57
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: fix test failures from credential provider migration

- Remove unused TAT stub registrations in api and service tests
  (CredentialProvider manages tokens, SDK no longer calls TAT endpoint)
- Update strict mode integration test: +chat-create now supports user
  identity, so it should succeed under strict mode user

Change-Id: Iab51c2e12a97995e0b95dcd71df212d2d1f76570
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: migrate remaining os calls to internal/vfs

Replace direct os.Stat/Open/MkdirAll/OpenFile/Remove/ReadDir/UserHomeDir
with vfs equivalents in shortcuts/minutes, shortcuts/drive, and
internal/keychain. Add ReadDir to the vfs interface and OsFs implementation.

Change-Id: I8f97e5fb3e1731b4684d276644fcb10fae823067

* fix: resolve gofmt and goimports formatting issues

Change-Id: If61578631f5698f7ca2d9a946ca59753651463fb

* feat: add Flag.Input support for @file and stdin input sources

Add framework-level support for reading flag values from files (@path)
or stdin (-), solving the fundamental problem of passing complex text
(markdown, multi-line content) via CLI arguments where shell escaping
breaks content. Closes #239, fixes #163.

- Add File/Stdin constants and Input field to Flag struct
- Add resolveInputFlags() in runner pipeline (pre-Validate)
- Support @@ escape for literal @ prefix
- Guard against multiple stdin consumers
- Auto-append "(supports @file, - for stdin)" to help text
- Apply to: docs +create/+update --markdown, im +messages-send/+reply
  --text/--markdown/--content, task +comment --content,
  drive +add-comment --content

Change-Id: I305a326d972417542aeadd70f37b74ea456461ef
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: fix pre-existing test failures in task, minutes, and registry

- task/minutes: remove unused tenant_access_token httpmock stubs
  (TestFactory's testDefaultToken provides tokens directly, so the
  HTTP stub was never consumed and failed verification)
- registry: fix hasEmbeddedData() to check for actual services instead
  of just byte length (meta_data_default.json has empty services array)

Change-Id: Ic7b5fc7f9de09137a7254fe1ddf47d24ade40587
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: suppress nilerr lint for intentional nil returns

Both cases intentionally return nil on error for graceful degradation:
- profile list: show friendly message when config is not initialized
- service: skip scope check when token resolution fails

Change-Id: I7285c37277c9b0361a421ab00359244c2cd150b3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback

- runner.go: fail fast when Input is used on non-string flags
- remote_test.go: rename hasEmbeddedData → hasEmbeddedServices
- profile/list.go: add omitempty to optional JSON fields
- service.go: surface context cancellation errors in scope check

Change-Id: I7072d41f8c711b4b37c542e32dfd8150f42b13c0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: tighten credential resolution and profile flows

Change-Id: I83f6d424540eab9b1708944b9b6e26e8477cc60d

* refactor: centralize identity hint resolution

Change-Id: I38d5f98160b92adb62dc929ae73697ae5b3d64f8

* fix: surface unverified extension identities

Change-Id: Ia86d9bd19add9010176339ec4cc89deb033f5b4f

* fix: honor runtime credential sources in config views

Change-Id: I40b2ffedc5c1db5e08e86b9472ea2b84fa02bb29

* fix: prefer runtime values in config show commands

Change-Id: I5663a53e147577f0f1f533f67d12bea504e6b839

* Revert "fix: prefer runtime values in config show commands"

This reverts commit 4f9db3a227.

* Revert "fix: honor runtime credential sources in config views"

This reverts commit b3bfd526c5.

* fix: harden profile flows and credential boundaries

Change-Id: Ica61cd2730a639f71516cb1b237a639cb6511f7a

* fix: optimize profile and config inspection for agents

Change-Id: I19c368102f19654952638180ab947788a6971563

* refactor: unify credential env contracts

Change-Id: I0ff2c0a650ea53589a0626333e8f6e628ef10a54

* docs: expand AGENTS guidance

Change-Id: I289027dfd364c92205012feef6f05037066c035b

* fix: resolve regression bugs found during PR #252 review

- im: fix double SafeInputPath in resolveLocalMedia → uploadImageToIM/
  uploadFileToIM chain that rejected all local image/file uploads
- credential: stop writing plain-text warnings to stderr, preserving
  JSON envelope contract for AI agent consumers
- profile add: reject duplicate app-id to prevent keychain credential
  collisions across profiles
- profile rename: exclude self when checking name uniqueness so renaming
  to own appId works correctly
- config: replace bare fmt.Errorf with output.Errorf in save-failure
  paths (default_as, strict_mode ×2, profile add)
- factory: remove unused resolveDefaultAs method (lint)

Change-Id: I6aa0d064414016f367f1edb08dd0604adf7bf13d
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove flaky TestColdStart_UsesEmbedded (race in registry)

The test triggers a data race: resetInit() writes package globals while
a background goroutine from a previous test may still be reading them.
The embedded-data path is covered by other tests.

Change-Id: I7a0c3bf85a9fb337b9279c9053697f40a0c0a0d4
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: type-strengthen Brand and DefaultAs across credential chain

Replace raw string fields with typed enums for compile-time safety:
- extension/credential: add Brand and Identity named types
- internal/core: AppConfig.DefaultAs and CliConfig.DefaultAs → Identity
- internal/credential: Account.DefaultAs and IdentityHint.DefaultAs → core.Identity

The full data flow is now typed end-to-end:
  extcred.Brand → core.LarkBrand (named-type cast)
  extcred.Identity → core.Identity (named-type cast)

No string intermediaries, no implicit conversions.

Change-Id: I715b3b3f033fcb624010f1af9619e3562740ef08
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix gofmt alignment in extension/credential/types.go

Change-Id: Ibfac0703a5a28f3c6ba4a47bf40696028d0f3b90
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove file/stdin input support from task comment content flag

Change-Id: If49704ca4612465a23bd30b755d6e72a35fc2349
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(cmdutil): remove dead code autoDetectIdentity

autoDetectIdentity() is only called from tests, never from production
code. Remove it along with its 3 test cases to reduce surface area
before the upcoming ctx propagation refactor.

Change-Id: I35a188860f17656f3e1fe9874f87f284985ae196

* refactor(cmdutil): add ctx parameter to resolveIdentityHint

Private method resolveIdentityHint now accepts context.Context and
passes it to CredentialProvider.ResolveIdentityHint instead of using
context.Background(). The caller (ResolveAs) still uses
context.Background() temporarily until its own signature is updated.

Change-Id: I14634a4e0dc1d657d56936ba61a7b7a206da8ac4

* refactor(cmdutil): add ctx parameter to ResolveStrictMode

ResolveStrictMode now accepts context.Context and passes it to
CredentialProvider.ResolveAccount instead of using context.Background().

Callers in cobra RunE pass cmd.Context(); callers outside RunE
(cmd/root.go startup, tests) use context.Background() explicitly.

Change-Id: I31be48e548ac5ac5640a65f3bfdde4a53ed1dc7e

* refactor(cmdutil): add ctx parameter to CheckStrictMode

CheckStrictMode now accepts context.Context and forwards it to
ResolveStrictMode. Callers pass cmd.Context() (cobra RunE) or
opts.Ctx (APIOptions/ServiceMethodOptions).

Change-Id: I47888519d4cae8c94054771c32aff075565a8cdc

* refactor(cmdutil): add ctx parameter to ResolveAs

ResolveAs now accepts context.Context as first parameter and forwards
it to ResolveStrictMode and resolveIdentityHint. This completes the
ctx propagation chain: all Factory methods that call
CredentialProvider now receive ctx from cobra cmd.Context().

No more context.Background() calls remain in factory.go for
credential provider operations.

Change-Id: I6d10b6350e3b149470660de3e7855614314e8b29

* test: fix gofmt in cmdutil factory tests

Change-Id: I4a87d5a815b959f14cc4371b73dee4aae106932f

* fix: remove file/stdin input support from im send/reply and drive comment

The Input (file/stdin) feature is not yet ready for these flags:
- im send/reply: --content, --text, --markdown
- drive add-comment: --content

Retained only in doc create/update where markdown from file is essential.

Change-Id: I582b6349528fccb639ad9edc84650cca3b68535c
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: liushiyao <liushiyao.1206@bytedance.com>
2026-04-07 15:21:14 +08:00
MaxHuang22
7baba213bc feat: add --jq flag for filtering JSON output (#211)
* feat: add --jq flag for filtering JSON output across all command types

Add jq expression filtering (--jq / -q) to api, service, and shortcut
commands using gojq. Includes early expression validation, mutual
exclusion checks with --output and non-json --format, pagination+jq
aggregation path, and comprehensive test coverage.

* fix: correct gofmt alignment in jq_test.go struct literal


* fix: downgrade gojq to v0.12.17 to keep Go 1.23 compatibility

gojq v0.12.18 requires Go 1.24, which unnecessarily bumped the project
minimum version. v0.12.17 requires only Go 1.21 and provides the same
jq functionality needed.


* refactor: consolidate jq validation and pagination logic

Extract ValidateJqFlags() and PaginateWithJq() shared functions to
eliminate duplicated jq logic across api, service, and shortcut commands.

* fix: reject --jq for non-JSON responses and propagate shortcut jq errors

- HandleResponse now returns a validation error when --jq is used with
  a non-JSON Content-Type instead of silently falling through to binary save.
- Shortcut runtime jq errors are captured in RuntimeContext.outputErr
  and propagated as the command exit code, matching api/service behavior.
2026-04-02 18:36:59 +08:00
evandance
a13bee8fda fix: resolve silent failure in lark-cli api error output (#39) (#85)
MarkRaw previously skipped both enrichPermissionError and
WriteErrorEnvelope, causing api command to exit 1 with no output
on API errors. Now MarkRaw only skips enrichPermissionError while
WriteErrorEnvelope always runs, ensuring stderr error envelope is
always written.

- Simplify apiRun MarkRaw logic (remove unnecessary IsJSONContentType check)
- Update existing tests to match new behavior
- Add 5 e2e tests covering api/service/shortcut error output
2026-03-30 17:19:24 +08:00
梁硕
83dfb068ad feat: open-source lark-cli — the official CLI for Lark/Feishu
Change-Id: I113d9cdb5403cec347efe4595415e34a18b7decf
2026-03-28 10:36:25 +08:00