@strongkeep/watchdog 0.1.1

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.
Files changed (67) hide show
  1. package/assets/PINNED_PUBLIC_KEY.txt +13 -0
  2. package/assets/healthcheck-skill/SKILL.md +28 -0
  3. package/assets/skill/SKILL.md +30 -0
  4. package/dist/appliers/add-hook.d.ts +19 -0
  5. package/dist/appliers/add-hook.js +81 -0
  6. package/dist/appliers/add-hook.js.map +1 -0
  7. package/dist/appliers/applier.d.ts +86 -0
  8. package/dist/appliers/applier.js +60 -0
  9. package/dist/appliers/applier.js.map +1 -0
  10. package/dist/appliers/block-domain.d.ts +20 -0
  11. package/dist/appliers/block-domain.js +77 -0
  12. package/dist/appliers/block-domain.js.map +1 -0
  13. package/dist/appliers/fs-guard.d.ts +36 -0
  14. package/dist/appliers/fs-guard.js +109 -0
  15. package/dist/appliers/fs-guard.js.map +1 -0
  16. package/dist/appliers/min-version.d.ts +15 -0
  17. package/dist/appliers/min-version.js +47 -0
  18. package/dist/appliers/min-version.js.map +1 -0
  19. package/dist/appliers/pin-mcp.d.ts +17 -0
  20. package/dist/appliers/pin-mcp.js +99 -0
  21. package/dist/appliers/pin-mcp.js.map +1 -0
  22. package/dist/appliers/set-config.d.ts +12 -0
  23. package/dist/appliers/set-config.js +67 -0
  24. package/dist/appliers/set-config.js.map +1 -0
  25. package/dist/appliers/update-rule-file.d.ts +15 -0
  26. package/dist/appliers/update-rule-file.js +69 -0
  27. package/dist/appliers/update-rule-file.js.map +1 -0
  28. package/dist/autonomy.d.ts +24 -0
  29. package/dist/autonomy.js +35 -0
  30. package/dist/autonomy.js.map +1 -0
  31. package/dist/cli/guard-check.d.ts +16 -0
  32. package/dist/cli/guard-check.js +108 -0
  33. package/dist/cli/guard-check.js.map +1 -0
  34. package/dist/cli/init.d.ts +37 -0
  35. package/dist/cli/init.js +226 -0
  36. package/dist/cli/init.js.map +1 -0
  37. package/dist/cli/login.d.ts +32 -0
  38. package/dist/cli/login.js +144 -0
  39. package/dist/cli/login.js.map +1 -0
  40. package/dist/cli/mcp.d.ts +23 -0
  41. package/dist/cli/mcp.js +84 -0
  42. package/dist/cli/mcp.js.map +1 -0
  43. package/dist/cli/poll.d.ts +20 -0
  44. package/dist/cli/poll.js +111 -0
  45. package/dist/cli/poll.js.map +1 -0
  46. package/dist/cli/revert.d.ts +31 -0
  47. package/dist/cli/revert.js +129 -0
  48. package/dist/cli/revert.js.map +1 -0
  49. package/dist/feed-client.d.ts +110 -0
  50. package/dist/feed-client.js +178 -0
  51. package/dist/feed-client.js.map +1 -0
  52. package/dist/fires.d.ts +28 -0
  53. package/dist/fires.js +36 -0
  54. package/dist/fires.js.map +1 -0
  55. package/dist/guard-check.d.ts +39 -0
  56. package/dist/guard-check.js +94 -0
  57. package/dist/guard-check.js.map +1 -0
  58. package/dist/init.d.ts +105 -0
  59. package/dist/init.js +283 -0
  60. package/dist/init.js.map +1 -0
  61. package/dist/pipeline.d.ts +50 -0
  62. package/dist/pipeline.js +110 -0
  63. package/dist/pipeline.js.map +1 -0
  64. package/dist/undo.d.ts +98 -0
  65. package/dist/undo.js +67 -0
  66. package/dist/undo.js.map +1 -0
  67. package/package.json +40 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Applier: min_version {tool, version} — require a tool version floor (CVE gate).
