@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
package/dist/fires.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Guardrail-fire log — the activity-lite source the Phase 3 health check reads.
3
+ *
4
+ * Phase 2 shipped the applied-actions undo log but nothing recorded guardrail FIRES. This
5
+ * module is that record: one append-only NDJSON line per fire, carrying ONLY
6
+ * { firedAt, ruleId } — never the matched command, never any content. The health check
7
+ * counts these per rule since the last report (@strongkeep/healthcheck activity-lite).
8
+ *
9
+ * SECURITY: the matched text / command is deliberately NOT written. A fire says "rule X
10
+ * triggered at time T" and nothing more — enough for a count, insufficient to leak what
11
+ * the user was doing. Append-only; a corrupt line is a reader's problem to skip, never a
12
+ * writer's to repair.
13
+ *
14
+ * ponytail: single NDJSON file, append = atomicity unit — mirrors the undo log's design.
15
+ */
16
+ import { appendFile, mkdir } from 'node:fs/promises';
17
+ import { dirname, join } from 'node:path';
18
+ /** Basename of the fire log inside `.claude/strongkeep`. */
19
+ export const FIRES_FILE = 'fires.ndjson';
20
+ /**
21
+ * Append a fire to the log at `<strongkeepDir>/fires.ndjson`. Best-effort and non-throwing
22
+ * on a write failure (a guardrail must never crash the agent's tool call just because its
23
+ * telemetry line could not be written) — but it DOES create the directory if missing.
24
+ */
25
+ export async function recordFire(strongkeepDir, ruleId, at = new Date()) {
26
+ const record = { firedAt: at.toISOString(), ruleId };
27
+ const path = join(strongkeepDir, FIRES_FILE);
28
+ try {
29
+ await mkdir(dirname(path), { recursive: true });
30
+ await appendFile(path, JSON.stringify(record) + '\n', 'utf8');
31
+ }
32
+ catch {
33
+ // Telemetry must not break the tool call — drop the line rather than throw.
34
+ }
35
+ }
36
+ //# sourceMappingURL=fires.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fires.js","sourceRoot":"","sources":["../src/fires.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,4DAA4D;AAC5D,MAAM,CAAC,MAAM,UAAU,GAAG,cAAc,CAAC;AAQzC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,aAAqB,EAAE,MAAc,EAAE,KAAW,IAAI,IAAI,EAAE;IAC3F,MAAM,MAAM,GAAe,EAAE,OAAO,EAAE,EAAE,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,CAAC;IACjE,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;IAC7C,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,MAAM,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,4EAA4E;IAC9E,CAAC;AACH,CAAC"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Guard-rule evaluation — the PreToolUse check that reads guard-rules.yaml, matches a
3
+ * candidate command, records any fire (fires.ndjson), and returns the rule's action.
4
+ *
5
+ * This closes the Phase 2 gap: init wrote the rules file + hook wiring, but nothing
6
+ * evaluated a command against the rules or recorded a fire. This module is that evaluator.
7
+ * It stays true to the D6 least-privilege contract — it reads config + the command string
8
+ * passed to it, records a metadata-only fire, and returns advise/warn/block. It NEVER
9
+ * executes anything and never writes the command anywhere.
10
+ *
11
+ * ponytail: guard-rules.yaml is a tiny fixed shape (id / match / action per rule). We hand-
12
+ * parse those three fields with a line scanner rather than adding a YAML dependency — the
13
+ * same zero-dep discipline as the rest of the repo. A malformed rule line is skipped.
14
+ */
15
+ /** A single guard rule, as parsed from guard-rules.yaml. */
16
+ export interface GuardRule {
17
+ id: string;
18
+ /** Regex-source string matched (case-insensitively) against the candidate command. */
19
+ match: string;
20
+ action: 'advise' | 'warn' | 'block';
21
+ /** Optional plain-language reason shown to the user when the rule fires (why it's guarded). */
22
+ reason?: string;
23
+ }
24
+ /** Outcome of evaluating a command: which rules fired and the strongest action to take. */
25
+ export interface GuardOutcome {
26
+ firedRuleIds: string[];
27
+ /** Strongest action among fired rules; 'allow' when nothing fired. */
28
+ action: 'allow' | 'advise' | 'warn' | 'block';
29
+ /** The full rules that fired, in file order (used to build the user-facing message). */
30
+ fired: GuardRule[];
31
+ }
32
+ /**
33
+ * Evaluate `command` against the harness's guard rules. Records a fire (metadata only) for
34
+ * each matching rule, then returns the fired ids + the strongest action. Never throws.
35
+ * @param strongkeepDir absolute `.claude/strongkeep` dir (rules + fire log live here).
36
+ */
37
+ export declare function evaluateCommand(strongkeepDir: string, command: string, at?: Date): Promise<GuardOutcome>;
38
+ /** Parse the fixed id/match/action shape of guard-rules.yaml. Hand-rolled, no YAML dep. */
39
+ export declare function loadRules(path: string): Promise<GuardRule[]>;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Guard-rule evaluation — the PreToolUse check that reads guard-rules.yaml, matches a
3
+ * candidate command, records any fire (fires.ndjson), and returns the rule's action.
4
+ *
5
+ * This closes the Phase 2 gap: init wrote the rules file + hook wiring, but nothing
6
+ * evaluated a command against the rules or recorded a fire. This module is that evaluator.
7
+ * It stays true to the D6 least-privilege contract — it reads config + the command string
8
+ * passed to it, records a metadata-only fire, and returns advise/warn/block. It NEVER
9
+ * executes anything and never writes the command anywhere.
10
+ *
11
+ * ponytail: guard-rules.yaml is a tiny fixed shape (id / match / action per rule). We hand-
12
+ * parse those three fields with a line scanner rather than adding a YAML dependency — the
13
+ * same zero-dep discipline as the rest of the repo. A malformed rule line is skipped.
14
+ */
15
+ import { readFile } from 'node:fs/promises';
16
+ import { join } from 'node:path';
17
+ import { recordFire } from './fires.js';
18
+ const ACTION_RANK = { allow: 0, advise: 1, warn: 2, block: 3 };
19
+ /**
20
+ * Evaluate `command` against the harness's guard rules. Records a fire (metadata only) for
21
+ * each matching rule, then returns the fired ids + the strongest action. Never throws.
22
+ * @param strongkeepDir absolute `.claude/strongkeep` dir (rules + fire log live here).
23
+ */
24
+ export async function evaluateCommand(strongkeepDir, command, at = new Date()) {
25
+ const rules = await loadRules(join(strongkeepDir, 'guard-rules.yaml'));
26
+ const firedRuleIds = [];
27
+ const fired = [];
28
+ let action = 'allow';
29
+ for (const rule of rules) {
30
+ if (matches(rule.match, command)) {
31
+ firedRuleIds.push(rule.id);
32
+ fired.push(rule);
33
+ if (ACTION_RANK[rule.action] > ACTION_RANK[action])
34
+ action = rule.action;
35
+ await recordFire(strongkeepDir, rule.id, at); // metadata only — never `command`
36
+ }
37
+ }
38
+ return { firedRuleIds, action, fired };
39
+ }
40
+ /** Parse the fixed id/match/action shape of guard-rules.yaml. Hand-rolled, no YAML dep. */
41
+ export async function loadRules(path) {
42
+ let text;
43
+ try {
44
+ text = await readFile(path, 'utf8');
45
+ }
46
+ catch {
47
+ return [];
48
+ }
49
+ const rules = [];
50
+ let cur = {};
51
+ const flush = () => {
52
+ if (cur.id && cur.match !== undefined && (cur.action === 'advise' || cur.action === 'warn' || cur.action === 'block')) {
53
+ rules.push({ id: cur.id, match: cur.match, action: cur.action, ...(cur.reason ? { reason: cur.reason } : {}) });
54
+ }
55
+ cur = {};
56
+ };
57
+ for (const raw of text.split('\n')) {
58
+ const line = raw.trim();
59
+ if (line.startsWith('- id:')) {
60
+ flush();
61
+ cur.id = unquote(line.slice('- id:'.length).trim());
62
+ }
63
+ else if (line.startsWith('id:')) {
64
+ flush();
65
+ cur.id = unquote(line.slice('id:'.length).trim());
66
+ }
67
+ else if (line.startsWith('match:')) {
68
+ cur.match = unquote(line.slice('match:'.length).trim());
69
+ }
70
+ else if (line.startsWith('action:')) {
71
+ const a = unquote(line.slice('action:'.length).trim());
72
+ if (a === 'advise' || a === 'warn' || a === 'block')
73
+ cur.action = a;
74
+ }
75
+ else if (line.startsWith('reason:')) {
76
+ cur.reason = unquote(line.slice('reason:'.length).trim());
77
+ }
78
+ }
79
+ flush();
80
+ return rules;
81
+ }
82
+ /** Case-insensitive regex match; a bad regex source is treated as a literal substring. */
83
+ function matches(source, command) {
84
+ try {
85
+ return new RegExp(source, 'i').test(command);
86
+ }
87
+ catch {
88
+ return command.toLowerCase().includes(source.toLowerCase());
89
+ }
90
+ }
91
+ function unquote(s) {
92
+ return s.replace(/^["']|["']$/g, '');
93
+ }
94
+ //# sourceMappingURL=guard-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard-check.js","sourceRoot":"","sources":["../src/guard-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAqBxC,MAAM,WAAW,GAA2C,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;AAEvG;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,aAAqB,EAAE,OAAe,EAAE,KAAW,IAAI,IAAI,EAAE;IACjG,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,aAAa,EAAE,kBAAkB,CAAC,CAAC,CAAC;IACvE,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,IAAI,MAAM,GAA2B,OAAO,CAAC;IAE7C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;YACjC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC3B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjB,IAAI,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,WAAW,CAAC,MAAM,CAAC;gBAAE,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;YACzE,MAAM,UAAU,CAAC,aAAa,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,kCAAkC;QAClF,CAAC;IACH,CAAC;IACD,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AACzC,CAAC;AAED,2FAA2F;AAC3F,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY;IAC1C,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,IAAI,GAAG,GAAuB,EAAE,CAAC;IACjC,MAAM,KAAK,GAAG,GAAG,EAAE;QACjB,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC,MAAM,KAAK,OAAO,CAAC,EAAE,CAAC;YACtH,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAClH,CAAC;QACD,GAAG,GAAG,EAAE,CAAC;IACX,CAAC,CAAC;IACF,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QACxB,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC7B,KAAK,EAAE,CAAC;YACR,GAAG,CAAC,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACtD,CAAC;aAAM,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YAClC,KAAK,EAAE,CAAC;YACR,GAAG,CAAC,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACpD,CAAC;aAAM,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrC,GAAG,CAAC,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC1D,CAAC;aAAM,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACtC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YACvD,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,OAAO;gBAAE,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;QACtE,CAAC;aAAM,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACtC,GAAG,CAAC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC5D,CAAC;IACH,CAAC;IACD,KAAK,EAAE,CAAC;IACR,OAAO,KAAK,CAAC;AACf,CAAC;AAED,0FAA0F;AAC1F,SAAS,OAAO,CAAC,MAAc,EAAE,OAAe;IAC9C,IAAI,CAAC;QACH,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,CAAS;IACxB,OAAO,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;AACvC,CAAC"}
package/dist/init.d.ts ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * strongkeep-init core — writes the config pack into the subscriber's OWN harness (D6).
3
+ * This is "consuming the feed" for v1: run init once, the harness consumes the feed forever.
4
+ *
5
+ * What init writes into the harness (Journey B step 1):
6
+ * (a) Guardrail HOOKS into .claude/settings.json (PreToolUse/PostToolUse) — auditable text
7
+ * that, on each agent tool call, invokes the watchdog poll+pipeline against the cached feed.
8
+ * (b) A RULES file (.claude/strongkeep/guard-rules.yaml) the hooks read.
9
+ * (c) A /strongkeep-status SKILL (SKILL.md) so the human can query threats / applied changes.
10
+ * (d) MCP config wiring the read-only strongkeep-mcp server for live feed queries.
11
+ * (e) Local CONFIG: feed URL + per-tenant token + pinned public key + autonomy tier + cacheDir
12
+ * — plus a scoped .gitignore so the token file cannot be committed by accident.
13
+ *
14
+ * SECURITY:
15
+ * - init is IDEMPOTENT and NON-DESTRUCTIVE: it merges into existing config, backing up any
16
+ * file it edits (so init itself is revertible), and never clobbers unrelated keys/hooks.
17
+ * - The per-tenant token is written to the local, gitignored config file ONLY — never to a
18
+ * committed file, never echoed into the returned summary/notes, never printed.
19
+ * - The pinned public key is bundled with the watchdog (assets/), not fetched — trust anchor.
20
+ * - init writes ONLY inside the chosen harness root. It performs no other side effects.
21
+ *
22
+ * The written hook text is the "thin stable mechanism"; it rarely changes. All fast-moving
23
+ * intelligence arrives as feed DATA, so init does not need re-running when threats update.
24
+ */
25
+ import type { AutonomyTier } from '@strongkeep/core';
26
+ /** Which harness to target. Determines file layout (.claude vs .cursor). */
27
+ export type HarnessKind = 'claude-code' | 'cursor';
28
+ export interface InitOptions {
29
+ /** Absolute harness root to write into. */
30
+ harnessRoot: string;
31
+ kind: HarnessKind;
32
+ /** Signed feed base URL. */
33
+ feedUrl: string;
34
+ /**
35
+ * Per-tenant token (from strongkeep-login). Written to local config only; never
36
+ * committed/logged. Optional: a public-read feed (e.g. the beta) needs no token.
37
+ */
38
+ token?: string;
39
+ /** Autonomy tier to set (SMB default = propose). */
40
+ tier: AutonomyTier;
41
+ /** If true, only report what WOULD be written (no writes). */
42
+ dryRun: boolean;
43
+ /** Override the bundled pinned public key (base64 SPKI). Used by tests + the fixture deploy kit. */
44
+ pinnedPublicKeyB64?: string;
45
+ /**
46
+ * Phase 3 (additive): per-tenant health-check ingest URL. Written to local config so
47
+ * `strongkeep-healthcheck --send` can auto-POST. Optional — omitted → email/paste only.
48
+ */
49
+ ingestUrl?: string;
50
+ /**
51
+ * Phase 3 (additive): the staff label this machine self-reports as in health checks.
52
+ * Optional — omitted → the health check derives username@hostname.
53
+ */
54
+ staffLabel?: string;
55
+ }
56
+ /** What init did (or would do, in dryRun) — for the CLI summary + tests. */
57
+ export interface InitResult {
58
+ /** Files created/edited, with a note per file. Notes NEVER contain the token. */
59
+ changes: Array<{
60
+ path: string;
61
+ action: 'created' | 'merged' | 'skipped';
62
+ note: string;
63
+ }>;
64
+ /** Backups written for any edited file (so init is revertible). */
65
+ backups: string[];
66
+ }
67
+ /**
68
+ * Parse a PINNED_PUBLIC_KEY.txt-style text into the pinned key SET: every non-comment,
69
+ * non-empty line is a base64 SPKI key (tolerant: trims, skips `#` comments + blank lines).
70
+ * Multiple lines support key rotation — a future ceremony just adds the new key line.
71
+ */
72
+ export declare function parsePinnedKeyLines(text: string): string[];
73
+ /**
74
+ * Write (or, in dryRun, plan) the config pack into the harness. Idempotent + non-destructive.
75
+ */
76
+ export declare function runInit(opts: InitOptions): Promise<InitResult>;
77
+ /** The shape of the local config file strongkeep-init writes (read by the CLIs). */
78
+ export interface LocalConfig {
79
+ feedUrl: string;
80
+ tier: AutonomyTier;
81
+ cacheDir: string;
82
+ /** Legacy single pinned key (base64 SPKI). Kept for configs written before the key-set change. */
83
+ pinnedPublicKey: string;
84
+ /** Canonical pinned key SET (base64 SPKI each). Absent in legacy configs → 1-element set. */
85
+ pinnedPublicKeys?: string[];
86
+ /** Per-tenant token. Absent when init ran without one (public-read feed). */
87
+ token?: string;
88
+ /** Phase 3 (additive): health-check ingest URL for `strongkeep-healthcheck --send`. */
89
+ ingestUrl?: string;
90
+ /** Phase 3 (additive): self-reported staff label for health checks. */
91
+ staffLabel?: string;
92
+ }
93
+ /** Load the local config for a harness (used by strongkeep-poll / -revert / -mcp). */
94
+ export declare function loadLocalConfig(harnessRoot: string, kind?: HarnessKind): Promise<LocalConfig>;
95
+ /**
96
+ * The pinned key set from a local config, as verify-ready keys. Backward compatible: a legacy
97
+ * config with only the single `pinnedPublicKey` string yields a 1-element set. Never fails
98
+ * open — a config with no key at all yields an EMPTY set, which every verify path rejects.
99
+ */
100
+ export declare function pinnedKeysFromConfig(config: LocalConfig): Array<{
101
+ keyId: string;
102
+ bytes: Uint8Array;
103
+ }>;
104
+ /** The strongkeep state dir for a harness kind (where undo log + cache live). */
105
+ export declare function strongkeepDir(kind?: HarnessKind): string;
package/dist/init.js ADDED
@@ -0,0 +1,283 @@
1
+ /**
2
+ * strongkeep-init core — writes the config pack into the subscriber's OWN harness (D6).
3
+ * This is "consuming the feed" for v1: run init once, the harness consumes the feed forever.
4
+ *
5
+ * What init writes into the harness (Journey B step 1):
6
+ * (a) Guardrail HOOKS into .claude/settings.json (PreToolUse/PostToolUse) — auditable text
7
+ * that, on each agent tool call, invokes the watchdog poll+pipeline against the cached feed.
8
+ * (b) A RULES file (.claude/strongkeep/guard-rules.yaml) the hooks read.
9
+ * (c) A /strongkeep-status SKILL (SKILL.md) so the human can query threats / applied changes.
10
+ * (d) MCP config wiring the read-only strongkeep-mcp server for live feed queries.
11
+ * (e) Local CONFIG: feed URL + per-tenant token + pinned public key + autonomy tier + cacheDir
12
+ * — plus a scoped .gitignore so the token file cannot be committed by accident.
13
+ *
14
+ * SECURITY:
15
+ * - init is IDEMPOTENT and NON-DESTRUCTIVE: it merges into existing config, backing up any
16
+ * file it edits (so init itself is revertible), and never clobbers unrelated keys/hooks.
17
+ * - The per-tenant token is written to the local, gitignored config file ONLY — never to a
18
+ * committed file, never echoed into the returned summary/notes, never printed.
19
+ * - The pinned public key is bundled with the watchdog (assets/), not fetched — trust anchor.
20
+ * - init writes ONLY inside the chosen harness root. It performs no other side effects.
21
+ *
22
+ * The written hook text is the "thin stable mechanism"; it rarely changes. All fast-moving
23
+ * intelligence arrives as feed DATA, so init does not need re-running when threats update.
24
+ */
25
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
26
+ import { dirname, join } from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
28
+ import { readIfExists, backupFile } from './appliers/fs-guard.js';
29
+ /**
30
+ * Resolve a watchdog CLI bin to an absolute path (relative to this compiled module, i.e.
31
+ * dist/cli/<bin>). init bakes the resolved path into the harness hooks at write time — no npx,
32
+ * no PATH lookup, no "could not determine executable to run" (which fails OPEN: Claude Code
33
+ * treats a hook-execution error as non-blocking, so a broken hook = no protection).
34
+ * ponytail: for the ephemeral `npx @strongkeep/watchdog init` model the package can live in a
35
+ * temp npx cache that may be cleaned, staling this path; the production hardening is to copy a
36
+ * self-contained guard script into the harness. For workspace / local / global installs it is stable.
37
+ */
38
+ function watchdogBinPath(binFile) {
39
+ return fileURLToPath(new URL(`./cli/${binFile}`, import.meta.url));
40
+ }
41
+ /** Same, wrapped as a shell `node "<path>"` command for a hook `command` string. */
42
+ function watchdogBinCommand(binFile) {
43
+ return `node ${JSON.stringify(watchdogBinPath(binFile))}`;
44
+ }
45
+ /** The auditable hook command (thin stable mechanism — polls the feed + runs the pipeline). */
46
+ const GUARD_HOOK_COMMAND = watchdogBinCommand('poll.js');
47
+ /**
48
+ * The PreToolUse guard-check command (Phase 3, additive): evaluates the candidate command
49
+ * against guard-rules.yaml, records metadata-only fires to fires.ndjson, and denies on block.
50
+ */
51
+ const GUARD_CHECK_COMMAND = watchdogBinCommand('guard-check.js');
52
+ /** Marker used to recognise our own hook entries on re-runs (idempotency). */
53
+ const HOOK_MARKER = 'strongkeep';
54
+ /** Starter guard rules — DATA read by the hooks; updated later via update_rule_file actions. */
55
+ const STARTER_RULES = `# StrongKeep guard rules (data — read by the strongkeep guard hooks, never executed)
56
+ # Updated via signed update_rule_file feed actions. Edit freely; this file is yours.
57
+ #
58
+ # 'block' denies the AGENT's tool call (PreToolUse exit 2). A user's manual bash
59
+ # (e.g. \`! rm -rf ...\` typed into the harness) is NOT a tool call, so it never reaches
60
+ # this guard — you keep full manual control; only the AGENT is stopped.
61
+ version: 1
62
+ rules:
63
+ # ponytail: covers the common rm -rf / -fr / split-flag forms; extend if you see novel spellings.
64
+ - id: gr-001-destructive-commands
65
+ match: "rm -rf|rm -fr|rm -r -f|rm -f -r"
66
+ action: block
67
+ reason: "Recursive force-delete (rm -rf) can permanently and irreversibly wipe files or whole directories. This is the AI-agent-wiped-my-home-directory class of incident."
68
+ - id: gr-002-skip-permissions
69
+ match: "dangerously-skip-permissions"
70
+ action: block
71
+ reason: "This flag turns off the agent's own permission prompts, removing the safety checks that stop unintended or malicious actions."
72
+ - id: gr-003-risky-history
73
+ match: "git reset --hard|DROP TABLE"
74
+ action: warn
75
+ reason: "Destructive to your git history or database (git reset --hard / DROP TABLE). Allowed, but flagged so you notice before data is lost."
76
+ `;
77
+ /** Layout per harness kind. */
78
+ function layout(kind) {
79
+ if (kind === 'claude-code') {
80
+ return {
81
+ settingsFile: '.claude/settings.json',
82
+ mcpFile: '.mcp.json',
83
+ strongkeepDir: '.claude/strongkeep',
84
+ skillFile: '.claude/skills/strongkeep-status/SKILL.md',
85
+ };
86
+ }
87
+ // cursor: MCP config only — no hooks/skills surface in v1 (noted in the change list).
88
+ return {
89
+ settingsFile: undefined,
90
+ mcpFile: '.cursor/mcp.json',
91
+ strongkeepDir: '.cursor/strongkeep',
92
+ skillFile: undefined,
93
+ };
94
+ }
95
+ /** Absolute path of a bundled asset (assets/ ships beside dist/ in the published package). */
96
+ function assetPath(rel) {
97
+ return join(dirname(fileURLToPath(import.meta.url)), '../assets', rel);
98
+ }
99
+ /**
100
+ * Parse a PINNED_PUBLIC_KEY.txt-style text into the pinned key SET: every non-comment,
101
+ * non-empty line is a base64 SPKI key (tolerant: trims, skips `#` comments + blank lines).
102
+ * Multiple lines support key rotation — a future ceremony just adds the new key line.
103
+ */
104
+ export function parsePinnedKeyLines(text) {
105
+ return text
106
+ .split('\n')
107
+ .map((l) => l.trim())
108
+ .filter((l) => l !== '' && !l.startsWith('#'));
109
+ }
110
+ /** Read the bundled pinned public key set (assets/PINNED_PUBLIC_KEY.txt): all key lines. */
111
+ async function bundledPinnedKeysB64() {
112
+ const keys = parsePinnedKeyLines(await readFile(assetPath('PINNED_PUBLIC_KEY.txt'), 'utf8'));
113
+ if (keys.length === 0)
114
+ throw new Error('bundled PINNED_PUBLIC_KEY.txt has no key line');
115
+ return keys;
116
+ }
117
+ /** Read the bundled /strongkeep-status skill body. */
118
+ async function bundledSkill() {
119
+ return readFile(assetPath('skill/SKILL.md'), 'utf8');
120
+ }
121
+ /** Read the bundled /strongkeep-healthcheck skill body (Phase 3), or null if not bundled. */
122
+ async function bundledHealthcheckSkill() {
123
+ try {
124
+ return await readFile(assetPath('healthcheck-skill/SKILL.md'), 'utf8');
125
+ }
126
+ catch {
127
+ return null;
128
+ }
129
+ }
130
+ /**
131
+ * Write (or, in dryRun, plan) the config pack into the harness. Idempotent + non-destructive.
132
+ */
133
+ export async function runInit(opts) {
134
+ const result = { changes: [], backups: [] };
135
+ const files = layout(opts.kind);
136
+ /** Write a file if its content differs; back up an existing file before editing it. */
137
+ const writeManaged = async (relPath, content, mergeNote) => {
138
+ const absPath = join(opts.harnessRoot, relPath);
139
+ const existing = await readIfExists(absPath);
140
+ const existingText = existing === undefined ? undefined : new TextDecoder().decode(existing);
141
+ if (existingText === content) {
142
+ result.changes.push({ path: relPath, action: 'skipped', note: 'already up to date' });
143
+ return;
144
+ }
145
+ if (!opts.dryRun) {
146
+ if (existing !== undefined)
147
+ result.backups.push(await backupFile(opts.harnessRoot, absPath));
148
+ await mkdir(dirname(absPath), { recursive: true });
149
+ await writeFile(absPath, content, 'utf8');
150
+ }
151
+ result.changes.push({
152
+ path: relPath,
153
+ action: existing === undefined ? 'created' : 'merged',
154
+ note: mergeNote,
155
+ });
156
+ };
157
+ // (a) Guard hooks — MERGED into existing settings, never clobbering unrelated keys/hooks.
158
+ if (files.settingsFile !== undefined) {
159
+ const absPath = join(opts.harnessRoot, files.settingsFile);
160
+ const existing = await readIfExists(absPath);
161
+ const settings = existing === undefined ? {} : JSON.parse(new TextDecoder().decode(existing));
162
+ const hooks = (settings['hooks'] ?? {});
163
+ settings['hooks'] = hooks;
164
+ let added = 0;
165
+ /** Add a command hook to a phase iff that exact command is not already present. */
166
+ const ensureHook = (phase, command) => {
167
+ const arr = Array.isArray(hooks[phase]) ? hooks[phase] : [];
168
+ hooks[phase] = arr;
169
+ // Compare the actual command field, not a JSON.stringify substring: the resolved
170
+ // `node "<abs path>"` command contains quotes that JSON-escaping would mangle, breaking
171
+ // idempotency. Match the exact command string on any nested hook entry.
172
+ const present = arr.some((h) => {
173
+ const nested = h?.hooks;
174
+ return Array.isArray(nested) && nested.some((x) => x?.command === command);
175
+ });
176
+ if (!present) {
177
+ arr.push({ matcher: '*', hooks: [{ type: 'command', command }] });
178
+ added++;
179
+ }
180
+ };
181
+ // Poll on both phases (feed apply). Guard-check on PreToolUse (rule fires + block).
182
+ ensureHook('PreToolUse', GUARD_HOOK_COMMAND);
183
+ ensureHook('PostToolUse', GUARD_HOOK_COMMAND);
184
+ ensureHook('PreToolUse', GUARD_CHECK_COMMAND);
185
+ await writeManaged(files.settingsFile, JSON.stringify(settings, null, 2) + '\n', added > 0 ? `merged ${added} strongkeep guard hook(s); existing hooks + settings preserved` : 'guard hooks present');
186
+ }
187
+ else {
188
+ result.changes.push({ path: '(hooks)', action: 'skipped', note: `${opts.kind} has no hooks surface in v1` });
189
+ }
190
+ // (b) Rules file.
191
+ await writeManaged(join(files.strongkeepDir, 'guard-rules.yaml'), STARTER_RULES, 'starter guard rules written');
192
+ // (c) Skills: /strongkeep-status (Phase 2) + /strongkeep-healthcheck (Phase 3, additive).
193
+ if (files.skillFile !== undefined) {
194
+ await writeManaged(files.skillFile, await bundledSkill(), '/strongkeep-status skill installed');
195
+ const healthcheckSkill = await bundledHealthcheckSkill();
196
+ if (healthcheckSkill !== null) {
197
+ await writeManaged('.claude/skills/strongkeep-healthcheck/SKILL.md', healthcheckSkill, '/strongkeep-healthcheck skill installed');
198
+ }
199
+ }
200
+ // (d) MCP wiring — ADDITIVE merge; pre-existing servers survive untouched.
201
+ {
202
+ const absPath = join(opts.harnessRoot, files.mcpFile);
203
+ const existing = await readIfExists(absPath);
204
+ const mcp = existing === undefined ? {} : JSON.parse(new TextDecoder().decode(existing));
205
+ const servers = (mcp['mcpServers'] ?? {});
206
+ mcp['mcpServers'] = servers;
207
+ if (servers['strongkeep'] === undefined) {
208
+ servers['strongkeep'] = { command: 'node', args: [watchdogBinPath('mcp.js')] };
209
+ }
210
+ await writeManaged(files.mcpFile, JSON.stringify(mcp, null, 2) + '\n', 'strongkeep-mcp wired in additively');
211
+ }
212
+ // (e) Local config: token + pinned key set + tier + cacheDir. Token lands HERE and nowhere else.
213
+ // A caller-supplied single key (tests / deploy-kit scripts) becomes a 1-element set.
214
+ const pinnedKeys = opts.pinnedPublicKeyB64 !== undefined ? [opts.pinnedPublicKeyB64] : await bundledPinnedKeysB64();
215
+ const cacheDir = join(files.strongkeepDir, 'cache');
216
+ const localConfig = {
217
+ feedUrl: opts.feedUrl,
218
+ tier: opts.tier,
219
+ cacheDir,
220
+ // Legacy single-key field kept so older readers (e.g. strongkeep-verify.sh, previously
221
+ // installed CLIs) still find a key; pinnedPublicKeys is the canonical set.
222
+ pinnedPublicKey: pinnedKeys[0],
223
+ pinnedPublicKeys: pinnedKeys,
224
+ // Token omitted entirely when not provided (public-read feed): no empty/placeholder value.
225
+ ...(opts.token !== undefined ? { token: opts.token } : {}),
226
+ // Phase 3 (additive): health-check transport + identity. Omitted when not provided.
227
+ ...(opts.ingestUrl !== undefined ? { ingestUrl: opts.ingestUrl } : {}),
228
+ ...(opts.staffLabel !== undefined ? { staffLabel: opts.staffLabel } : {}),
229
+ };
230
+ {
231
+ const relPath = join(files.strongkeepDir, 'config.local.json');
232
+ const absPath = join(opts.harnessRoot, relPath);
233
+ const content = JSON.stringify(localConfig, null, 2) + '\n';
234
+ const existing = await readIfExists(absPath);
235
+ const existingText = existing === undefined ? undefined : new TextDecoder().decode(existing);
236
+ if (existingText === content) {
237
+ result.changes.push({ path: relPath, action: 'skipped', note: 'already up to date' });
238
+ }
239
+ else {
240
+ if (!opts.dryRun) {
241
+ if (existing !== undefined)
242
+ result.backups.push(await backupFile(opts.harnessRoot, absPath));
243
+ await mkdir(dirname(absPath), { recursive: true });
244
+ await writeFile(absPath, content, { encoding: 'utf8', mode: 0o600 });
245
+ }
246
+ // Note deliberately does NOT echo any config value — the token must never leave this file.
247
+ result.changes.push({
248
+ path: relPath,
249
+ action: existing === undefined ? 'created' : 'merged',
250
+ note: opts.token !== undefined
251
+ ? 'local config written (feed URL, tier, cache dir, pinned key, token — token stays in this file only)'
252
+ : 'local config written (feed URL, tier, cache dir, pinned key — no token, public-read feed)',
253
+ });
254
+ }
255
+ }
256
+ // Scoped .gitignore: belt-and-braces so the token file + cache can never be committed.
257
+ await writeManaged(join(files.strongkeepDir, '.gitignore'), 'config.local.json\ncache/\nbackups/\nhook-packs/\n', 'scoped gitignore for token/cache');
258
+ return result;
259
+ }
260
+ /** Load the local config for a harness (used by strongkeep-poll / -revert / -mcp). */
261
+ export async function loadLocalConfig(harnessRoot, kind = 'claude-code') {
262
+ const files = layout(kind);
263
+ const path = join(harnessRoot, files.strongkeepDir, 'config.local.json');
264
+ return JSON.parse(await readFile(path, 'utf8'));
265
+ }
266
+ /**
267
+ * The pinned key set from a local config, as verify-ready keys. Backward compatible: a legacy
268
+ * config with only the single `pinnedPublicKey` string yields a 1-element set. Never fails
269
+ * open — a config with no key at all yields an EMPTY set, which every verify path rejects.
270
+ */
271
+ export function pinnedKeysFromConfig(config) {
272
+ const b64s = config.pinnedPublicKeys !== undefined && config.pinnedPublicKeys.length > 0
273
+ ? config.pinnedPublicKeys
274
+ : typeof config.pinnedPublicKey === 'string' && config.pinnedPublicKey !== ''
275
+ ? [config.pinnedPublicKey]
276
+ : [];
277
+ return b64s.map((b64, i) => ({ keyId: `pinned-${i + 1}`, bytes: new Uint8Array(Buffer.from(b64, 'base64')) }));
278
+ }
279
+ /** The strongkeep state dir for a harness kind (where undo log + cache live). */
280
+ export function strongkeepDir(kind = 'claude-code') {
281
+ return layout(kind).strongkeepDir;
282
+ }
283
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AA0ClE;;;;;;;;GAQG;AACH,SAAS,eAAe,CAAC,OAAe;IACtC,OAAO,aAAa,CAAC,IAAI,GAAG,CAAC,SAAS,OAAO,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACrE,CAAC;AACD,oFAAoF;AACpF,SAAS,kBAAkB,CAAC,OAAe;IACzC,OAAO,QAAQ,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;AAC5D,CAAC;AAED,+FAA+F;AAC/F,MAAM,kBAAkB,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;AAEzD;;;GAGG;AACH,MAAM,mBAAmB,GAAG,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;AAEjE,8EAA8E;AAC9E,MAAM,WAAW,GAAG,YAAY,CAAC;AAEjC,gGAAgG;AAChG,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;CAqBrB,CAAC;AAEF,+BAA+B;AAC/B,SAAS,MAAM,CAAC,IAAiB;IAM/B,IAAI,IAAI,KAAK,aAAa,EAAE,CAAC;QAC3B,OAAO;YACL,YAAY,EAAE,uBAAuB;YACrC,OAAO,EAAE,WAAW;YACpB,aAAa,EAAE,oBAAoB;YACnC,SAAS,EAAE,2CAA2C;SACvD,CAAC;IACJ,CAAC;IACD,sFAAsF;IACtF,OAAO;QACL,YAAY,EAAE,SAAS;QACvB,OAAO,EAAE,kBAAkB;QAC3B,aAAa,EAAE,oBAAoB;QACnC,SAAS,EAAE,SAAS;KACrB,CAAC;AACJ,CAAC;AAED,8FAA8F;AAC9F,SAAS,SAAS,CAAC,GAAW;IAC5B,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,CAAC;AACzE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,OAAO,IAAI;SACR,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AACnD,CAAC;AAED,4FAA4F;AAC5F,KAAK,UAAU,oBAAoB;IACjC,MAAM,IAAI,GAAG,mBAAmB,CAAC,MAAM,QAAQ,CAAC,SAAS,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAC7F,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACxF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sDAAsD;AACtD,KAAK,UAAU,YAAY;IACzB,OAAO,QAAQ,CAAC,SAAS,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC,CAAC;AACvD,CAAC;AAED,6FAA6F;AAC7F,KAAK,UAAU,uBAAuB;IACpC,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,SAAS,CAAC,4BAA4B,CAAC,EAAE,MAAM,CAAC,CAAC;IACzE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAiB;IAC7C,MAAM,MAAM,GAAe,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IACxD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEhC,uFAAuF;IACvF,MAAM,YAAY,GAAG,KAAK,EAAE,OAAe,EAAE,OAAe,EAAE,SAAiB,EAAiB,EAAE;QAChG,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAChD,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC;QAC7C,MAAM,YAAY,GAAG,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC7F,IAAI,YAAY,KAAK,OAAO,EAAE,CAAC;YAC7B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,oBAAoB,EAAE,CAAC,CAAC;YACtF,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,IAAI,QAAQ,KAAK,SAAS;gBAAE,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;YAC7F,MAAM,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACnD,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QAC5C,CAAC;QACD,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;YAClB,IAAI,EAAE,OAAO;YACb,MAAM,EAAE,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ;YACrD,IAAI,EAAE,SAAS;SAChB,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,0FAA0F;IAC1F,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;QAC3D,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC;QAC7C,MAAM,QAAQ,GACZ,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAE,IAAI,CAAC,KAAK,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAA6B,CAAC;QAC5G,MAAM,KAAK,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,CAA4B,CAAC;QACnE,QAAQ,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC;QAC1B,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,mFAAmF;QACnF,MAAM,UAAU,GAAG,CAAC,KAAa,EAAE,OAAe,EAAQ,EAAE;YAC1D,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAE,KAAK,CAAC,KAAK,CAAe,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3E,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC;YACnB,iFAAiF;YACjF,wFAAwF;YACxF,wEAAwE;YACxE,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;gBAC7B,MAAM,MAAM,GAAI,CAAyB,EAAE,KAAK,CAAC;gBACjD,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAE,CAA2B,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC;YACxG,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;gBAClE,KAAK,EAAE,CAAC;YACV,CAAC;QACH,CAAC,CAAC;QACF,oFAAoF;QACpF,UAAU,CAAC,YAAY,EAAE,kBAAkB,CAAC,CAAC;QAC7C,UAAU,CAAC,aAAa,EAAE,kBAAkB,CAAC,CAAC;QAC9C,UAAU,CAAC,YAAY,EAAE,mBAAmB,CAAC,CAAC;QAC9C,MAAM,YAAY,CAChB,KAAK,CAAC,YAAY,EAClB,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EACxC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,KAAK,gEAAgE,CAAC,CAAC,CAAC,qBAAqB,CACpH,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,6BAA6B,EAAE,CAAC,CAAC;IAC/G,CAAC;IAED,kBAAkB;IAClB,MAAM,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,kBAAkB,CAAC,EAAE,aAAa,EAAE,6BAA6B,CAAC,CAAC;IAEhH,0FAA0F;IAC1F,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QAClC,MAAM,YAAY,CAAC,KAAK,CAAC,SAAS,EAAE,MAAM,YAAY,EAAE,EAAE,oCAAoC,CAAC,CAAC;QAChG,MAAM,gBAAgB,GAAG,MAAM,uBAAuB,EAAE,CAAC;QACzD,IAAI,gBAAgB,KAAK,IAAI,EAAE,CAAC;YAC9B,MAAM,YAAY,CAChB,gDAAgD,EAChD,gBAAgB,EAChB,yCAAyC,CAC1C,CAAC;QACJ,CAAC;IACH,CAAC;IAED,2EAA2E;IAC3E,CAAC;QACC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QACtD,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC;QAC7C,MAAM,GAAG,GACP,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAE,IAAI,CAAC,KAAK,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAA6B,CAAC;QAC5G,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE,CAA4B,CAAC;QACrE,GAAG,CAAC,YAAY,CAAC,GAAG,OAAO,CAAC;QAC5B,IAAI,OAAO,CAAC,YAAY,CAAC,KAAK,SAAS,EAAE,CAAC;YACxC,OAAO,CAAC,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;QACjF,CAAC;QACD,MAAM,YAAY,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,oCAAoC,CAAC,CAAC;IAC/G,CAAC;IAED,iGAAiG;IACjG,qFAAqF;IACrF,MAAM,UAAU,GAAG,IAAI,CAAC,kBAAkB,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,MAAM,oBAAoB,EAAE,CAAC;IACpH,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;IACpD,MAAM,WAAW,GAAG;QAClB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,QAAQ;QACR,uFAAuF;QACvF,2EAA2E;QAC3E,eAAe,EAAE,UAAU,CAAC,CAAC,CAAE;QAC/B,gBAAgB,EAAE,UAAU;QAC5B,2FAA2F;QAC3F,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1D,oFAAoF;QACpF,GAAG,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtE,GAAG,CAAC,IAAI,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1E,CAAC;IACF,CAAC;QACC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,mBAAmB,CAAC,CAAC;QAC/D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAChD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;QAC5D,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC;QAC7C,MAAM,YAAY,GAAG,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC7F,IAAI,YAAY,KAAK,OAAO,EAAE,CAAC;YAC7B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,oBAAoB,EAAE,CAAC,CAAC;QACxF,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACjB,IAAI,QAAQ,KAAK,SAAS;oBAAE,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,UAAU,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;gBAC7F,MAAM,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBACnD,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACvE,CAAC;YACD,2FAA2F;YAC3F,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;gBAClB,IAAI,EAAE,OAAO;gBACb,MAAM,EAAE,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ;gBACrD,IAAI,EACF,IAAI,CAAC,KAAK,KAAK,SAAS;oBACtB,CAAC,CAAC,qGAAqG;oBACvG,CAAC,CAAC,2FAA2F;aAClG,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,uFAAuF;IACvF,MAAM,YAAY,CAChB,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,YAAY,CAAC,EACvC,oDAAoD,EACpD,kCAAkC,CACnC,CAAC;IAEF,OAAO,MAAM,CAAC;AAChB,CAAC;AAmBD,sFAAsF;AACtF,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,WAAmB,EAAE,OAAoB,aAAa;IAC1F,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,aAAa,EAAE,mBAAmB,CAAC,CAAC;IACzE,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAgB,CAAC;AACjE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAmB;IACtD,MAAM,IAAI,GACR,MAAM,CAAC,gBAAgB,KAAK,SAAS,IAAI,MAAM,CAAC,gBAAgB,CAAC,MAAM,GAAG,CAAC;QACzE,CAAC,CAAC,MAAM,CAAC,gBAAgB;QACzB,CAAC,CAAC,OAAO,MAAM,CAAC,eAAe,KAAK,QAAQ,IAAI,MAAM,CAAC,eAAe,KAAK,EAAE;YAC3E,CAAC,CAAC,CAAC,MAAM,CAAC,eAAe,CAAC;YAC1B,CAAC,CAAC,EAAE,CAAC;IACX,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACjH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,aAAa,CAAC,OAAoB,aAAa;IAC7D,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,aAAa,CAAC;AACpC,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Apply pipeline — the orchestrator that ties verified entries → autonomy → applier → undo.
3
+ * This is the runtime the harness hook invokes on each poll (steady state, Journey B step 2).
4
+ *
5
+ * For each VERIFIED, parsed entry (from feed-client.pollFeed), for each of its actions:
6
+ * 1. decideDisposition(action, consumerTier, entryTier) (autonomy.ts)
7
+ * 2. report → surface to the human, no write.
8
+ * stage → applyAction(dryRun:true) → write a 'staged' UndoRecord + a pending diff file
9
+ * (the PR-style pending change a human approves via {@link applyStaged}).
10
+ * apply → applyAction(dryRun:false) → append 'applied' UndoRecord.
11
+ * 3. All apply/stage outcomes append to the audit/undo log; a refused action is counted and
12
+ * reported, never silently dropped.
13
+ *
14
+ * The pipeline NEVER executes an entry body and NEVER runs feed-supplied text; it only routes
15
+ * closed-vocabulary actions to allowlisted appliers. Crash-safety (D3): every applier writes
16
+ * its byte-exact out-of-log BACKUP before touching a target file, so prior state is
17
+ * recoverable on disk even if the process dies between the write and the log append.
18
+ */
19
+ import type { ParsedEntry, AutonomyTier } from '@strongkeep/core';
20
+ import type { ApplyContext } from './appliers/applier.js';
21
+ export interface PipelineConfig {
22
+ consumerTier: AutonomyTier;
23
+ applyCtx: ApplyContext;
24
+ /** Directory holding the undo/audit log (+ pending/ staged diffs). */
25
+ logDir: string;
26
+ }
27
+ /** Summary of one pipeline run for the UI / dashboard heartbeat. */
28
+ export interface PipelineRunSummary {
29
+ reported: number;
30
+ staged: number;
31
+ applied: number;
32
+ refused: number;
33
+ /** Plain-language lines for the human (report advisories, refusal reasons, apply summaries). */
34
+ messages: string[];
35
+ }
36
+ /** Where staged PR-style diffs are written, under the log dir. */
37
+ export declare const PENDING_DIR = "pending";
38
+ /**
39
+ * Process all verified entries through the autonomy → applier → undo pipeline.
40
+ * Aggregates per-action failures (refusals) instead of aborting the whole run.
41
+ */
42
+ export declare function runPipeline(entries: ParsedEntry[], config: PipelineConfig): Promise<PipelineRunSummary>;
43
+ /**
44
+ * Human-approval path for a staged change: apply a previously-staged record for real and log
45
+ * the transition under the SAME record id (staged → applied → revertible). This is the
46
+ * "human applies the PR-style diff" step of the propose tier.
47
+ */
48
+ export declare function applyStaged(recordId: string, config: PipelineConfig): Promise<{
49
+ summary: string;
50
+ }>;