fix: normalize styled markdown links#18
Open
hehanlin1996 wants to merge 61 commits into
Open
Conversation
…ite#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>
Change-Id: I87bb32c86e3c3362f541ccc6320c656eb795ec9b
…larksuite#935) Two DryRun functions in the sheets shortcuts called json.Unmarshal without checking the return value. This looks like a bug, but Validate already parses and validates the same --style / --data JSON before DryRun runs, so the error is structurally impossible at this point. Use _ = assignment + comment to silence the unchecked-error lint warning and make the safety invariant explicit to future readers. Co-authored-by: KhanCold <KhanCold@users.noreply.github.com>
Bidirectional sync between a local directory and a Drive folder with diff detection (new_local, new_remote, modified, unchanged) and conflict resolution strategies (--on-conflict: remote-wins, local-wins, keep-both, ask). Key behaviors: - Type conflict detection: hard-fail when local file vs remote non-file or local directory vs remote file - Keep-both: rename local with __lark_<hash> suffix, then pull remote; occupied map includes localDirs to prevent suffix collision - Local-wins partial-success: prefer returned file_token on upload failure - Empty directory mirroring: pre-create local dirs on Drive via drivePushWalkLocal before scope preflight - Structured errors throughout (output.Errorf / output.ErrWithHint) Includes unit tests and E2E tests (dry-run + live workflow).
* feat(auth): add QR code support for device auth flow * docs: update login QR code display hints for AI agent * feat(auth): add ASCII QR code support for auth flow * docs: add comments for login and auth helper functions * chore: remove unused qrCodeToBase64 helper function * fix(auth/login): clarify verification_url handling in login hint
…ite#847) refactor(slides): rename slide layout lint scope Change-Id: I1b0e42b6508ec2c5f6ae6dc0d1b7ac23c5bbe2e3 feat(slides): improve lark slides skill guidance Change-Id: I49563da4ca623a89f5391f36ceb8f5a31417e321 feat(slides): strengthen lark slides planning guidance Change-Id: If49330e1f9b779bc76a919565ed61a31c255f508 feat(slides): remove lark slides layout lint rules Change-Id: I64f1fc3b33d05c069c9ef58e61d00aa57ac18ecd refactor(slides): streamline skill guidance Change-Id: I3b39faaab7dcac52fac1572590fc5d8934428da5 feat(slides): add slides asset planning guidance Change-Id: I37303043f7704e4ba484552158390a4e24bf9c42 feat(slides): add visual planning guidance Change-Id: Idee7c392d41ff02124313d572c547d0a086d9c35 feat(slides): add lark slides planning layer Change-Id: I3f0765aa53656070d9ba9b388dade19355e7bc6f
* feat: add markdown +patch shortcut Change-Id: I8159941ff9dec4e5cbf0c757ec19ee172b302224 * fix: align markdown patch validation and dry-run Change-Id: I98079901e980b74998938afc4917b91a79689948
…te#942)" (larksuite#950) This reverts commit 7af616b.
Change-Id: Iea77769a6a0f4e77e8946b72ddb619782be3ea42
Change-Id: I3e04a82f622853549f11ac49cbd6fefa194c7c56
…arksuite#904) - +node-get: wrap wiki.spaces.get_node; accepts node_token, obj_token, or a Lark URL (URL path auto-infers obj_type); formatted output with creator / updated_at. No synthesized url — get_node returns none and a BuildResourceURL fallback is a non-canonical link that misleads in a read/confirm command (sibling read shortcuts omit it too) - +node-delete: wrap space.node delete; high-risk-write (--yes gated), async delete-node task polling, auto-resolves space_id via get_node when --space-id omitted, actionable hints for codes 131011 / 131003. The delete-node task result lives under the gateway's generic `simple_task_result` key (NOT `delete_node_result`) - +space-create: wrap spaces.create; user-only identity, --name required (no empty-name spaces), flattened space output, no url - factor the shared wiki async-task poll loop into wiki_async_task.go; preserve upstream Lark Detail.Code on poll exhaustion (no longer rebuilt via lossy ErrWithHint) - drive +task_result: add wiki_delete_node scenario so +node-delete's async-timeout next_command actually resolves - skill docs: reference pages for the 3 new shortcuts + SKILL.md shortcuts table (no raw nodes.delete API exists — it's shortcut-only, so it is intentionally absent from API Resources / permission table); drop the circular TestWikiShortcutsIncludeAllCommands change-detector Change-Id: I316f78290cec5bc50f80d629173e3bf2a35dd005
* feat: support base attachment APIs * fix: handle duplicate base attachment downloads * fix: remove unused attachment token helper
Test files legitimately need to construct dangerous Unicode inputs (RLO, ZWSP, BOM, etc.) to verify validation logic rejects them. bidichk treats decoded \u escape literals as Trojan Source risks, which is a false positive for intentional test data. Change-Id: I555028a992ab008da16129eb41075c333d0099b8
…t --set-priority (larksuite#779) Add a Priority field to DraftProjection populated from the EML header pair X-Cli-Priority (CLI/OAPI primary) → X-Priority (RFC fallback for IMAP-回灌 historical drafts), with case-insensitive lookup via the existing headerValue helper and a local mapping table aligned with the backend gopkg/mail_priority.PriorityValueToType vocabulary. When neither header is present (the symmetric read of --set-priority normal=remove_header) the projection emits "unknown" so agents have a stable read-side surface. Append one notes entry to buildDraftEditPatchTemplate documenting the --set-priority flag and the X-Cli-Priority translation contract. The write-side (--set-priority flag, parsePriority helper, translation branch in mail_draft_edit.go, EML header target) is unchanged — already shipped on master. sprint: S4
* docs(lark-im): clarify message activity search Change-Id: I2a9a928aab2354dfaf103cdf53add435088ff9e2 * docs(lark-im): keep bot history guidance additive Change-Id: I6d89610db9f9d1488f207dcc6b92f7aada839f8b
…rksuite#895) * feat(mail): bot+mailbox=me validation and dynamic --as help tests Add validateBotMailboxNotMe helper to shortcuts/mail/helpers.go and wire it as a Validate callback into +message, +messages, +thread and +triage, so bot identity combined with the default --mailbox me is rejected early with a clear fixup hint instead of a late opaque API error. The --as help text was already dynamic via AddShortcutIdentityFlag; add TC-10/TC-11 tests in internal/cmdutil/identity_flag_test.go to pin that behaviour, and TC-1 through TC-9 in shortcuts/mail/mail_shortcut_validation_test.go to cover the new Validate callbacks. +watch is excluded: its AuthTypes is ["user"], so bot is never valid. sprint: S2 * test(cmdutil): add Hidden and DefValue assertions to identity flag tests * fix(mail): add bot+mailbox=me validation to +template-create and +template-update * fix(mail): add bot+mailbox=me validation to +template-update * fix(mail): gofmt mail_template_create.go * fix(mail): gofmt mail_template_update.go * fix(mail): skip bot+mailbox=me check for print-patch-template local path
…uite#960) * fix(wiki): surface real node url for +node-create / +node-copy The create-node and copy-node OpenAPI responses carry a real `url` field (present in practice though absent from the documented schema). Both shortcuts ignored it: +node-create synthesized a link via BuildResourceURL, and +node-copy emitted no URL at all. Parse `url` into the shared wikiNodeRecord and add a wikiNodeURL helper that prefers the response url, falling back to BuildResourceURL only when it is blank. Wire +node-create and +node-copy to the helper so both surface the canonical link when available. Change-Id: I0ca5f91b02c24e81d083793e6a8e4f8c966aeec3 * refactor(wiki): move wikiNodeURL to shared wiki_helpers.go The helper is consumed by both +node-create and +node-copy, so its placement should reflect the broader usage rather than living in the create command's file. Pure move; no behavior change. Change-Id: I9990c12da042f631fe2519911c6a9d663fd5c22b
…iki unwrapping (larksuite#947) * feat(drive): add +inspect shortcut for document URL inspection with wiki unwrapping Implements larksuite#662: `lark-cli drive +inspect --url <url>` inspects any Lark/Feishu document URL to get its type, title, and canonical token, with automatic wiki URL unwrapping via get_node API. - Add ParseResourceURL (inverse of BuildResourceURL) in common - Extract FetchDriveMetaTitle as public shared helper - Add drive +inspect shortcut with wiki unwrapping support - Add skill reference docs and update SKILL.md - Dry-run E2E tests for docx URL, wiki URL, and bare token * refactor: move host validation from ParseResourceURL to +inspect ParseResourceURL is a general-purpose URL parser that should not hardcode domain lists — future Lark domains would silently break. Move isLarkHost/larkHostSuffixes to drive_inspect.go where host validation is a business decision of the +inspect command. Add E2E test for non-Lark host with Lark-like path. * refactor: remove host validation from +inspect Lark supports custom enterprise domains, so a hardcoded suffix list can never be exhaustive and would falsely reject valid URLs. Path-based matching in ParseResourceURL is sufficient; invalid URLs will fail naturally at the API call stage.
…arksuite#961) * fix(identitydiag): harden verify path and tighten status semantics Follow-ups to larksuite#957: - bound bot/user verify calls with a 10s timeout (mirrors the doctor endpoint probe) so a hanging server cannot wedge `auth status --verify` or `doctor` - return StatusNotConfigured (not StatusMissing) when the user-identity path is blocked by missing app config, matching the bot side - surface the `{code, msg}` envelope on bot-info HTTP 4xx responses so callers see why bot auth was rejected, not just the bare HTTP code - introduce identity{User,Bot,None} constants in cmd/auth/status.go and use the exported StatusMessage() in the human-readable note instead of raw status codes like "not_configured" - collapse the duplicated verify-failed identity construction in the user path into a local helper - cover the new failure paths with unit tests (HTTP 4xx with envelope, business error code, user server-rejected, expired user token, strict-mode user-only, missing app config for user) Change-Id: I581348a65f15b1452a6f48a3e3245d09257314ac * fix(identitydiag): decode bot/v3/info from "bot" field, not "data" `/open-apis/bot/v3/info` returns `{code, msg, bot: {...}}` — the bot payload is under `bot`, not `data` as the newer Lark API convention would suggest. The decoder was reading from a non-existent `data` field, so `envelope.Data.OpenID` was always empty and every successful verify was reported as `Bot identity: verify failed: open_id is empty`. The pre-existing test mocks used `{"data": {...}}` matching the buggy decoder, so unit tests passed while production reads of every Lark account failed verification. Fix: - change the JSON tag on the envelope from `json:"data"` to `json:"bot"` - update mocks in identitydiag and cmd/auth/status tests to emit `bot` Verified locally: `lark-cli doctor` now reports `bot_identity: pass` for both a normal account and a bot-only profile, restoring the behavior that larksuite#957 set out to deliver. Change-Id: Ib26dfdd5a0cc37d2d62537ae2bf5e854e67cb83c * fix(shortcuts/common): decode bot/v3/info from "bot" field, not "data" Same schema bug as the one fixed in identitydiag — `RuntimeContext. fetchBotInfo` reads from a non-existent "data" key, so every successful call would report "open_id is empty" once a caller starts depending on it. There are no production callers of `RuntimeContext.BotInfo()` yet (only tests + the `TestNewRuntimeContextWithBotInfo` helper), so this bug is dormant — but the pre-existing tests pass with the same wrong schema in their mocks, so the first real consumer would silently break. Fix: tag `json:"data"` → `json:"bot"` plus aligning the four mock fixtures in runner_botinfo_test.go. The Go field name `Data` is kept to minimize the diff; only the JSON contract is corrected. Change-Id: I11e1e871603e5349f8df29b1d58e35d07b628dfd
…e#948) Switch `drive +export --file-extension markdown` from the legacy V1 GET /open-apis/docs/v1/content API to the V2 POST /open-apis/docs_ai/v1/documents/{token}/fetch API for higher-quality Lark-flavored Markdown output. - Update DryRun and Execute paths to use V2 endpoint with JSON body - Add docx:document:readonly scope for the new API - Validate V2 response structure (fail fast on missing document/content) - Encode token in URL path via validate.EncodePathSegment - Update unit tests and add V2 response validation error path tests - Add E2E dry-run test for markdown export path - Update skill documentation
Change-Id: I637cfaf2d6a228c43e3b3041fef8e030bc80b9d0
* docs(lark-vc): clarify meeting search evidence flow Change-Id: I997ec0654b9448eb0cc6ed7c15493dd2316ffa39 * docs(lark-vc): clarify pagination precedence Change-Id: Icdcc38db2ce3db3a3371c6451624fd52a71170e3
Change-Id: I0908c20f6ab9cf76a5d75cc1c81871591aa6a841
… file blocks (larksuite#825) * feat(doc): warn before overwrite when document contains whiteboard or file blocks Before executing an overwrite in v1 mode, pre-fetch the current document and scan the Markdown for <whiteboard> and <file> resource blocks. If any are found, print a warning to stderr listing the counts and suggesting the user take a backup with `docs +fetch` first. Overwrite replaces the entire document and cannot reconstruct these blocks from Markdown; previously the data was lost with no indication to the caller. The check is best-effort: a failed pre-fetch silently skips the guard rather than blocking the overwrite. * test(doc): add validateSelectionByTitleV1 tests and drop redundant empty-md guard in warnOverwriteResourceBlocks * fix(doc): use regex for resource block detection, add latency/coverage comments, document skip_task_detail purpose
* feat: add markdown +diff shortcut Change-Id: I7da27889517707ac6f1d5e8c429e4bdfb49fdcf8 * fix: harden markdown diff downloads Change-Id: I0020e14ebee780617d790836af1368db851b8cf1 * refactor: address markdown diff review feedback Change-Id: I0ddb852218ec4784c0f9491896796c3007f04122
…e#991) * docs(im): clarify media path restrictions * docs(im): clarify file key formats for message file flags Change-Id: I329ca0db9e7a01b774846d522d1b2a64da74233c --------- Co-authored-by: mtsui-cmyk <mervyntsui@gmail.com>
Change-Id: I6ddc8cfc029c684deb5de4f210357e19ade083e1
Change-Id: I000c2d56962e6da2a7ef77d986c2eb73ec286546
…arksuite#992) Change-Id: I6b513eef57a3479c8971b3bb6cbf005cad3f8040
larksuite#999) strings.Fields("") returns an empty slice, causing --scope "" to bypass validation and return ok: true. Replace the false-positive success path with an ErrValidation error so callers correctly detect the invalid input. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Change-Id: Ic95e8a74a0d6fc7f89782dccde867fd794cfcf46
Change-Id: Ifb0b6bf05d486943d9a689bf63dde2251dcd3500
…1015) When a resource is created with bot identity, the CLI attempts to auto-grant full_access to the current user. If the user open_id is missing or the grant API call fails, the result was only written to the JSON permission_grant field and easily overlooked. Changes: - Add stderr warnings when auto-grant is skipped or fails - Add 'hint' field to permission_grant JSON output with failure reason and actionable next step (e.g. auth login, check scope, retry) - Add end-to-end skipped/failed tests across all affected shortcuts (doc, drive, sheets, slides, wiki, markdown, base) Closes larksuite#963
…uite#1002) Adds the apps domain to lark-cli for managing Miaoda (妙搭) applications: 6 shortcuts covering the full lifecycle (+create / +update / +list / +access-scope-set / +access-scope-get / +html-publish). Aligned with the OAPI v2 design — app_type enum (currently HTML), string scope enum (All / Tenant / Range), cursor pagination, in-memory tar.gz multipart publish flow. Namespace registered at /open-apis/spark/v1/ with spark:app.* scopes. --------- Co-authored-by: wangjiangwen-gif <286006750+wangjiangwen-gif@users.noreply.github.com>
…larksuite#997) - +member-add: wrap POST /spaces/{id}/members; --member-type / --member-role enums, optional --need-notification query (omitted entirely when the flag is unset, instead of forcing need_notification=false), my_library resolution under --as user, flattened single-member output - +member-remove: wrap DELETE /spaces/{id}/members/{member_id}; surfaces the required member_type + member_role body the API expects, my_library resolution, fallback to echoing the caller's inputs when the API omits the member echo - +member-list: wrap GET /spaces/{id}/members; reuses the +space-list / +node-list pagination contract (single page by default, --page-all walks every page capped by --page-limit, --page-token resumes a cursor) - All three reject bot identity + my_library upfront with a clear hint and declare the narrowest scope the API accepts (wiki:member:create / wiki:member:update / wiki:member:retrieve) so tokens carrying only the narrow scope are not false-rejected by the exact-string preflight - skill docs: reference pages for the three new shortcuts + SKILL.md shortcuts table; switch the membership flow guidance from raw `wiki members create` to the new +member-add path Change-Id: I158a86aa7f00bb7cecc7a4e99346f3fb151b3c09
Change-Id: Ifcc78649e294d516015846d746bb2bc65b239eb3
* feat: support markdown files in drive +add-comment Change-Id: Id9a87706a1e43756d8142637be9ec1e0748d4ddf * fix: use markdown file comment anchor placeholder Change-Id: Ifffc4cdd963c13e53f4cad154aebe11ae309df9e * fix: gate drive file comments by supported extensions Change-Id: Ie6c7f38dbbea1f87a81600da71180627b53a2355
* feat(apps): gate apps domain off on Lark brand The Miaoda apps OpenAPI is Feishu-only. On Lark brand: - shortcut subtree is registered + hidden, RunE returns a structured brand-restriction error so users see a clear message instead of cobra's generic "unknown command" - auth login `--domain apps` is treated as unknown; `--domain all` skips apps; help text omits it - scope collection skips apps shortcuts so spark:* scopes are never requested The leaf-stub pattern mirrors internal/cmdpolicy/apply.go::installDenyStub (DisableFlagParsing + ArbitraryArgs + leaf-level PersistentPreRunE override) so cobra can't short-circuit the stub with a missing-flag or parent-PreRunE detour. Change-Id: I5817e87ae6fedabdb5faf05d0d32ea988f7effc9
- Bump version to 1.0.38 - Update CHANGELOG.md with the apps brand gating change since v1.0.37 - Backfill the [v1.0.38] link reference at the bottom of CHANGELOG.md Change-Id: I6fd0d1243e2219a1eaa1fae5fae4ff6d8de361da
…#893) add documentation for sending Markdown images, and align image handling guidance with actual runtime behavior
…larksuite#934) * feat(sidecar): support multi-client identity isolation in server-demo When multiple CLI sandbox environments share a single sidecar instance, user tokens (UAT) were not isolated -- the last user to log in would overwrite previous users' tokens, causing identity cross-contamination. This change introduces per-client HMAC key isolation: - Each client gets a unique client-*.key file for data-plane HMAC signing, allowing the sidecar to identify request origin. - A new auth_bridge.go handles management endpoints (login/poll/status) with explicit client-to-feishuOpenId binding. - User token resolution is strictly bound to the matched client -- no fallback to other users' tokens when a client has no mapping. - The shared proxy.key is reused across restarts instead of regenerated, fixing a race condition when multiple sidecar instances start together. Wire protocol (sidecar package) is unchanged; existing single-client deployments are fully backward compatible. Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech) * fix(sidecar): address review feedback on filesystem and safety - Replace os.ReadFile/WriteFile/ReadDir with vfs.* equivalents for test mockability, consistent with project coding guidelines. - Limit auth bridge request body to 64KB to prevent memory exhaustion. - Log errors in saveUserMap instead of silently discarding them. - Reject client keys that collide with the shared proxy key. - Reject duplicate client keys instead of silently overwriting. Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech) * refactor(sidecar): remove workspace-specific naming and backward compat - parseClientID: only accept "client_id" field, remove legacy fallback - loadClientKeys: scan all *.key (excluding proxy.key), no prefix required - Remove legacy file migration logic in newAuthBridge - Update flag description to reflect generic key scanning Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech) * refactor(sidecar): extract multi-tenant demo and add unit tests Address review feedback from sang-neo03: 1. Extract multi-client code into sidecar/server-multi-tenant-demo/, keeping server-demo as the minimal single-tenant reference. 2. Add unit tests for the isolation guarantee: - loadClientKeys: shared-key collision and duplicate keyHex are skipped - verifyWithClientKeys: correct client matched, unknown key rejected - loadUserMap/saveUserMap: round-trip persistence across restart 3. Cross-link READMEs between server-demo and server-multi-tenant-demo. Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech) * docs(sidecar): rewrite multi-tenant demo README with problem statement and client guide - Explain the multi-app credential isolation problem (app_secret must not be exposed to client environments) - Document typical deployment topology with multiple sidecar instances - Add complete client setup guide: env vars, multi-app switching, login flow, and end-to-end workflow example - Document design decisions and management endpoint details Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech) * fix(sidecar): address CodeRabbit review feedback on tests and docs - Make TestProxyHandler_AcceptsAllowedAuthHeaders fully offline by using httptest.NewTLSServer instead of depending on open.feishu.cn - Isolate TestRun_RejectsSelfProxy config state with t.Setenv and temp dirs - Check os.MkdirAll error in test fixture setup - Add language identifiers to fenced code blocks (MD040) - Validate user-supplied CLI paths with validate.SafeInputPath Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech) --------- Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech)
…e#1040) - description: switch from trigger-word enumeration to a general principle (any HTML artifact intended to be independently accessible falls under this skill; defer the deploy-vs-demo decision to the skill body) - surface apps +access-scope-get in prerequisites list and Shortcuts table so agents can find the read side of access-scope - add "writing HTML hard constraints" section: index.html is the required entry filename, --path cannot equal cwd (both are CLI-side hard rejects that previously only lived in the html-publish ref)
Change-Id: Ice3e8784e78986d427c4c94664e1e5edff2a4fcd
Change-Id: I2e7bb2e2971bfb071c3976d349b2d2bc4cc485ae
Change-Id: I06bca4f3aedec1adee9ecd3d060c333cc6dd301e
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Tests
Closes larksuite#1035