3
+ *
4
+ * This applier is ADVISORY: it cannot and must not upgrade software on the user's machine.
5
+ * "apply" here means: check the installed version against the floor and REPORT if below, with
6
+ * the fix instruction. It writes NOTHING, so its undo inverse is a no-op.
7
+ *
8
+ * This is why min_version is safe at low blast radius (it never mutates state) but still can't
9
+ * silently "fix" anything — surfacing the gap is the action.
10
+ */
11
+ import { RefusedActionError } from './applier.js';
12
+ /** Compare dotted numeric versions: negative when a < b. Non-numeric parts compare as 0. */
13
+ export function compareVersions(a, b) {
14
+ const pa = a.split('.').map((n) => Number.parseInt(n, 10) || 0);
15
+ const pb = b.split('.').map((n) => Number.parseInt(n, 10) || 0);
16
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
17
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
18
+ if (diff !== 0)
19
+ return diff;
20
+ }
21
+ return 0;
22
+ }
23
+ const NOOP_INVERSE = { kind: 'min_version', noop: true };
24
+ export const minVersionApplier = {
25
+ type: 'min_version',
26
+ async apply(action, ctx) {
27
+ const installed = ctx.resolveInstalledVersion?.(action.tool);
28
+ let summary;
29
+ if (installed === undefined) {
30
+ summary = `advisory: could not determine installed ${action.tool} version — required floor is ${action.version}`;
31
+ }
32
+ else if (compareVersions(installed, action.version) < 0) {
33
+ summary = `advisory: ${action.tool} ${installed} is BELOW the safe floor ${action.version} — update ${action.tool} now`;
34
+ }
35
+ else {
36
+ summary = `advisory: ${action.tool} ${installed} meets the safe floor ${action.version}`;
37
+ }
38
+ // Never mutates anything, in dryRun or not.
39
+ return { summary, inverse: NOOP_INVERSE, diffPreview: '(advisory — no change)' };
40
+ },
41
+ async revert(inverse, _ctx) {
42
+ if (inverse.kind !== 'min_version')
43
+ throw new RefusedActionError('min_version.revert: wrong inverse kind');
44
+ return { summary: 'min_version is advisory — nothing was changed, nothing to revert' };
45
+ },
46
+ };
47
+ //# sourceMappingURL=min-version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"min-version.js","sourceRoot":"","sources":["../../src/appliers/min-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGlD,4FAA4F;AAC5F,MAAM,UAAU,eAAe,CAAC,CAAS,EAAE,CAAS;IAClD,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAChE,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAChE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACxD,MAAM,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACzC,IAAI,IAAI,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;IAC9B,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,YAAY,GAAgB,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AAEtE,MAAM,CAAC,MAAM,iBAAiB,GAA8B;IAC1D,IAAI,EAAE,aAAa;IAEnB,KAAK,CAAC,KAAK,CAAC,MAAwB,EAAE,GAAiB;QACrD,MAAM,SAAS,GAAG,GAAG,CAAC,uBAAuB,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC7D,IAAI,OAAe,CAAC;QACpB,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO,GAAG,2CAA2C,MAAM,CAAC,IAAI,gCAAgC,MAAM,CAAC,OAAO,EAAE,CAAC;QACnH,CAAC;aAAM,IAAI,eAAe,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1D,OAAO,GAAG,aAAa,MAAM,CAAC,IAAI,IAAI,SAAS,4BAA4B,MAAM,CAAC,OAAO,aAAa,MAAM,CAAC,IAAI,MAAM,CAAC;QAC1H,CAAC;aAAM,CAAC;YACN,OAAO,GAAG,aAAa,MAAM,CAAC,IAAI,IAAI,SAAS,yBAAyB,MAAM,CAAC,OAAO,EAAE,CAAC;QAC3F,CAAC;QACD,4CAA4C;QAC5C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,wBAAwB,EAAE,CAAC;IACnF,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,OAAoB,EAAE,IAAkB;QACnD,IAAI,OAAO,CAAC,IAAI,KAAK,aAAa;YAAE,MAAM,IAAI,kBAAkB,CAAC,wCAAwC,CAAC,CAAC;QAC3G,OAAO,EAAE,OAAO,EAAE,kEAAkE,EAAE,CAAC;IACzF,CAAC;CACF,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Applier: pin_mcp {server, version?} — pin a configured MCP server to a reviewed version
3
+ * (anti rug-pull). Supports the ALL_CONFIGURED_MCP sentinel = audit every configured server's
4
+ * pin state (already-pinned specs are untouched; unpinned specs are surfaced for a human —
5
+ * their current version cannot be known locally without a registry call, and guessing one
6
+ * would be a lie).
7
+ *
8
+ * Least privilege: edits only the MCP server spec(s) in the harness's `.mcp.json`. Never
9
+ * installs or runs a server. The version is written by rewriting the `package@version` token
10
+ * in the server's args; undo captures the prior version string + a byte-exact file backup so
11
+ * a revert restores the exact prior spec.
12
+ */
13
+ import type { PinMcpAction } from '@strongkeep/core';
14
+ import type { Applier } from './applier.js';
15
+ /** The MCP config file this applier manages, relative to the harness root. */
16
+ export declare const MCP_CONFIG_FILE = ".mcp.json";
17
+ export declare const pinMcpApplier: Applier<PinMcpAction>;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Applier: pin_mcp {server, version?} — pin a configured MCP server to a reviewed version
3
+ * (anti rug-pull). Supports the ALL_CONFIGURED_MCP sentinel = audit every configured server's
4
+ * pin state (already-pinned specs are untouched; unpinned specs are surfaced for a human —
5
+ * their current version cannot be known locally without a registry call, and guessing one
6
+ * would be a lie).
7
+ *
8
+ * Least privilege: edits only the MCP server spec(s) in the harness's `.mcp.json`. Never
9
+ * installs or runs a server. The version is written by rewriting the `package@version` token
10
+ * in the server's args; undo captures the prior version string + a byte-exact file backup so
11
+ * a revert restores the exact prior spec.
12
+ */
13
+ import { ALL_CONFIGURED_MCP } from '@strongkeep/core';
14
+ import { RefusedActionError } from './applier.js';
15
+ import { resolveInsideHarness, readJsonConfig, writeJsonConfig, backupFile, restoreBackup } from './fs-guard.js';
16
+ /** The MCP config file this applier manages, relative to the harness root. */
17
+ export const MCP_CONFIG_FILE = '.mcp.json';
18
+ /** Match a `name@version` / `@scope/name@version` npm spec token. */
19
+ const SPEC_RE = /^(@?[^@\s]+)@([^@\s]+)$/;
20
+ /** Find the index + parsed spec of the versioned package token in a server's args, if any. */
21
+ function findSpecToken(spec) {
22
+ if (!Array.isArray(spec.args))
23
+ return undefined;
24
+ for (let i = 0; i < spec.args.length; i++) {
25
+ const arg = spec.args[i];
26
+ if (typeof arg !== 'string' || arg.startsWith('-'))
27
+ continue;
28
+ const m = SPEC_RE.exec(arg);
29
+ if (m !== null)
30
+ return { index: i, name: m[1], version: m[2] };
31
+ }
32
+ return undefined;
33
+ }
34
+ export const pinMcpApplier = {
35
+ type: 'pin_mcp',
36
+ async apply(action, ctx) {
37
+ const absPath = resolveInsideHarness(ctx.harnessRoot, MCP_CONFIG_FILE);
38
+ const { value: config, existed } = await readJsonConfig(absPath);
39
+ if (!existed)
40
+ throw new RefusedActionError(`refused pin_mcp: no ${MCP_CONFIG_FILE} in this harness`);
41
+ const servers = (config['mcpServers'] ?? {});
42
+ if (action.server === ALL_CONFIGURED_MCP) {
43
+ // Audit mode: report pin state of every configured server. No writes → no-op inverse.
44
+ const pinned = [];
45
+ const unpinned = [];
46
+ for (const [name, spec] of Object.entries(servers)) {
47
+ (findSpecToken(spec) !== undefined ? pinned : unpinned).push(name);
48
+ }
49
+ const summary = `pin audit: ${pinned.length} server(s) already version-pinned` +
50
+ (unpinned.length > 0 ? `; NEEDS ATTENTION — unpinned: ${unpinned.join(', ')}` : '');
51
+ return {
52
+ summary,
53
+ inverse: { kind: 'pin_mcp', server: ALL_CONFIGURED_MCP, priorVersion: undefined },
54
+ diffPreview: `(audit only) pinned: [${pinned.join(', ')}] unpinned: [${unpinned.join(', ')}]`,
55
+ };
56
+ }
57
+ const spec = servers[action.server];
58
+ if (spec === undefined) {
59
+ throw new RefusedActionError(`refused pin_mcp: server "${action.server}" is not configured`);
60
+ }
61
+ const token = findSpecToken(spec);
62
+ if (token === undefined) {
63
+ throw new RefusedActionError(`refused pin_mcp: server "${action.server}" has no package@version spec to rewrite (pin manually)`);
64
+ }
65
+ const targetVersion = action.version ?? token.version; // no version = pin to current
66
+ const inverse = { kind: 'pin_mcp', server: action.server, priorVersion: token.version };
67
+ const summary = `pinned MCP server "${action.server}" to ${token.name}@${targetVersion} (was @${token.version})`;
68
+ const diffPreview = `- ${token.name}@${token.version}\n+ ${token.name}@${targetVersion}`;
69
+ if (!ctx.dryRun && targetVersion !== token.version) {
70
+ inverse.backupRef = await backupFile(ctx.harnessRoot, absPath); // backup BEFORE write
71
+ spec.args[token.index] = `${token.name}@${targetVersion}`;
72
+ await writeJsonConfig(absPath, config);
73
+ }
74
+ return { summary, inverse, diffPreview };
75
+ },
76
+ async revert(inverse, ctx) {
77
+ if (inverse.kind !== 'pin_mcp')
78
+ throw new RefusedActionError('pin_mcp.revert: wrong inverse kind');
79
+ if (inverse.server === ALL_CONFIGURED_MCP || inverse.priorVersion === undefined) {
80
+ return { summary: 'pin audit had no changes to revert' };
81
+ }
82
+ const absPath = resolveInsideHarness(ctx.harnessRoot, MCP_CONFIG_FILE);
83
+ if (inverse.backupRef !== undefined) {
84
+ await restoreBackup(ctx.harnessRoot, inverse.backupRef, absPath);
85
+ return { summary: `restored ${MCP_CONFIG_FILE} from backup (server "${inverse.server}" back to @${inverse.priorVersion})` };
86
+ }
87
+ const { value: config } = await readJsonConfig(absPath);
88
+ const servers = (config['mcpServers'] ?? {});
89
+ const spec = servers[inverse.server];
90
+ const token = spec === undefined ? undefined : findSpecToken(spec);
91
+ if (spec === undefined || token === undefined) {
92
+ return { summary: `server "${inverse.server}" no longer present — nothing to revert` };
93
+ }
94
+ spec.args[token.index] = `${token.name}@${inverse.priorVersion}`;
95
+ await writeJsonConfig(absPath, config);
96
+ return { summary: `restored server "${inverse.server}" to @${inverse.priorVersion}` };
97
+ },
98
+ };
99
+ //# sourceMappingURL=pin-mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pin-mcp.js","sourceRoot":"","sources":["../../src/appliers/pin-mcp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Applier: set_config {file, keypath, value} — set one config keypath to a safe scalar.
3
+ *
4
+ * Least privilege: writes ONLY the named keypath in the named file, both resolved INSIDE the
5
+ * harness root. Refuses paths that escape harnessRoot (no `..`, no absolute paths). Value is a
6
+ * scalar only (bounded blast radius — the parser already enforces this). Undo captures the
7
+ * prior scalar AND a byte-exact out-of-log backup of the prior file, so revert restores the
8
+ * file exactly (formatting included) and no potentially-secret file content sits in the log.
9
+ */
10
+ import type { SetConfigAction } from '@strongkeep/core';
11
+ import type { Applier } from './applier.js';
12
+ export declare const setConfigApplier: Applier<SetConfigAction>;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Applier: set_config {file, keypath, value} — set one config keypath to a safe scalar.
3
+ *
4
+ * Least privilege: writes ONLY the named keypath in the named file, both resolved INSIDE the
5
+ * harness root. Refuses paths that escape harnessRoot (no `..`, no absolute paths). Value is a
6
+ * scalar only (bounded blast radius — the parser already enforces this). Undo captures the
7
+ * prior scalar AND a byte-exact out-of-log backup of the prior file, so revert restores the
8
+ * file exactly (formatting included) and no potentially-secret file content sits in the log.
9
+ */
10
+ import { RefusedActionError } from './applier.js';
11
+ import { resolveInsideHarness, readJsonConfig, writeJsonConfig, getKeypath, setKeypath, backupFile, restoreBackup, } from './fs-guard.js';
12
+ import { rm } from 'node:fs/promises';
13
+ export const setConfigApplier = {
14
+ type: 'set_config',
15
+ async apply(action, ctx) {
16
+ const absPath = resolveInsideHarness(ctx.harnessRoot, action.file);
17
+ const { value: config, existed } = await readJsonConfig(absPath);
18
+ const rawPrior = getKeypath(config, action.keypath);
19
+ if (rawPrior !== undefined && typeof rawPrior === 'object') {
20
+ // Refuse to clobber a structured subtree with a scalar — outside this action's mandate.
21
+ throw new RefusedActionError(`refused set_config: "${action.keypath}" holds a non-scalar value`);
22
+ }
23
+ const priorValue = rawPrior;
24
+ const inverse = {
25
+ kind: 'set_config',
26
+ file: action.file,
27
+ keypath: action.keypath,
28
+ priorValue,
29
+ fileExistedBefore: existed,
30
+ };
31
+ const diffPreview = [
32
+ `--- ${action.file}`,
33
+ `+++ ${action.file}`,
34
+ `- ${action.keypath}: ${JSON.stringify(priorValue)}`,
35
+ `+ ${action.keypath}: ${JSON.stringify(action.value)}`,
36
+ ].join('\n');
37
+ const summary = `set ${action.keypath} = ${JSON.stringify(action.value)} in ${action.file}`;
38
+ if (!ctx.dryRun) {
39
+ if (existed)
40
+ inverse.backupRef = await backupFile(ctx.harnessRoot, absPath); // backup BEFORE write
41
+ setKeypath(config, action.keypath, action.value);
42
+ await writeJsonConfig(absPath, config);
43
+ }
44
+ return { summary, inverse, diffPreview };
45
+ },
46
+ async revert(inverse, ctx) {
47
+ if (inverse.kind !== 'set_config')
48
+ throw new RefusedActionError('set_config.revert: wrong inverse kind');
49
+ const absPath = resolveInsideHarness(ctx.harnessRoot, inverse.file);
50
+ if (!inverse.fileExistedBefore) {
51
+ // The apply created the file; revert removes it entirely.
52
+ await rm(absPath, { force: true });
53
+ return { summary: `removed ${inverse.file} (did not exist before apply)` };
54
+ }
55
+ if (inverse.backupRef !== undefined) {
56
+ // Byte-exact restore from the out-of-log backup (T-REV-1).
57
+ await restoreBackup(ctx.harnessRoot, inverse.backupRef, absPath);
58
+ return { summary: `restored ${inverse.file} from backup (byte-exact)` };
59
+ }
60
+ // No backup (e.g. record from a staged run) — restore the scalar semantically.
61
+ const { value: config } = await readJsonConfig(absPath);
62
+ setKeypath(config, inverse.keypath, inverse.priorValue);
63
+ await writeJsonConfig(absPath, config);
64
+ return { summary: `restored ${inverse.keypath} = ${JSON.stringify(inverse.priorValue)} in ${inverse.file}` };
65
+ },
66
+ };
67
+ //# sourceMappingURL=set-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"set-config.js","sourceRoot":"","sources":["../../src/appliers/set-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Applier: update_rule_file {path, content_hash, payloadRef?} — drop/refresh a hash-pinned
3
+ * rule file inside the harness.
4
+ *
5
+ * SECURITY:
6
+ * - The written content is resolved out-of-band and VERIFIED against content_hash
7
+ * (core.verifyPayloadHash) BEFORE writing. Hash mismatch → refuse, write nothing.
8
+ * - `path` is resolved INSIDE harnessRoot; escapes (`..`, absolute) are refused (T-APP-5).
9
+ * - The rule file is data read by guard hooks; this applier never executes it.
10
+ * Undo backs up any prior file (backupRef) so revert restores byte-for-byte, or removes the
11
+ * file if none existed before.
12
+ */
13
+ import type { UpdateRuleFileAction } from '@strongkeep/core';
14
+ import type { Applier } from './applier.js';
15
+ export declare const updateRuleFileApplier: Applier<UpdateRuleFileAction>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Applier: update_rule_file {path, content_hash, payloadRef?} — drop/refresh a hash-pinned
3
+ * rule file inside the harness.
4
+ *
5
+ * SECURITY:
6
+ * - The written content is resolved out-of-band and VERIFIED against content_hash
7
+ * (core.verifyPayloadHash) BEFORE writing. Hash mismatch → refuse, write nothing.
8
+ * - `path` is resolved INSIDE harnessRoot; escapes (`..`, absolute) are refused (T-APP-5).
9
+ * - The rule file is data read by guard hooks; this applier never executes it.
10
+ * Undo backs up any prior file (backupRef) so revert restores byte-for-byte, or removes the
11
+ * file if none existed before.
12
+ */
13
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
14
+ import { dirname } from 'node:path';
15
+ import { verifyPayloadHash } from '@strongkeep/core';
16
+ import { RefusedActionError } from './applier.js';
17
+ import { resolveInsideHarness, readIfExists, backupFile, restoreBackup } from './fs-guard.js';
18
+ export const updateRuleFileApplier = {
19
+ type: 'update_rule_file',
20
+ async apply(action, ctx) {
21
+ // Path guard FIRST — a hostile path never reaches any I/O.
22
+ const absPath = resolveInsideHarness(ctx.harnessRoot, action.path);
23
+ if (action.payloadRef === undefined) {
24
+ throw new RefusedActionError(`refused update_rule_file "${action.path}": no payload source to write from`);
25
+ }
26
+ if (ctx.resolvePayload === undefined) {
27
+ throw new RefusedActionError(`refused update_rule_file "${action.path}": no payload resolver available`);
28
+ }
29
+ let bytes;
30
+ try {
31
+ bytes = await ctx.resolvePayload(action.payloadRef);
32
+ }
33
+ catch (err) {
34
+ // An unavailable payload is a REFUSAL (loud, run continues), never a crash.
35
+ throw new RefusedActionError(`refused update_rule_file "${action.path}": payload "${action.payloadRef.path}" unavailable (${err instanceof Error ? err.message : String(err)})`);
36
+ }
37
+ // The parser guarantees payloadRef.sha256 === content_hash; verify the BYTES against it.
38
+ const check = verifyPayloadHash(bytes, action.content_hash);
39
+ if (!check.ok) {
40
+ throw new RefusedActionError(`refused update_rule_file "${action.path}": ${check.reason} — nothing written`);
41
+ }
42
+ const existedBefore = (await readIfExists(absPath)) !== undefined;
43
+ const inverse = { kind: 'update_rule_file', path: action.path, existedBefore };
44
+ const summary = `${existedBefore ? 'refreshed' : 'wrote'} rule file ${action.path} (hash-verified)`;
45
+ const diffPreview = `${existedBefore ? '~' : '+'} ${action.path} (${bytes.byteLength} bytes, sha256 ${action.content_hash.slice(0, 12)}…)`;
46
+ if (!ctx.dryRun) {
47
+ if (existedBefore)
48
+ inverse.backupRef = await backupFile(ctx.harnessRoot, absPath); // backup BEFORE write
49
+ await mkdir(dirname(absPath), { recursive: true });
50
+ await writeFile(absPath, bytes);
51
+ }
52
+ return { summary, inverse, diffPreview };
53
+ },
54
+ async revert(inverse, ctx) {
55
+ if (inverse.kind !== 'update_rule_file')
56
+ throw new RefusedActionError('update_rule_file.revert: wrong inverse kind');
57
+ const absPath = resolveInsideHarness(ctx.harnessRoot, inverse.path);
58
+ if (!inverse.existedBefore) {
59
+ await rm(absPath, { force: true });
60
+ return { summary: `removed ${inverse.path} (did not exist before apply)` };
61
+ }
62
+ if (inverse.backupRef !== undefined) {
63
+ await restoreBackup(ctx.harnessRoot, inverse.backupRef, absPath);
64
+ return { summary: `restored ${inverse.path} from backup (byte-exact)` };
65
+ }
66
+ return { summary: `${inverse.path} left as-is (pre-existing, no backup captured)` };
67
+ },
68
+ };
69
+ //# sourceMappingURL=update-rule-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-rule-file.js","sourceRoot":"","sources":["../../src/appliers/update-rule-file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Autonomy tiers — decide, per action, whether the consumer reports, stages, or applies.
3
+ *
4
+ * Tiers (BUILD-PLAN + design-notes; SMB default = propose):
5
+ * advise → report only. Never writes. The entry is surfaced to the human; nothing changes.
6
+ * propose → stage a PR-style diff / pending-changes record for human approval. No live write.
7
+ * auto → apply ONLY actions whose LOCAL blast-radius classification is `low`; log + undo.
8
+ * Everything else DOWNGRADES to propose (never silently dropped, never force-applied).
9
+ *
10
+ * SECURITY:
11
+ * - Blast radius is computed LOCALLY (core.blastRadiusOf), NEVER read from the feed, so a
12
+ * hostile feed cannot self-declare an action "low blast radius" to force an auto-apply.
13
+ * - The consumer's configured tier is a CEILING. An entry's own `autonomy` field can only
14
+ * request an EQUAL-OR-LOWER effective tier, never raise the consumer above its setting.
15
+ */
16
+ import type { ConstrainedAction, AutonomyTier } from '@strongkeep/core';
17
+ /** What the pipeline should do with a single action under the effective tier. */
18
+ export type Disposition = 'report' | 'stage' | 'apply';
19
+ /**
20
+ * Decide the disposition of an action given the CONSUMER's configured tier and the ENTRY's
21
+ * requested tier. Effective tier = min(consumerTier, entryTier). Then:
22
+ * advise → report; propose → stage; auto → apply iff blastRadiusOf(action)==='low' else stage.
23
+ */
24
+ export declare function decideDisposition(action: ConstrainedAction, consumerTier: AutonomyTier, entryTier: AutonomyTier): Disposition;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Autonomy tiers — decide, per action, whether the consumer reports, stages, or applies.
3
+ *
4
+ * Tiers (BUILD-PLAN + design-notes; SMB default = propose):
5
+ * advise → report only. Never writes. The entry is surfaced to the human; nothing changes.
6
+ * propose → stage a PR-style diff / pending-changes record for human approval. No live write.
7
+ * auto → apply ONLY actions whose LOCAL blast-radius classification is `low`; log + undo.
8
+ * Everything else DOWNGRADES to propose (never silently dropped, never force-applied).
9
+ *
10
+ * SECURITY:
11
+ * - Blast radius is computed LOCALLY (core.blastRadiusOf), NEVER read from the feed, so a
12
+ * hostile feed cannot self-declare an action "low blast radius" to force an auto-apply.
13
+ * - The consumer's configured tier is a CEILING. An entry's own `autonomy` field can only
14
+ * request an EQUAL-OR-LOWER effective tier, never raise the consumer above its setting.
15
+ */
16
+ import { blastRadiusOf } from '@strongkeep/core';
17
+ const TIER_RANK = { advise: 0, propose: 1, auto: 2 };
18
+ /**
19
+ * Decide the disposition of an action given the CONSUMER's configured tier and the ENTRY's
20
+ * requested tier. Effective tier = min(consumerTier, entryTier). Then:
21
+ * advise → report; propose → stage; auto → apply iff blastRadiusOf(action)==='low' else stage.
22
+ */
23
+ export function decideDisposition(action, consumerTier, entryTier) {
24
+ const effective = TIER_RANK[entryTier] < TIER_RANK[consumerTier] ? entryTier : consumerTier;
25
+ switch (effective) {
26
+ case 'advise':
27
+ return 'report';
28
+ case 'propose':
29
+ return 'stage';
30
+ case 'auto':
31
+ // LOCAL blast-radius gate: high-blast actions downgrade to stage, never auto-apply.
32
+ return blastRadiusOf(action) === 'low' ? 'apply' : 'stage';
33
+ }
34
+ }
35
+ //# sourceMappingURL=autonomy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autonomy.js","sourceRoot":"","sources":["../src/autonomy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAKjD,MAAM,SAAS,GAAiC,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAEnF;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAyB,EACzB,YAA0B,EAC1B,SAAuB;IAEvB,MAAM,SAAS,GAAiB,SAAS,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC;IAC1G,QAAQ,SAAS,EAAE,CAAC;QAClB,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAC;QAClB,KAAK,SAAS;YACZ,OAAO,OAAO,CAAC;QACjB,KAAK,MAAM;YACT,oFAAoF;YACpF,OAAO,aAAa,CAAC,MAAM,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;IAC/D,CAAC;AACH,CAAC"}
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * strongkeep-guard-check — the PreToolUse hook entrypoint that evaluates a tool call against
4
+ * the guard rules, records any fire (metadata only, fires.ndjson), and signals the harness.
5
+ *
6
+ * Reads the Claude Code hook payload as JSON on stdin, extracts the candidate command from
7
+ * tool_input (Bash `command`, or a best-effort string of the input), evaluates it, and:
8
+ * - exits 0 when nothing fires or the strongest action is advise/warn (the call proceeds;
9
+ * a warn prints a one-line reason to stderr),
10
+ * - exits 2 on `block` (Claude Code treats non-zero PreToolUse as "deny the tool call").
11
+ *
12
+ * It NEVER prints or logs the command itself. The fire log carries rule ids only.
13
+ * Installed into the PreToolUse hook by strongkeep-init alongside the poll hook.
14
+ */
15
+ /** Pull the candidate command out of a Claude Code hook payload. Best-effort, tolerant. */
16
+ export declare function extractCommand(payload: unknown): string;
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * strongkeep-guard-check — the PreToolUse hook entrypoint that evaluates a tool call against
4
+ * the guard rules, records any fire (metadata only, fires.ndjson), and signals the harness.
5
+ *
6
+ * Reads the Claude Code hook payload as JSON on stdin, extracts the candidate command from
7
+ * tool_input (Bash `command`, or a best-effort string of the input), evaluates it, and:
8
+ * - exits 0 when nothing fires or the strongest action is advise/warn (the call proceeds;
9
+ * a warn prints a one-line reason to stderr),
10
+ * - exits 2 on `block` (Claude Code treats non-zero PreToolUse as "deny the tool call").
11
+ *
12
+ * It NEVER prints or logs the command itself. The fire log carries rule ids only.
13
+ * Installed into the PreToolUse hook by strongkeep-init alongside the poll hook.
14
+ */
15
+ import process from 'node:process';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { realpathSync } from 'node:fs';
18
+ import { resolve, join } from 'node:path';
19
+ import { evaluateCommand } from '../guard-check.js';
20
+ import { strongkeepDir } from '../init.js';
21
+ /** Read all of stdin as a string (empty on no input). */
22
+ async function readStdin() {
23
+ const chunks = [];
24
+ for await (const chunk of process.stdin)
25
+ chunks.push(chunk);
26
+ return Buffer.concat(chunks).toString('utf8');
27
+ }
28
+ /** Pull the candidate command out of a Claude Code hook payload. Best-effort, tolerant. */
29
+ export function extractCommand(payload) {
30
+ if (typeof payload !== 'object' || payload === null)
31
+ return '';
32
+ const input = payload['tool_input'];
33
+ if (typeof input === 'object' && input !== null) {
34
+ const cmd = input['command'];
35
+ if (typeof cmd === 'string')
36
+ return cmd;
37
+ return JSON.stringify(input);
38
+ }
39
+ return '';
40
+ }
41
+ async function main() {
42
+ const harnessRoot = resolve(process.env['STRONGKEEP_HARNESS'] ?? process.cwd());
43
+ const skDir = join(harnessRoot, strongkeepDir());
44
+ let payload = {};
45
+ try {
46
+ const raw = await readStdin();
47
+ if (raw.trim() !== '')
48
+ payload = JSON.parse(raw);
49
+ }
50
+ catch {
51
+ // Unparseable hook input → nothing to evaluate; let the call proceed.
52
+ return 0;
53
+ }
54
+ const command = extractCommand(payload);
55
+ if (command === '')
56
+ return 0;
57
+ const outcome = await evaluateCommand(skDir, command);
58
+ if (outcome.action === 'block') {
59
+ process.stderr.write(formatBlockMessage(outcome.fired));
60
+ return 2;
61
+ }
62
+ if (outcome.action === 'warn') {
63
+ process.stderr.write(formatWarnMessage(outcome.fired));
64
+ }
65
+ return 0;
66
+ }
67
+ /** Reason text for a fired rule (falls back to the rule id when no reason is authored). */
68
+ function reasonOf(r) {
69
+ return r.reason && r.reason.trim() !== '' ? r.reason.trim() : `matched guard rule ${r.id}`;
70
+ }
71
+ /**
72
+ * The deny message the developer sees when StrongKeep stops the AGENT. Explains WHY (the
73
+ * security reason), names the rule, and points out that a manual `! <command>` is never
74
+ * blocked — so the human always keeps control. No emoji, no command echoed.
75
+ */
76
+ function formatBlockMessage(fired) {
77
+ const blockers = fired.filter((r) => r.action === 'block');
78
+ const lines = [];
79
+ lines.push('StrongKeep blocked this command to keep you safe.');
80
+ for (const r of blockers)
81
+ lines.push(` - ${reasonOf(r)} [rule ${r.id}]`);
82
+ lines.push('This guard stops the AI agent, not you: to run it yourself, use manual bash');
83
+ lines.push('(type "! <command>") — manual commands are never intercepted. Edit or relax the');
84
+ lines.push('rule any time in .claude/strongkeep/guard-rules.yaml.');
85
+ return lines.join('\n') + '\n';
86
+ }
87
+ /** The heads-up shown when a warn-level rule fires. The command still proceeds. */
88
+ function formatWarnMessage(fired) {
89
+ const warners = fired.filter((r) => r.action === 'warn');
90
+ const first = warners[0];
91
+ const why = first ? reasonOf(first) : 'a guarded pattern matched';
92
+ return `StrongKeep heads-up (allowed): ${why}. StrongKeep is watching but let this proceed.\n`;
93
+ }
94
+ function isMainModule() {
95
+ const entry = process.argv[1];
96
+ if (entry === undefined)
97
+ return false;
98
+ try {
99
+ return fileURLToPath(import.meta.url) === realpathSync(entry);
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ }
105
+ if (isMainModule()) {
106
+ main().then((code) => process.exit(code), () => process.exit(0));
107
+ }
108
+ //# sourceMappingURL=guard-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard-check.js","sourceRoot":"","sources":["../../src/cli/guard-check.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * strongkeep-init CLI — write the config pack into the subscriber's harness (D6).
4
+ *
5
+ * Usage:
6
+ * strongkeep-init [--harness <dir>] [--kind claude-code|cursor]
7
+ * [--tier advise|propose|auto] [--feed-url <url>] [--dry-run]
8
+ * strongkeep-init --help
9
+ *
10
+ * Token: read from STRONGKEEP_TOKEN or the local login state (~/.strongkeep/token, written by
11
+ * strongkeep-login). NEVER accepted on argv (would leak into shell history) and NEVER printed.
12
+ *
13
+ * Design: arg-parse + orchestration + exit only. All work is in ../init.ts (runInit).
14
+ */
15
+ import type { AutonomyTier } from '@strongkeep/core';
16
+ import { type HarnessKind } from '../init.js';
17
+ export interface InitCliOptions {
18
+ harnessRoot: string;
19
+ kind: HarnessKind;
20
+ tier: AutonomyTier;
21
+ feedUrl?: string;
22
+ dryRun: boolean;
23
+ showHelp: boolean;
24
+ error?: string;
25
+ /** Phase 3 (additive): health-check ingest URL written to local config. */
26
+ ingestUrl?: string;
27
+ /** Phase 3 (additive): self-reported staff label written to local config. */
28
+ staffLabel?: string;
29
+ /** Phase 3 (additive): skip the auto first health-check at the end of init. */
30
+ noHealthcheck: boolean;
31
+ /** Beta 2-command flow: skip the auto first feed poll at the end of init. */
32
+ noPoll: boolean;
33
+ }
34
+ /**
35
+ * Parse argv → options. Total; never throws. Token is NOT an argv field (read from env/login state).
36
+ */
37
+ export declare function parseInitArgs(argv: string[], cwd?: string): InitCliOptions;