akm-cli 0.7.5 → 0.8.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{.github/CHANGELOG.md → CHANGELOG.md} +113 -2
- package/README.md +20 -4
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +133 -0
- package/dist/cli.js +1995 -551
- package/dist/commands/agent-dispatch.js +110 -0
- package/dist/commands/agent-support.js +68 -0
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +130 -534
- package/dist/commands/consolidate.js +1531 -0
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +660 -0
- package/dist/commands/distill.js +990 -75
- package/dist/commands/eval-cases.js +43 -0
- package/dist/commands/events.js +5 -23
- package/dist/commands/graph.js +477 -0
- package/dist/commands/health.js +400 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +77 -0
- package/dist/commands/help/help-proposals.md +15 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +8 -0
- package/dist/commands/history.js +54 -46
- package/dist/commands/improve-profiles.js +146 -0
- package/dist/commands/improve-result-file.js +103 -0
- package/dist/commands/improve.js +2175 -0
- package/dist/commands/info.js +5 -2
- package/dist/commands/init.js +50 -2
- package/dist/commands/installed-stashes.js +102 -139
- package/dist/commands/knowledge.js +136 -0
- package/dist/commands/lint/agent-linter.js +49 -0
- package/dist/commands/lint/base-linter.js +479 -0
- package/dist/commands/lint/command-linter.js +49 -0
- package/dist/commands/lint/default-linter.js +16 -0
- package/dist/commands/lint/index.js +183 -0
- package/dist/commands/lint/knowledge-linter.js +16 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +61 -0
- package/dist/commands/lint/registry.js +36 -0
- package/dist/commands/lint/skill-linter.js +45 -0
- package/dist/commands/lint/task-linter.js +50 -0
- package/dist/commands/lint/types.js +4 -0
- package/dist/commands/lint/vault-key-rules.js +139 -0
- package/dist/commands/lint/workflow-linter.js +56 -0
- package/dist/commands/lint.js +4 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal.js +66 -12
- package/dist/commands/propose.js +86 -31
- package/dist/commands/reflect.js +1119 -73
- package/dist/commands/registry-search.js +5 -2
- package/dist/commands/remember.js +69 -6
- package/dist/commands/schema-repair.js +203 -0
- package/dist/commands/search.js +115 -14
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +144 -25
- package/dist/commands/source-add.js +17 -45
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +14 -19
- package/dist/commands/tasks.js +438 -0
- package/dist/commands/url-checker.js +42 -0
- package/dist/commands/vault.js +130 -77
- package/dist/core/action-contributors.js +28 -0
- package/dist/core/asset-ref.js +7 -0
- package/dist/core/asset-registry.js +7 -16
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +22 -0
- package/dist/core/common.js +157 -0
- package/dist/core/concurrent.js +25 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +625 -0
- package/dist/core/config-schema.js +501 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +327 -987
- package/dist/core/errors.js +40 -19
- package/dist/core/events.js +91 -138
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +3 -6
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +20 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +806 -0
- package/dist/core/parse.js +158 -0
- package/dist/core/paths.js +326 -14
- package/dist/core/proposal-quality-validators.js +364 -0
- package/dist/core/proposal-validators.js +69 -0
- package/dist/core/proposals.js +498 -42
- package/dist/core/state-db.js +927 -0
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +54 -0
- package/dist/core/warn.js +62 -1
- package/dist/core/write-source.js +3 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +152 -253
- package/dist/indexer/db.js +933 -103
- package/dist/indexer/ensure-index.js +64 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +376 -101
- package/dist/indexer/graph-db.js +391 -0
- package/dist/indexer/graph-dedup.js +95 -0
- package/dist/indexer/graph-extraction.js +550 -124
- package/dist/indexer/index-context.js +4 -0
- package/dist/indexer/indexer.js +506 -291
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +148 -160
- package/dist/indexer/memory-inference.js +99 -74
- package/dist/indexer/metadata-contributors.js +29 -0
- package/dist/indexer/metadata.js +255 -196
- package/dist/indexer/path-resolver.js +92 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +331 -0
- package/dist/indexer/ranking.js +81 -0
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +111 -0
- package/dist/indexer/search-source.js +44 -10
- package/dist/indexer/semantic-status.js +5 -16
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +28 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +122 -230
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +7 -13
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +70 -5
- package/dist/integrations/agent/prompts.js +150 -74
- package/dist/integrations/agent/runner.js +151 -0
- package/dist/integrations/agent/sdk-runner.js +126 -0
- package/dist/integrations/agent/spawn.js +118 -23
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +32 -69
- package/dist/integrations/session-logs/index.js +68 -0
- package/dist/integrations/session-logs/providers/claude-code.js +59 -0
- package/dist/integrations/session-logs/providers/opencode.js +55 -0
- package/dist/integrations/session-logs/types.js +4 -0
- package/dist/llm/call-ai.js +62 -0
- package/dist/llm/client.js +72 -124
- package/dist/llm/embedder.js +3 -19
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +3 -0
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +89 -48
- package/dist/llm/graph-extract.js +676 -70
- package/dist/llm/index-passes.js +9 -23
- package/dist/llm/memory-infer.js +52 -71
- package/dist/llm/metadata-enhance.js +42 -29
- package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/output/cli-hints-full.md +281 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +5 -318
- package/dist/output/context.js +3 -0
- package/dist/output/renderers.js +223 -256
- package/dist/output/shapes.js +150 -105
- package/dist/output/text.js +318 -30
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +3 -0
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +70 -49
- package/dist/registry/providers/static-index.js +53 -48
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17307 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8900 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +775 -37
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +5 -12
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +7 -5
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +7 -0
- package/dist/tasks/backends/cron.js +203 -0
- package/dist/tasks/backends/exec-utils.js +28 -0
- package/dist/tasks/backends/index.js +24 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +187 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +215 -0
- package/dist/tasks/parser.js +211 -0
- package/dist/tasks/resolveAkmBin.js +87 -0
- package/dist/tasks/runner.js +458 -0
- package/dist/tasks/schedule.js +211 -0
- package/dist/tasks/schema.js +15 -0
- package/dist/tasks/validator.js +62 -0
- package/dist/version.js +3 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +15 -0
- package/dist/wiki/wiki.js +13 -61
- package/dist/workflows/authoring.js +8 -25
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +11 -3
- package/dist/workflows/runs.js +62 -91
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +4 -8
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +9 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +48 -0
- package/docs/migration/v0.7-to-v0.8.md +1307 -0
- package/package.json +20 -8
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -381
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { BaseLinter } from "./base-linter";
|
|
5
|
+
/**
|
|
6
|
+
* Linter for `tasks/` assets.
|
|
7
|
+
*
|
|
8
|
+
* Tasks are pure YAML files at `<stash>/tasks/<id>.yml`. In addition to the
|
|
9
|
+
* base checks this linter validates the required task fields:
|
|
10
|
+
*
|
|
11
|
+
* - `schedule` (string, non-empty) — cron expression or `@`-alias
|
|
12
|
+
* - `enabled` (boolean)
|
|
13
|
+
* - At least one of: `prompt`, `workflow`, or `command` field present
|
|
14
|
+
*
|
|
15
|
+
* All issues are reported as `invalid-task-yaml` and are **not** auto-fixable.
|
|
16
|
+
* Cron expression syntax validation is intentionally out of scope (that
|
|
17
|
+
* belongs to `parseSchedule()`).
|
|
18
|
+
*/
|
|
19
|
+
export class TaskLinter extends BaseLinter {
|
|
20
|
+
types = ["tasks"];
|
|
21
|
+
lint(ctx) {
|
|
22
|
+
const issues = this.runBaseChecks(ctx);
|
|
23
|
+
// Skip files that failed to parse — `data` will be empty.
|
|
24
|
+
if (ctx.data === null || Object.keys(ctx.data).length === 0)
|
|
25
|
+
return issues;
|
|
26
|
+
const missing = [];
|
|
27
|
+
// schedule: must be present and non-empty
|
|
28
|
+
if (!("schedule" in ctx.data) || typeof ctx.data.schedule !== "string" || ctx.data.schedule.trim() === "") {
|
|
29
|
+
missing.push("schedule");
|
|
30
|
+
}
|
|
31
|
+
// enabled: must be present (boolean — value of false is valid)
|
|
32
|
+
if (!("enabled" in ctx.data)) {
|
|
33
|
+
missing.push("enabled");
|
|
34
|
+
}
|
|
35
|
+
// At least one of: prompt, workflow, or command
|
|
36
|
+
const hasTarget = "prompt" in ctx.data || "workflow" in ctx.data || "command" in ctx.data;
|
|
37
|
+
if (!hasTarget) {
|
|
38
|
+
missing.push("prompt, workflow, or command");
|
|
39
|
+
}
|
|
40
|
+
if (missing.length > 0) {
|
|
41
|
+
issues.push({
|
|
42
|
+
file: ctx.relPath,
|
|
43
|
+
issue: "invalid-task-yaml",
|
|
44
|
+
detail: `missing required fields: ${missing.join(", ")}`,
|
|
45
|
+
fixed: false,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return issues;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Vault security lint rules — flags known-dangerous environment variable names.
|
|
6
|
+
*
|
|
7
|
+
* These env var names, when present as vault keys, indicate the vault can be
|
|
8
|
+
* used to hijack process execution via loader injection, path override, or
|
|
9
|
+
* shell/runtime startup hooks. The lint pass emits a warning-level finding;
|
|
10
|
+
* it does NOT block vault load or `akm vault setKey`.
|
|
11
|
+
*
|
|
12
|
+
* Enforcement scope:
|
|
13
|
+
* - `akm lint` reports findings as `dangerous-vault-key` (non-blocking warn).
|
|
14
|
+
* - `akm add` BLOCKS install unless `--allow-insecure` is set (or, on TTY,
|
|
15
|
+
* the user explicitly confirms at the prompt).
|
|
16
|
+
* - `akm vault setKey` does NOT consult this list — by design, the operator
|
|
17
|
+
* owns their own vault and may legitimately store any key locally. The
|
|
18
|
+
* gate exists only for third-party stash installation.
|
|
19
|
+
*
|
|
20
|
+
* False-positive tradeoff:
|
|
21
|
+
* A handful of keys (EDITOR, VISUAL, PAGER) are included because they are
|
|
22
|
+
* invoked by many interactive tools and are a documented RCE vector when
|
|
23
|
+
* sourced from untrusted vaults. They will also flag on benign vaults
|
|
24
|
+
* where the operator legitimately wants to set their editor — accept the
|
|
25
|
+
* FP and bypass with `--allow-insecure` after review.
|
|
26
|
+
*/
|
|
27
|
+
import { listKeys } from "../vault";
|
|
28
|
+
// ── Dangerous key set ─────────────────────────────────────────────────────────
|
|
29
|
+
export const DANGEROUS_VAULT_KEYS = new Set([
|
|
30
|
+
// Dynamic linker hijacking (Linux glibc ld.so)
|
|
31
|
+
"LD_PRELOAD", // forces shared library injection
|
|
32
|
+
"LD_LIBRARY_PATH", // overrides library search path
|
|
33
|
+
"LD_AUDIT", // loads auditing libs (CVE-class injection vector)
|
|
34
|
+
"LD_DEBUG", // info disclosure / loader behaviour leak
|
|
35
|
+
"LD_BIND_NOW", // eager symbol resolution — can trigger malicious libs
|
|
36
|
+
"LD_PROFILE", // writes profile data — abusable for info disclosure
|
|
37
|
+
"LD_ASSUME_KERNEL", // kernel-version spoofing affecting loader behaviour
|
|
38
|
+
"LD_TRACE_LOADED_OBJECTS", // info disclosure (lists linked libs)
|
|
39
|
+
// Dynamic linker hijacking (macOS dyld)
|
|
40
|
+
"DYLD_INSERT_LIBRARIES", // macOS analogue of LD_PRELOAD
|
|
41
|
+
"DYLD_LIBRARY_PATH", // overrides dyld library search path
|
|
42
|
+
"DYLD_FRAMEWORK_PATH", // overrides framework search path
|
|
43
|
+
// Shell and command resolution
|
|
44
|
+
"PATH", // command lookup hijack
|
|
45
|
+
"BASH_ENV", // sourced on non-interactive bash startup (RCE)
|
|
46
|
+
"ENV", // sourced on POSIX sh startup (RCE)
|
|
47
|
+
"PROMPT_COMMAND", // command run before each bash prompt (RCE)
|
|
48
|
+
"PS1", // prompt — command substitution arbitrary code
|
|
49
|
+
"PS2", // continuation prompt — command substitution
|
|
50
|
+
"IFS", // Internal Field Separator — classic word-splitting attack
|
|
51
|
+
// Shell startup hijack
|
|
52
|
+
"ZDOTDIR", // zsh startup file lookup directory hijack
|
|
53
|
+
// Language runtime hijacking — Node.js
|
|
54
|
+
"NODE_OPTIONS", // injects flags incl. --require module-load RCE
|
|
55
|
+
"NODE_PATH", // module resolution hijack
|
|
56
|
+
"NODE_TLS_REJECT_UNAUTHORIZED", // silently disables TLS verification — MITM enabler
|
|
57
|
+
// Language runtime hijacking — Python
|
|
58
|
+
"PYTHONSTARTUP", // sourced by interactive python (RCE)
|
|
59
|
+
"PYTHONPATH", // module resolution hijack
|
|
60
|
+
"PYTHONINSPECT", // drops into REPL after script — sandbox escape vector
|
|
61
|
+
"PYTHONHOME", // python install prefix hijack
|
|
62
|
+
"PYTHONNOUSERSITE", // disables user-site isolation — sandbox weakening
|
|
63
|
+
// Language runtime hijacking — Ruby
|
|
64
|
+
"RUBYLIB", // ruby load path hijack
|
|
65
|
+
"RUBYOPT", // injects ruby command-line opts
|
|
66
|
+
// Language runtime hijacking — Perl
|
|
67
|
+
"PERL5LIB", // perl @INC hijack
|
|
68
|
+
"PERL5OPT", // injects perl command-line opts
|
|
69
|
+
// Language runtime hijacking — Java
|
|
70
|
+
"JAVA_TOOL_OPTIONS", // honoured by every JVM — flag injection / agent load
|
|
71
|
+
"JDK_JAVA_OPTIONS", // JDK launcher options injection
|
|
72
|
+
"_JAVA_OPTIONS", // legacy JVM options injection
|
|
73
|
+
// Git (RCE via git invocations)
|
|
74
|
+
"GIT_SSH_COMMAND", // replaces ssh with arbitrary command (RCE)
|
|
75
|
+
"GIT_EXTERNAL_DIFF", // runs arbitrary command during diff (RCE)
|
|
76
|
+
"GIT_PAGER", // runs arbitrary command for paging (RCE)
|
|
77
|
+
"GIT_EDITOR", // runs arbitrary command for editor (RCE)
|
|
78
|
+
// Interactive-tool invocation hijack — high FP rate but documented RCE vectors
|
|
79
|
+
"EDITOR", // invoked by git, crontab, sudoedit, etc. (RCE)
|
|
80
|
+
"VISUAL", // EDITOR fallback used by many tools (RCE)
|
|
81
|
+
"PAGER", // invoked by git, man, systemctl, etc. (RCE)
|
|
82
|
+
]);
|
|
83
|
+
/**
|
|
84
|
+
* Pattern-based dangerous key matchers.
|
|
85
|
+
*
|
|
86
|
+
* Some attack vectors target a family of variable names rather than a single
|
|
87
|
+
* literal — most famously Shellshock (CVE-2014-6271), which exploits keys
|
|
88
|
+
* prefixed with `BASH_FUNC_`. Listing every concrete name is impossible; we
|
|
89
|
+
* test against this pattern set in addition to the literal `Set`.
|
|
90
|
+
*/
|
|
91
|
+
export const DANGEROUS_VAULT_KEY_PATTERNS = [
|
|
92
|
+
{
|
|
93
|
+
// CVE-2014-6271 (Shellshock) — bash imports exported functions named
|
|
94
|
+
// `BASH_FUNC_<name>%%` and parses their bodies, enabling RCE.
|
|
95
|
+
pattern: /^BASH_FUNC_/,
|
|
96
|
+
reason: "Shellshock-class bash function injection (CVE-2014-6271)",
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
/**
|
|
100
|
+
* Returns `true` if the given key name is dangerous — either by literal match
|
|
101
|
+
* against `DANGEROUS_VAULT_KEYS` or by matching any entry in
|
|
102
|
+
* `DANGEROUS_VAULT_KEY_PATTERNS`.
|
|
103
|
+
*/
|
|
104
|
+
export function isDangerousVaultKey(key) {
|
|
105
|
+
if (DANGEROUS_VAULT_KEYS.has(key))
|
|
106
|
+
return true;
|
|
107
|
+
for (const { pattern } of DANGEROUS_VAULT_KEY_PATTERNS) {
|
|
108
|
+
if (pattern.test(key))
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
// ── Checker ───────────────────────────────────────────────────────────────────
|
|
114
|
+
/**
|
|
115
|
+
* Inspect a vault `.env` file and return a lint finding for every key whose
|
|
116
|
+
* name appears in `DANGEROUS_VAULT_KEYS` or matches a pattern in
|
|
117
|
+
* `DANGEROUS_VAULT_KEY_PATTERNS`.
|
|
118
|
+
*
|
|
119
|
+
* @param vaultPath Absolute path to the `.env` file.
|
|
120
|
+
* @param relPath Stash-relative path used as the `file` field in findings
|
|
121
|
+
* (e.g. `"vaults/prod.env"`).
|
|
122
|
+
* @param vaultRef Human-readable vault ref (e.g. `"vault:prod"`) shown in
|
|
123
|
+
* the finding message.
|
|
124
|
+
*/
|
|
125
|
+
export function checkVaultForDangerousKeys(vaultPath, relPath, vaultRef) {
|
|
126
|
+
const { keys } = listKeys(vaultPath);
|
|
127
|
+
const issues = [];
|
|
128
|
+
for (const key of keys) {
|
|
129
|
+
if (!isDangerousVaultKey(key))
|
|
130
|
+
continue;
|
|
131
|
+
issues.push({
|
|
132
|
+
file: relPath,
|
|
133
|
+
issue: "dangerous-vault-key",
|
|
134
|
+
detail: `Vault key \`${key}\` can be used to hijack process execution when injected via \`akm vault run\`. Vault ref: ${vaultRef}. Review this vault file before running \`akm vault run\` commands against untrusted stashes.`,
|
|
135
|
+
fixed: false,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return issues;
|
|
139
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { BaseLinter } from "./base-linter";
|
|
6
|
+
const PLACEHOLDER_STRINGS = ["Describe what this workflow accomplishes", "Example Workflow"];
|
|
7
|
+
/**
|
|
8
|
+
* Linter for `workflows/` assets.
|
|
9
|
+
*
|
|
10
|
+
* Extra check beyond base:
|
|
11
|
+
* - `placeholder-stub`: body contains a known placeholder string.
|
|
12
|
+
* Fix: delete the file.
|
|
13
|
+
*/
|
|
14
|
+
export class WorkflowLinter extends BaseLinter {
|
|
15
|
+
types = ["workflows"];
|
|
16
|
+
lint(ctx) {
|
|
17
|
+
const issues = this.runBaseChecks(ctx);
|
|
18
|
+
const placeholderMatch = this.#checkPlaceholderStub(ctx.body);
|
|
19
|
+
if (placeholderMatch) {
|
|
20
|
+
if (ctx.fix) {
|
|
21
|
+
try {
|
|
22
|
+
fs.unlinkSync(ctx.filePath);
|
|
23
|
+
issues.push({
|
|
24
|
+
file: ctx.relPath,
|
|
25
|
+
issue: "placeholder-stub",
|
|
26
|
+
detail: `deleted: found "${placeholderMatch}"`,
|
|
27
|
+
fixed: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
issues.push({
|
|
32
|
+
file: ctx.relPath,
|
|
33
|
+
issue: "placeholder-stub",
|
|
34
|
+
detail: `could not delete: ${e instanceof Error ? e.message : String(e)}`,
|
|
35
|
+
fixed: false,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return issues;
|
|
39
|
+
}
|
|
40
|
+
issues.push({
|
|
41
|
+
file: ctx.relPath,
|
|
42
|
+
issue: "placeholder-stub",
|
|
43
|
+
detail: `placeholder text: "${placeholderMatch}"`,
|
|
44
|
+
fixed: false,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return issues;
|
|
48
|
+
}
|
|
49
|
+
#checkPlaceholderStub(body) {
|
|
50
|
+
for (const placeholder of PLACEHOLDER_STRINGS) {
|
|
51
|
+
if (body.includes(placeholder))
|
|
52
|
+
return placeholder;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
import fs from "node:fs";
|
|
2
5
|
import path from "node:path";
|
|
3
|
-
const CHANGELOG_URL = "https://github.com/itlackey/akm/blob/main
|
|
6
|
+
const CHANGELOG_URL = "https://github.com/itlackey/akm/blob/main/CHANGELOG.md";
|
|
4
7
|
const MIGRATION_DOC_URL = "https://github.com/itlackey/akm/blob/main/docs/migration/v0.5-to-v0.6.md";
|
|
5
8
|
/**
|
|
6
9
|
* Directory containing per-version release notes. Resolved relative to
|
|
@@ -14,7 +17,7 @@ function releaseNotesDir() {
|
|
|
14
17
|
}
|
|
15
18
|
function loadChangelog() {
|
|
16
19
|
try {
|
|
17
|
-
const changelogPath = path.resolve(import.meta.dir, "
|
|
20
|
+
const changelogPath = path.resolve(import.meta.dir, "../../CHANGELOG.md");
|
|
18
21
|
if (fs.existsSync(changelogPath)) {
|
|
19
22
|
return fs.readFileSync(changelogPath, "utf8");
|
|
20
23
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* `akm proposal {list,show,accept,reject,diff}` — review surface for the
|
|
3
6
|
* proposal substrate (#225).
|
|
@@ -12,7 +15,7 @@ import { resolveStashDir } from "../core/common";
|
|
|
12
15
|
import { loadConfig } from "../core/config";
|
|
13
16
|
import { UsageError } from "../core/errors";
|
|
14
17
|
import { appendEvent } from "../core/events";
|
|
15
|
-
import { archiveProposal, createProposal, diffProposal, getProposal, listProposals, promoteProposal, validateProposal, } from "../core/proposals";
|
|
18
|
+
import { archiveProposal, createProposal, diffProposal, getProposal, isProposalSkipped, listProposals, promoteProposal, resolveProposalId, revertProposal, validateProposal, } from "../core/proposals";
|
|
16
19
|
// ── Shared helpers ──────────────────────────────────────────────────────────
|
|
17
20
|
function resolveStash(stashDir) {
|
|
18
21
|
if (stashDir)
|
|
@@ -21,9 +24,12 @@ function resolveStash(stashDir) {
|
|
|
21
24
|
}
|
|
22
25
|
export function akmProposalList(options = {}) {
|
|
23
26
|
const stash = resolveStash(options.stashDir);
|
|
24
|
-
// `--status accepted|rejected` implies archive-inclusion since the
|
|
25
|
-
// queue only ever contains pending entries.
|
|
26
|
-
const includeArchive = options.includeArchive === true ||
|
|
27
|
+
// `--status accepted|rejected|reverted` implies archive-inclusion since the
|
|
28
|
+
// live queue only ever contains pending entries.
|
|
29
|
+
const includeArchive = options.includeArchive === true ||
|
|
30
|
+
options.status === "accepted" ||
|
|
31
|
+
options.status === "rejected" ||
|
|
32
|
+
options.status === "reverted";
|
|
27
33
|
const proposals = listProposals(stash, {
|
|
28
34
|
includeArchive,
|
|
29
35
|
status: options.status,
|
|
@@ -43,7 +49,8 @@ export function akmProposalShow(options) {
|
|
|
43
49
|
export async function akmProposalAccept(options) {
|
|
44
50
|
const stash = resolveStash(options.stashDir);
|
|
45
51
|
const config = options.config ?? loadConfig();
|
|
46
|
-
const
|
|
52
|
+
const resolvedId = resolveProposalId(stash, options.id).id;
|
|
53
|
+
const result = await promoteProposal(stash, config, resolvedId, { target: options.target }, options.ctx);
|
|
47
54
|
// Emit `promoted` to the events stream so observers (audit, dashboards,
|
|
48
55
|
// sync) see the accept happen. Only emit on the happy path — promotion
|
|
49
56
|
// throws on validation failure, so reaching this point means the asset
|
|
@@ -69,11 +76,11 @@ export async function akmProposalAccept(options) {
|
|
|
69
76
|
}
|
|
70
77
|
export function akmProposalReject(options) {
|
|
71
78
|
const stash = resolveStash(options.stashDir);
|
|
72
|
-
const existing =
|
|
79
|
+
const existing = resolveProposalId(stash, options.id);
|
|
73
80
|
if (existing.status !== "pending") {
|
|
74
|
-
throw new UsageError(`Proposal ${
|
|
81
|
+
throw new UsageError(`Proposal ${existing.id} is not pending (current status: ${existing.status}). Only pending proposals can be rejected.`, "INVALID_FLAG_VALUE");
|
|
75
82
|
}
|
|
76
|
-
const updated = archiveProposal(stash,
|
|
83
|
+
const updated = archiveProposal(stash, existing.id, "rejected", options.reason, options.ctx);
|
|
77
84
|
appendEvent({
|
|
78
85
|
eventType: "rejected",
|
|
79
86
|
ref: updated.ref,
|
|
@@ -96,8 +103,8 @@ export function akmProposalReject(options) {
|
|
|
96
103
|
export function akmProposalDiff(options) {
|
|
97
104
|
const stash = resolveStash(options.stashDir);
|
|
98
105
|
const config = options.config ?? loadConfig();
|
|
99
|
-
const proposal =
|
|
100
|
-
const diff = diffProposal(stash, config,
|
|
106
|
+
const proposal = resolveProposalId(stash, options.id);
|
|
107
|
+
const diff = diffProposal(stash, config, proposal.id, { target: options.target });
|
|
101
108
|
return {
|
|
102
109
|
schemaVersion: 1,
|
|
103
110
|
id: proposal.id,
|
|
@@ -109,11 +116,58 @@ export function akmProposalDiff(options) {
|
|
|
109
116
|
}
|
|
110
117
|
export function akmProposalCreate(options) {
|
|
111
118
|
const stash = resolveStash(options.stashDir);
|
|
112
|
-
|
|
119
|
+
// Manual proposal creation (via `akm proposal create`) always bypasses
|
|
120
|
+
// dedup/cooldown guards — the operator is explicitly requesting a proposal.
|
|
121
|
+
const result = createProposal(stash, {
|
|
113
122
|
ref: options.ref,
|
|
114
123
|
source: options.source,
|
|
115
124
|
...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
|
|
116
125
|
payload: options.payload,
|
|
126
|
+
force: true,
|
|
117
127
|
}, options.ctx);
|
|
118
|
-
|
|
128
|
+
if (isProposalSkipped(result)) {
|
|
129
|
+
// Should never happen with force:true — defensive only.
|
|
130
|
+
throw new Error(`Unexpected proposal skip: ${result.message}`);
|
|
131
|
+
}
|
|
132
|
+
return { schemaVersion: 1, ok: true, proposal: result };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Restore an accepted proposal's prior content from the backup captured at
|
|
136
|
+
* promotion time (Advantage D6c / Phase 6C).
|
|
137
|
+
*
|
|
138
|
+
* Failure modes (all surface as typed errors so the CLI can map exit codes):
|
|
139
|
+
* - Proposal id does not resolve → `NotFoundError("FILE_NOT_FOUND")`
|
|
140
|
+
* (raised by `resolveProposalId` / `getProposal`).
|
|
141
|
+
* - Proposal is not `status === "accepted"` → `UsageError("INVALID_FLAG_VALUE")`
|
|
142
|
+
* with message `"only accepted proposals can be reverted ..."`.
|
|
143
|
+
* - No `backup` field, or the backup file is missing on disk →
|
|
144
|
+
* `UsageError` with message `"no backup available for this proposal ..."`.
|
|
145
|
+
*
|
|
146
|
+
* On success, emits a `proposal_reverted` event for observability, mirroring
|
|
147
|
+
* how `akmProposalAccept` emits `promoted` and `akmProposalReject` emits
|
|
148
|
+
* `rejected`.
|
|
149
|
+
*/
|
|
150
|
+
export async function akmProposalRevert(options) {
|
|
151
|
+
const stash = resolveStash(options.stashDir);
|
|
152
|
+
const config = options.config ?? loadConfig();
|
|
153
|
+
const resolvedId = resolveProposalId(stash, options.id).id;
|
|
154
|
+
const result = await revertProposal(stash, config, resolvedId, { target: options.target }, options.ctx);
|
|
155
|
+
appendEvent({
|
|
156
|
+
eventType: "proposal_reverted",
|
|
157
|
+
ref: result.ref,
|
|
158
|
+
metadata: {
|
|
159
|
+
proposalId: result.proposal.id,
|
|
160
|
+
source: result.proposal.source,
|
|
161
|
+
...(result.proposal.sourceRun !== undefined ? { sourceRun: result.proposal.sourceRun } : {}),
|
|
162
|
+
assetPath: result.assetPath,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
return {
|
|
166
|
+
schemaVersion: 1,
|
|
167
|
+
ok: true,
|
|
168
|
+
id: result.proposal.id,
|
|
169
|
+
ref: result.ref,
|
|
170
|
+
assetPath: result.assetPath,
|
|
171
|
+
proposal: result.proposal,
|
|
172
|
+
};
|
|
119
173
|
}
|
package/dist/commands/propose.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* `akm propose <type> <name> --task ...` — proposal-producing agent
|
|
3
6
|
* command (#226).
|
|
@@ -9,37 +12,23 @@
|
|
|
9
12
|
* Failures use the same {@link AgentFailureReason} discriminants as
|
|
10
13
|
* `akm reflect`. `propose_invoked` is emitted at command entry.
|
|
11
14
|
*/
|
|
15
|
+
import fs from "node:fs";
|
|
12
16
|
import { parseAssetRef } from "../core/asset-ref";
|
|
13
17
|
import { TYPE_DIRS } from "../core/asset-spec";
|
|
14
18
|
import { resolveStashDir } from "../core/common";
|
|
15
|
-
import { loadConfig } from "../core/config";
|
|
16
19
|
import { ConfigError, UsageError } from "../core/errors";
|
|
17
20
|
import { appendEvent } from "../core/events";
|
|
18
|
-
import { createProposal } from "../core/proposals";
|
|
19
|
-
import {
|
|
21
|
+
import { createProposal, isProposalSkipped, } from "../core/proposals";
|
|
22
|
+
import { runAgent, } from "../integrations/agent";
|
|
23
|
+
import { resolveProcessAgentProfile } from "../integrations/agent/config";
|
|
20
24
|
import { buildProposePrompt, parseAgentProposalPayload } from "../integrations/agent/prompts";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return parseAgentConfig(config.agent);
|
|
24
|
-
}
|
|
25
|
-
function resolveProfile(options) {
|
|
26
|
-
if (options.agentProfile)
|
|
27
|
-
return options.agentProfile;
|
|
28
|
-
const agent = options.agentConfig ?? loadAgentConfigFromDisk();
|
|
29
|
-
return requireAgentProfile(agent, options.profile);
|
|
30
|
-
}
|
|
25
|
+
import { runAgentSdk } from "../integrations/agent/sdk-runner";
|
|
26
|
+
import { baseFailureFields, enoentHintMessage, isEnoentFailure, loadAgentConfigFromDisk, resolveAgentProfile, } from "./agent-support";
|
|
31
27
|
function failureEnvelope(result, type, name, fallbackReason = "non_zero_exit") {
|
|
32
|
-
const reason = result.reason ?? fallbackReason;
|
|
33
28
|
return {
|
|
34
|
-
|
|
35
|
-
ok: false,
|
|
36
|
-
reason,
|
|
37
|
-
error: result.error ?? `agent failure (${reason})`,
|
|
29
|
+
...baseFailureFields(result, fallbackReason),
|
|
38
30
|
type,
|
|
39
31
|
name,
|
|
40
|
-
exitCode: result.exitCode,
|
|
41
|
-
...(result.stdout ? { stdout: result.stdout } : {}),
|
|
42
|
-
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
43
32
|
};
|
|
44
33
|
}
|
|
45
34
|
export async function akmPropose(options) {
|
|
@@ -68,9 +57,31 @@ export async function akmPropose(options) {
|
|
|
68
57
|
},
|
|
69
58
|
});
|
|
70
59
|
// 2. Resolve profile.
|
|
60
|
+
// When an explicit --profile flag is given, honour it directly (existing
|
|
61
|
+
// behaviour). Otherwise use resolveProcessAgentProfile so that per-process
|
|
62
|
+
// agent config (agent.processes["propose"]) is picked up automatically.
|
|
71
63
|
let profile;
|
|
64
|
+
let resolvedTimeoutMs = options.timeoutMs;
|
|
72
65
|
try {
|
|
73
|
-
|
|
66
|
+
if (options.agentProfile) {
|
|
67
|
+
// Test seam: injected profile bypasses all config.
|
|
68
|
+
profile = options.agentProfile;
|
|
69
|
+
}
|
|
70
|
+
else if (options.profile) {
|
|
71
|
+
// Explicit --profile flag wins over process config.
|
|
72
|
+
profile = resolveAgentProfile(options);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// Use per-process config resolution (falls back to agent.default).
|
|
76
|
+
const agent = options.agentConfig ?? loadAgentConfigFromDisk();
|
|
77
|
+
const processName = options.agentProcess ?? "propose";
|
|
78
|
+
const resolved = resolveProcessAgentProfile(processName, agent);
|
|
79
|
+
profile = resolved.profile;
|
|
80
|
+
// Only apply process-resolved timeoutMs when caller didn't supply one.
|
|
81
|
+
if (resolvedTimeoutMs === undefined) {
|
|
82
|
+
resolvedTimeoutMs = resolved.timeoutMs;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
74
85
|
}
|
|
75
86
|
catch (err) {
|
|
76
87
|
if (err instanceof ConfigError || err instanceof UsageError)
|
|
@@ -91,20 +102,40 @@ export async function akmPropose(options) {
|
|
|
91
102
|
// 4. Spawn the agent.
|
|
92
103
|
// Real agent runs use interactive mode so file tools can write the draft.
|
|
93
104
|
// Injected/custom spawns still need captured stdout for JSON payload tests.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
// Use callAi for the unified AI dispatch path (agent CLI preferred, LLM HTTP fallback).
|
|
106
|
+
const useCustomSpawn = Boolean(options.runAgentOptions?.spawn);
|
|
107
|
+
let result;
|
|
108
|
+
if (useCustomSpawn) {
|
|
109
|
+
// Test seam: use raw runAgent with injected spawn so tests remain deterministic.
|
|
110
|
+
const runOptions = {
|
|
111
|
+
stdio: "captured",
|
|
112
|
+
parseOutput: "text",
|
|
113
|
+
...(resolvedTimeoutMs !== undefined ? { timeoutMs: resolvedTimeoutMs } : {}),
|
|
114
|
+
...(options.runAgentOptions ?? {}),
|
|
115
|
+
};
|
|
116
|
+
result = await runAgent(profile, prompt, runOptions);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Production path: dispatch directly to the appropriate runner.
|
|
120
|
+
const runOptions = {
|
|
121
|
+
stdio: resolvedDraftPath ? "interactive" : "captured",
|
|
122
|
+
parseOutput: "text",
|
|
123
|
+
...(resolvedTimeoutMs !== undefined ? { timeoutMs: resolvedTimeoutMs } : {}),
|
|
124
|
+
};
|
|
125
|
+
result = profile.sdkMode
|
|
126
|
+
? await runAgentSdk(profile, prompt ?? "", runOptions)
|
|
127
|
+
: await runAgent(profile, prompt, runOptions);
|
|
128
|
+
}
|
|
101
129
|
if (!result.ok) {
|
|
130
|
+
// B3: ENOENT / not-found gives an actionable hint.
|
|
131
|
+
if (isEnoentFailure(result)) {
|
|
132
|
+
return { ...failureEnvelope(result, options.type, options.name), error: enoentHintMessage(profile.bin) };
|
|
133
|
+
}
|
|
102
134
|
return failureEnvelope(result, options.type, options.name);
|
|
103
135
|
}
|
|
104
136
|
// 5. Resolve the proposal content.
|
|
105
137
|
// Path A: opencode wrote the draft file — read it directly (no stdout parse).
|
|
106
138
|
// Path B: fallback to stdout JSON parse for non-file-writing agents.
|
|
107
|
-
const fs = await import("node:fs");
|
|
108
139
|
let payload;
|
|
109
140
|
if (fs.existsSync(resolvedDraftPath)) {
|
|
110
141
|
const draftContent = fs.readFileSync(resolvedDraftPath, "utf8");
|
|
@@ -115,6 +146,21 @@ export async function akmPropose(options) {
|
|
|
115
146
|
};
|
|
116
147
|
}
|
|
117
148
|
else {
|
|
149
|
+
// B1: When interactive mode was used and stdout is empty, the agent did not
|
|
150
|
+
// write the draft file and stdout was not captured — surface an actionable error.
|
|
151
|
+
const stdioWasInteractive = !useCustomSpawn;
|
|
152
|
+
if (stdioWasInteractive && (result.stdout ?? "") === "") {
|
|
153
|
+
return {
|
|
154
|
+
schemaVersion: 1,
|
|
155
|
+
ok: false,
|
|
156
|
+
reason: "parse_error",
|
|
157
|
+
error: "Agent did not write draft file and stdout was not captured (interactive mode). Check that the agent CLI understood the file-write instruction, or configure a headless profile with stdio: 'captured'.",
|
|
158
|
+
type: options.type,
|
|
159
|
+
name: options.name,
|
|
160
|
+
exitCode: result.exitCode,
|
|
161
|
+
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
118
164
|
try {
|
|
119
165
|
payload = parseAgentProposalPayload(result.stdout ?? "");
|
|
120
166
|
}
|
|
@@ -174,12 +220,21 @@ export async function akmPropose(options) {
|
|
|
174
220
|
ref,
|
|
175
221
|
source: "propose",
|
|
176
222
|
sourceRun: `propose-${Date.now()}`,
|
|
223
|
+
// User-initiated proposals always bypass dedup/cooldown guards — the
|
|
224
|
+
// operator is explicitly asking for a new proposal.
|
|
225
|
+
force: true,
|
|
177
226
|
payload: {
|
|
178
227
|
content: payload.content,
|
|
179
228
|
...(payload.frontmatter ? { frontmatter: payload.frontmatter } : {}),
|
|
180
229
|
},
|
|
181
230
|
};
|
|
182
|
-
const
|
|
231
|
+
const proposalResult = createProposal(stash, createInput, options.ctx);
|
|
232
|
+
// With force:true, the result is always a Proposal (never skipped).
|
|
233
|
+
if (isProposalSkipped(proposalResult)) {
|
|
234
|
+
// Should never happen when force:true, but be defensive.
|
|
235
|
+
throw new Error(`Unexpected skip in propose command: ${proposalResult.message}`);
|
|
236
|
+
}
|
|
237
|
+
const proposal = proposalResult;
|
|
183
238
|
return {
|
|
184
239
|
schemaVersion: 1,
|
|
185
240
|
ok: true,
|