@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.
- package/assets/PINNED_PUBLIC_KEY.txt +13 -0
- package/assets/healthcheck-skill/SKILL.md +28 -0
- package/assets/skill/SKILL.md +30 -0
- package/dist/appliers/add-hook.d.ts +19 -0
- package/dist/appliers/add-hook.js +81 -0
- package/dist/appliers/add-hook.js.map +1 -0
- package/dist/appliers/applier.d.ts +86 -0
- package/dist/appliers/applier.js +60 -0
- package/dist/appliers/applier.js.map +1 -0
- package/dist/appliers/block-domain.d.ts +20 -0
- package/dist/appliers/block-domain.js +77 -0
- package/dist/appliers/block-domain.js.map +1 -0
- package/dist/appliers/fs-guard.d.ts +36 -0
- package/dist/appliers/fs-guard.js +109 -0
- package/dist/appliers/fs-guard.js.map +1 -0
- package/dist/appliers/min-version.d.ts +15 -0
- package/dist/appliers/min-version.js +47 -0
- package/dist/appliers/min-version.js.map +1 -0
- package/dist/appliers/pin-mcp.d.ts +17 -0
- package/dist/appliers/pin-mcp.js +99 -0
- package/dist/appliers/pin-mcp.js.map +1 -0
- package/dist/appliers/set-config.d.ts +12 -0
- package/dist/appliers/set-config.js +67 -0
- package/dist/appliers/set-config.js.map +1 -0
- package/dist/appliers/update-rule-file.d.ts +15 -0
- package/dist/appliers/update-rule-file.js +69 -0
- package/dist/appliers/update-rule-file.js.map +1 -0
- package/dist/autonomy.d.ts +24 -0
- package/dist/autonomy.js +35 -0
- package/dist/autonomy.js.map +1 -0
- package/dist/cli/guard-check.d.ts +16 -0
- package/dist/cli/guard-check.js +108 -0
- package/dist/cli/guard-check.js.map +1 -0
- package/dist/cli/init.d.ts +37 -0
- package/dist/cli/init.js +226 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/login.d.ts +32 -0
- package/dist/cli/login.js +144 -0
- package/dist/cli/login.js.map +1 -0
- package/dist/cli/mcp.d.ts +23 -0
- package/dist/cli/mcp.js +84 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/cli/poll.d.ts +20 -0
- package/dist/cli/poll.js +111 -0
- package/dist/cli/poll.js.map +1 -0
- package/dist/cli/revert.d.ts +31 -0
- package/dist/cli/revert.js +129 -0
- package/dist/cli/revert.js.map +1 -0
- package/dist/feed-client.d.ts +110 -0
- package/dist/feed-client.js +178 -0
- package/dist/feed-client.js.map +1 -0
- package/dist/fires.d.ts +28 -0
- package/dist/fires.js +36 -0
- package/dist/fires.js.map +1 -0
- package/dist/guard-check.d.ts +39 -0
- package/dist/guard-check.js +94 -0
- package/dist/guard-check.js.map +1 -0
- package/dist/init.d.ts +105 -0
- package/dist/init.js +283 -0
- package/dist/init.js.map +1 -0
- package/dist/pipeline.d.ts +50 -0
- package/dist/pipeline.js +110 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/undo.d.ts +98 -0
- package/dist/undo.js +67 -0
- package/dist/undo.js.map +1 -0
- package/package.json +40 -0
package/dist/cli/poll.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* strongkeep-poll CLI — the invocable the init-written guard hooks call: poll the signed feed
|
|
4
|
+
* (verify → parse, D2 fail-safe) and run the apply pipeline under the configured autonomy tier.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* strongkeep-poll [--harness <dir>]
|
|
8
|
+
* strongkeep-poll --help
|
|
9
|
+
*
|
|
10
|
+
* This is the "thin stable mechanism" runtime (D6): reading it end-to-end shows everything the
|
|
11
|
+
* watchdog can do to this machine — verify, then route closed-vocabulary actions to the
|
|
12
|
+
* allowlisted appliers, with undo records. No shell, no body execution, no telemetry.
|
|
13
|
+
*/
|
|
14
|
+
import process from 'node:process';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { realpathSync } from 'node:fs';
|
|
17
|
+
import { readFile } from 'node:fs/promises';
|
|
18
|
+
import { join, resolve } from 'node:path';
|
|
19
|
+
import { pollFeed, lastGoodDir, DEFAULT_MAX_STALENESS_MS } from '../feed-client.js';
|
|
20
|
+
import { runPipeline } from '../pipeline.js';
|
|
21
|
+
import { loadLocalConfig, pinnedKeysFromConfig, strongkeepDir } from '../init.js';
|
|
22
|
+
const HELP_TEXT = `strongkeep-poll — verify the signed feed and run the apply pipeline (tier-gated)
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
strongkeep-poll [--harness <dir>]
|
|
26
|
+
|
|
27
|
+
Reads local config written by strongkeep-init. Fail-safe: if the feed is down or does not
|
|
28
|
+
verify, the last-good verified snapshot stays in force and nothing new is applied.`;
|
|
29
|
+
/** Parse argv → options. Total; never throws. */
|
|
30
|
+
export function parsePollArgs(argv, cwd = process.cwd()) {
|
|
31
|
+
const opts = { harnessRoot: cwd, showHelp: false };
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
const arg = argv[i];
|
|
34
|
+
if (arg === '--help' || arg === '-h')
|
|
35
|
+
opts.showHelp = true;
|
|
36
|
+
else if (arg === '--harness') {
|
|
37
|
+
const val = argv[++i];
|
|
38
|
+
if (val === undefined)
|
|
39
|
+
return { ...opts, showHelp: true, error: '--harness requires a dir' };
|
|
40
|
+
opts.harnessRoot = val;
|
|
41
|
+
}
|
|
42
|
+
else
|
|
43
|
+
return { ...opts, showHelp: true, error: `unknown argument "${arg}"` };
|
|
44
|
+
}
|
|
45
|
+
return opts;
|
|
46
|
+
}
|
|
47
|
+
async function main() {
|
|
48
|
+
const opts = parsePollArgs(process.argv.slice(2));
|
|
49
|
+
if (opts.error)
|
|
50
|
+
process.stderr.write(`strongkeep-poll: ${opts.error}\n\n`);
|
|
51
|
+
if (opts.showHelp) {
|
|
52
|
+
process.stdout.write(HELP_TEXT + '\n');
|
|
53
|
+
process.exit(opts.error ? 2 : 0);
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const harnessRoot = resolve(opts.harnessRoot);
|
|
57
|
+
const config = await loadLocalConfig(harnessRoot);
|
|
58
|
+
const poll = await pollFeed({
|
|
59
|
+
feedUrl: config.feedUrl,
|
|
60
|
+
token: config.token,
|
|
61
|
+
// The pinned key SET (legacy single-key configs yield a 1-element set).
|
|
62
|
+
pinnedKey: pinnedKeysFromConfig(config),
|
|
63
|
+
cacheDir: join(harnessRoot, config.cacheDir),
|
|
64
|
+
maxStalenessMs: DEFAULT_MAX_STALENESS_MS,
|
|
65
|
+
});
|
|
66
|
+
const summary = await runPipeline(poll.entries, {
|
|
67
|
+
consumerTier: config.tier,
|
|
68
|
+
applyCtx: {
|
|
69
|
+
harnessRoot,
|
|
70
|
+
dryRun: false,
|
|
71
|
+
// Payloads resolve from the verified last-good snapshot (hash-checked again at apply).
|
|
72
|
+
resolvePayload: async (ref) => new Uint8Array(await readFile(join(lastGoodDir(join(harnessRoot, config.cacheDir)), ref.path))),
|
|
73
|
+
},
|
|
74
|
+
logDir: join(harnessRoot, strongkeepDir(), 'undo'),
|
|
75
|
+
});
|
|
76
|
+
const badge = poll.freshness.state === 'fresh'
|
|
77
|
+
? `feed fresh (verified ${poll.freshness.verifiedAt})`
|
|
78
|
+
: `FEED ${poll.freshness.state.toUpperCase()} since ${poll.freshness.lastGoodAt} — ${poll.freshness.reason}`;
|
|
79
|
+
process.stdout.write([badge, ...summary.messages, `reported ${summary.reported} | staged ${summary.staged} | applied ${summary.applied} | refused ${summary.refused}`].join('\n') + '\n');
|
|
80
|
+
// AUTH-REJECTED (HTTP 401/403): a billing/credentials condition, NOT a tamper event.
|
|
81
|
+
// Calm, actionable message + a distinct non-zero exit (3) so callers can tell it apart
|
|
82
|
+
// from success (0) and hard/tamper errors (2). Note: exit 2 would BLOCK the agent's tool
|
|
83
|
+
// call when run as a PreToolUse hook; a credentials lapse must not do that.
|
|
84
|
+
if (poll.authRejected === true) {
|
|
85
|
+
process.stderr.write('strongkeep-poll: your StrongKeep feed token was rejected (this is a subscription/credentials issue, not an attack). ' +
|
|
86
|
+
'Run strongkeep-login to re-enter your token, or check your subscription status. ' +
|
|
87
|
+
'Existing local protection keeps working until the cached feed goes stale.\n');
|
|
88
|
+
process.exit(3);
|
|
89
|
+
}
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
process.stderr.write(`strongkeep-poll: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
94
|
+
process.exit(2);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (isMainModule()) {
|
|
98
|
+
void main();
|
|
99
|
+
}
|
|
100
|
+
function isMainModule() {
|
|
101
|
+
const entry = process.argv[1];
|
|
102
|
+
if (entry === undefined)
|
|
103
|
+
return false;
|
|
104
|
+
try {
|
|
105
|
+
return fileURLToPath(import.meta.url) === realpathSync(entry);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=poll.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poll.js","sourceRoot":"","sources":["../../src/cli/poll.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* strongkeep-revert CLI — one-click self-serve undo (D3). The primary churn-prevention path.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* strongkeep-revert [--harness <dir>] List "recently applied" actions + ids.
|
|
7
|
+
* strongkeep-revert <id> [--harness <dir>] Revert one applied action by id.
|
|
8
|
+
* strongkeep-revert --all-since <iso> [--harness <dir>] Revert everything applied since a timestamp.
|
|
9
|
+
* strongkeep-revert --help
|
|
10
|
+
*
|
|
11
|
+
* Reverting reads the UndoRecord's captured inverse and dispatches it back through the SAME
|
|
12
|
+
* applier that created it (applier.revert), then marks the record reverted (append-only log).
|
|
13
|
+
* No support ticket, no network needed — the undo log is local and is the audit trail.
|
|
14
|
+
*
|
|
15
|
+
* Design: arg-parse + orchestration + exit only. Work is in ../undo.ts + the appliers.
|
|
16
|
+
*/
|
|
17
|
+
export interface RevertCliOptions {
|
|
18
|
+
/** Specific application id to revert, if given. */
|
|
19
|
+
id?: string;
|
|
20
|
+
/** Revert all applications since this ISO timestamp, if given. */
|
|
21
|
+
allSince?: string;
|
|
22
|
+
/** List mode (no id, no --all-since) → show recently-applied list. */
|
|
23
|
+
list: boolean;
|
|
24
|
+
harnessRoot: string;
|
|
25
|
+
showHelp: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Parse argv → options. Total; never throws. */
|
|
29
|
+
export declare function parseRevertArgs(argv: string[], cwd?: string): RevertCliOptions;
|
|
30
|
+
/** Revert one record by id: applier.revert(inverse) then mark reverted. Exported for reuse/tests. */
|
|
31
|
+
export declare function revertById(harnessRoot: string, logDir: string, id: string): Promise<string>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* strongkeep-revert CLI — one-click self-serve undo (D3). The primary churn-prevention path.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* strongkeep-revert [--harness <dir>] List "recently applied" actions + ids.
|
|
7
|
+
* strongkeep-revert <id> [--harness <dir>] Revert one applied action by id.
|
|
8
|
+
* strongkeep-revert --all-since <iso> [--harness <dir>] Revert everything applied since a timestamp.
|
|
9
|
+
* strongkeep-revert --help
|
|
10
|
+
*
|
|
11
|
+
* Reverting reads the UndoRecord's captured inverse and dispatches it back through the SAME
|
|
12
|
+
* applier that created it (applier.revert), then marks the record reverted (append-only log).
|
|
13
|
+
* No support ticket, no network needed — the undo log is local and is the audit trail.
|
|
14
|
+
*
|
|
15
|
+
* Design: arg-parse + orchestration + exit only. Work is in ../undo.ts + the appliers.
|
|
16
|
+
*/
|
|
17
|
+
import process from 'node:process';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { realpathSync } from 'node:fs';
|
|
20
|
+
import { join, resolve } from 'node:path';
|
|
21
|
+
import { listApplied, markReverted } from '../undo.js';
|
|
22
|
+
import { revertInverse } from '../appliers/applier.js';
|
|
23
|
+
import { strongkeepDir } from '../init.js';
|
|
24
|
+
const HELP_TEXT = `strongkeep-revert — one-command local undo of any StrongKeep-applied change
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
strongkeep-revert [--harness <dir>] list recently applied changes + ids
|
|
28
|
+
strongkeep-revert <id> [--harness <dir>] revert one change exactly
|
|
29
|
+
strongkeep-revert --all-since <iso-timestamp> revert everything applied since then`;
|
|
30
|
+
/** Parse argv → options. Total; never throws. */
|
|
31
|
+
export function parseRevertArgs(argv, cwd = process.cwd()) {
|
|
32
|
+
const opts = { list: true, harnessRoot: cwd, showHelp: false };
|
|
33
|
+
for (let i = 0; i < argv.length; i++) {
|
|
34
|
+
const arg = argv[i];
|
|
35
|
+
if (arg === '--help' || arg === '-h')
|
|
36
|
+
opts.showHelp = true;
|
|
37
|
+
else if (arg === '--all-since') {
|
|
38
|
+
const val = argv[++i];
|
|
39
|
+
if (val === undefined || Number.isNaN(Date.parse(val))) {
|
|
40
|
+
return { ...opts, showHelp: true, error: '--all-since requires an ISO timestamp' };
|
|
41
|
+
}
|
|
42
|
+
opts.allSince = val;
|
|
43
|
+
opts.list = false;
|
|
44
|
+
}
|
|
45
|
+
else if (arg === '--harness') {
|
|
46
|
+
const val = argv[++i];
|
|
47
|
+
if (val === undefined)
|
|
48
|
+
return { ...opts, showHelp: true, error: '--harness requires a dir' };
|
|
49
|
+
opts.harnessRoot = val;
|
|
50
|
+
}
|
|
51
|
+
else if (!arg.startsWith('-') && opts.id === undefined) {
|
|
52
|
+
opts.id = arg;
|
|
53
|
+
opts.list = false;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
return { ...opts, showHelp: true, error: `unknown argument "${arg}"` };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return opts;
|
|
60
|
+
}
|
|
61
|
+
/** Revert one record by id: applier.revert(inverse) then mark reverted. Exported for reuse/tests. */
|
|
62
|
+
export async function revertById(harnessRoot, logDir, id) {
|
|
63
|
+
const records = await listApplied(logDir);
|
|
64
|
+
const record = records.find((r) => r.id === id);
|
|
65
|
+
if (record === undefined)
|
|
66
|
+
throw new Error(`no undo record with id "${id}"`);
|
|
67
|
+
if (record.status === 'reverted')
|
|
68
|
+
return `${id}: already reverted`;
|
|
69
|
+
if (record.status === 'staged') {
|
|
70
|
+
await markReverted(logDir, id); // a staged change never touched the system; just retire it
|
|
71
|
+
return `${id}: staged change discarded (nothing was applied)`;
|
|
72
|
+
}
|
|
73
|
+
const { summary } = await revertInverse(record.inverse, { harnessRoot, dryRun: false });
|
|
74
|
+
await markReverted(logDir, id);
|
|
75
|
+
return `${id}: ${summary}`;
|
|
76
|
+
}
|
|
77
|
+
async function main() {
|
|
78
|
+
const opts = parseRevertArgs(process.argv.slice(2));
|
|
79
|
+
if (opts.error)
|
|
80
|
+
process.stderr.write(`strongkeep-revert: ${opts.error}\n\n`);
|
|
81
|
+
if (opts.showHelp) {
|
|
82
|
+
process.stdout.write(HELP_TEXT + '\n');
|
|
83
|
+
process.exit(opts.error ? 2 : 0);
|
|
84
|
+
}
|
|
85
|
+
const harnessRoot = resolve(opts.harnessRoot);
|
|
86
|
+
const logDir = join(harnessRoot, strongkeepDir(), 'undo');
|
|
87
|
+
try {
|
|
88
|
+
if (opts.list) {
|
|
89
|
+
const records = await listApplied(logDir);
|
|
90
|
+
if (records.length === 0) {
|
|
91
|
+
process.stdout.write('nothing applied yet — no StrongKeep change has touched this machine.\n');
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
for (const r of records) {
|
|
95
|
+
process.stdout.write(`${r.id} [${r.status}] ${r.appliedAt} ${r.action.type} — ${r.why}\n`);
|
|
96
|
+
}
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
if (opts.allSince !== undefined) {
|
|
100
|
+
const since = Date.parse(opts.allSince);
|
|
101
|
+
const records = (await listApplied(logDir)).filter((r) => r.status === 'applied' && Date.parse(r.appliedAt) >= since);
|
|
102
|
+
for (const r of records)
|
|
103
|
+
process.stdout.write((await revertById(harnessRoot, logDir, r.id)) + '\n');
|
|
104
|
+
process.stdout.write(`reverted ${records.length} change(s) since ${opts.allSince}\n`);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
process.stdout.write((await revertById(harnessRoot, logDir, opts.id)) + '\n');
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
process.stderr.write(`strongkeep-revert: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
112
|
+
process.exit(2);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (isMainModule()) {
|
|
116
|
+
void main();
|
|
117
|
+
}
|
|
118
|
+
function isMainModule() {
|
|
119
|
+
const entry = process.argv[1];
|
|
120
|
+
if (entry === undefined)
|
|
121
|
+
return false;
|
|
122
|
+
try {
|
|
123
|
+
return fileURLToPath(import.meta.url) === realpathSync(entry);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=revert.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revert.js","sourceRoot":"","sources":["../../src/cli/revert.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feed client (consumer side) — poll the signed feed, verify it, and hand VERIFIED,
|
|
3
|
+
* parsed entries to the apply pipeline. This is the top of the trust flow.
|
|
4
|
+
*
|
|
5
|
+
* Trust flow (BUILD-PLAN "trust mechanics" — order is fixed and total):
|
|
6
|
+
* 1. Fetch index.json with the per-tenant token.
|
|
7
|
+
* 2. core.verifyManifestSignature against the PINNED public key. Fail → do NOT proceed.
|
|
8
|
+
* 3. For each entry, fetch its file, core.verifyEntryHash against the manifest. Fail → whole
|
|
9
|
+
* poll fails (a partially-authentic feed is not authentic).
|
|
10
|
+
* 4. core.parseEntry (frontmatter only; body stays opaque).
|
|
11
|
+
* 5. Hand parsed entries to the pipeline behind the autonomy tier.
|
|
12
|
+
*
|
|
13
|
+
* FAIL-SAFE (D2 — REQUIRED): on fetch failure OR verification failure, DO NOT drop existing
|
|
14
|
+
* guards. Fall back to the last-good cached feed and enforce it; fetch/apply nothing new;
|
|
15
|
+
* mark the feed stale. Fail CLOSED on new/unsigned actions only. Never fail open. A
|
|
16
|
+
* max-staleness threshold (default 72h) escalates to stale-critical (dashboard alert).
|
|
17
|
+
*
|
|
18
|
+
* SECURITY:
|
|
19
|
+
* - The pinned public key ships with the watchdog and is authoritative over the manifest's
|
|
20
|
+
* self-declared key.
|
|
21
|
+
* - The per-tenant token is read from the local config the init step wrote; it is NEVER
|
|
22
|
+
* committed and never logged (no code path here writes it anywhere).
|
|
23
|
+
* - The last-good cache only ever holds a FULLY-verified snapshot, and is re-verified on
|
|
24
|
+
* load (so cache tampering is also caught).
|
|
25
|
+
* - No entry BODY is ever executed; only parsed actions flow onward.
|
|
26
|
+
*/
|
|
27
|
+
import type { PinnedPublicKey, ParsedEntry, SignedManifest } from '@strongkeep/core';
|
|
28
|
+
/** Default max staleness before escalation: 72 hours (D2, confirmed default). */
|
|
29
|
+
export declare const DEFAULT_MAX_STALENESS_MS: number;
|
|
30
|
+
/** Where the client reads its config + last-good cache from (written by strongkeep-init). */
|
|
31
|
+
export interface FeedClientConfig {
|
|
32
|
+
/** Signed feed base URL (index.json + entries under it). https://… or file://… (tests/local). */
|
|
33
|
+
feedUrl: string;
|
|
34
|
+
/**
|
|
35
|
+
* Per-tenant token. Read from local config; never logged, never committed. Optional: a
|
|
36
|
+
* public-read feed (e.g. the beta) needs no token, and authenticity comes from signature
|
|
37
|
+
* verification against the pinned key, not from transport auth. Explicit undefined is
|
|
38
|
+
* allowed so callers can pass a possibly-absent config field straight through.
|
|
39
|
+
*/
|
|
40
|
+
token?: string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The pinned public key(s) the manifest must verify against. A set (key rotation): the
|
|
43
|
+
* manifest is accepted iff it verifies against ANY key in the set; an EMPTY set rejects
|
|
44
|
+
* everything (fail closed). A bare key is treated as a 1-element set (backward compatible).
|
|
45
|
+
*/
|
|
46
|
+
pinnedKey: PinnedPublicKey | readonly PinnedPublicKey[];
|
|
47
|
+
/** Local directory holding the last-good cache + state. */
|
|
48
|
+
cacheDir: string;
|
|
49
|
+
/** Max staleness before escalation, ms. Default 72h. */
|
|
50
|
+
maxStalenessMs: number;
|
|
51
|
+
/** Clock, injectable for staleness tests. Defaults to Date.now. */
|
|
52
|
+
now?: () => number;
|
|
53
|
+
}
|
|
54
|
+
/** Freshness state surfaced to the UI/badge (D2). */
|
|
55
|
+
export type FeedFreshness = {
|
|
56
|
+
state: 'fresh';
|
|
57
|
+
verifiedAt: string;
|
|
58
|
+
} | {
|
|
59
|
+
state: 'stale';
|
|
60
|
+
lastGoodAt: string;
|
|
61
|
+
reason: string;
|
|
62
|
+
} | {
|
|
63
|
+
state: 'stale-critical';
|
|
64
|
+
lastGoodAt: string;
|
|
65
|
+
reason: string;
|
|
66
|
+
};
|
|
67
|
+
/** Result of a poll: the verified entries currently in force + the freshness state. */
|
|
68
|
+
export interface PollResult {
|
|
69
|
+
/** Entries from the newly-verified feed, OR the last-good cache under fail-safe. */
|
|
70
|
+
entries: ParsedEntry[];
|
|
71
|
+
freshness: FeedFreshness;
|
|
72
|
+
/** True when these entries came from the last-good cache (a fail-safe fallback happened). */
|
|
73
|
+
usedLastGood: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* True when the poll failed because the feed REJECTED our token (HTTP 401/403). This is a
|
|
76
|
+
* billing/credentials condition, NOT a tamper/security event — surface it as its own,
|
|
77
|
+
* non-alarming message. The fail-safe still applied: last-good protection stays in force.
|
|
78
|
+
*/
|
|
79
|
+
authRejected?: boolean;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The feed rejected our per-tenant token (HTTP 401/403). Distinct from tamper/verification
|
|
83
|
+
* failures so callers can show a calm "fix your credentials/subscription" message instead of
|
|
84
|
+
* a security alarm. Never carries the token value.
|
|
85
|
+
*/
|
|
86
|
+
export declare class FeedAuthError extends Error {
|
|
87
|
+
readonly status: number;
|
|
88
|
+
readonly code: "auth-rejected";
|
|
89
|
+
constructor(status: number);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Poll the feed once: fetch → verify signature → verify per-entry hashes → parse. On ANY
|
|
93
|
+
* verification/fetch failure, fall back to the last-good cache (D2 fail-safe) rather than
|
|
94
|
+
* dropping protection. Returns the entries currently in force + freshness.
|
|
95
|
+
*/
|
|
96
|
+
export declare function pollFeed(config: FeedClientConfig): Promise<PollResult>;
|
|
97
|
+
/**
|
|
98
|
+
* Persist a freshly-verified snapshot (manifest + entry files + timestamp) as the new
|
|
99
|
+
* last-good cache. ONLY pollFeed calls this, and only after full verification succeeds, so the
|
|
100
|
+
* cache never holds unauthentic content — and it is re-verified again on load regardless.
|
|
101
|
+
*/
|
|
102
|
+
export declare function saveLastGood(config: FeedClientConfig, manifestBytes: Uint8Array, entryBytes: Map<string, Uint8Array>, manifest: SignedManifest, verifiedAt: string): Promise<void>;
|
|
103
|
+
/** Path of the last-good snapshot dir (feed-shaped: index.json + entries/) for the local MCP server. */
|
|
104
|
+
export declare function lastGoodDir(cacheDir: string): string;
|
|
105
|
+
/**
|
|
106
|
+
* Load the last-good cache during an outage. The snapshot is RE-VERIFIED against the pinned
|
|
107
|
+
* key on load (cache tampering fails closed too). Freshness is computed from the snapshot
|
|
108
|
+
* timestamp against maxStalenessMs; the cache is never reported fresh.
|
|
109
|
+
*/
|
|
110
|
+
export declare function loadLastGood(config: FeedClientConfig, reason?: string): Promise<PollResult>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feed client (consumer side) — poll the signed feed, verify it, and hand VERIFIED,
|
|
3
|
+
* parsed entries to the apply pipeline. This is the top of the trust flow.
|
|
4
|
+
*
|
|
5
|
+
* Trust flow (BUILD-PLAN "trust mechanics" — order is fixed and total):
|
|
6
|
+
* 1. Fetch index.json with the per-tenant token.
|
|
7
|
+
* 2. core.verifyManifestSignature against the PINNED public key. Fail → do NOT proceed.
|
|
8
|
+
* 3. For each entry, fetch its file, core.verifyEntryHash against the manifest. Fail → whole
|
|
9
|
+
* poll fails (a partially-authentic feed is not authentic).
|
|
10
|
+
* 4. core.parseEntry (frontmatter only; body stays opaque).
|
|
11
|
+
* 5. Hand parsed entries to the pipeline behind the autonomy tier.
|
|
12
|
+
*
|
|
13
|
+
* FAIL-SAFE (D2 — REQUIRED): on fetch failure OR verification failure, DO NOT drop existing
|
|
14
|
+
* guards. Fall back to the last-good cached feed and enforce it; fetch/apply nothing new;
|
|
15
|
+
* mark the feed stale. Fail CLOSED on new/unsigned actions only. Never fail open. A
|
|
16
|
+
* max-staleness threshold (default 72h) escalates to stale-critical (dashboard alert).
|
|
17
|
+
*
|
|
18
|
+
* SECURITY:
|
|
19
|
+
* - The pinned public key ships with the watchdog and is authoritative over the manifest's
|
|
20
|
+
* self-declared key.
|
|
21
|
+
* - The per-tenant token is read from the local config the init step wrote; it is NEVER
|
|
22
|
+
* committed and never logged (no code path here writes it anywhere).
|
|
23
|
+
* - The last-good cache only ever holds a FULLY-verified snapshot, and is re-verified on
|
|
24
|
+
* load (so cache tampering is also caught).
|
|
25
|
+
* - No entry BODY is ever executed; only parsed actions flow onward.
|
|
26
|
+
*/
|
|
27
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
28
|
+
import { dirname, join } from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import { validateManifestShape, verifyManifestSignatureAny, verifyEntryHash, parseEntry } from '@strongkeep/core';
|
|
31
|
+
/** Default max staleness before escalation: 72 hours (D2, confirmed default). */
|
|
32
|
+
export const DEFAULT_MAX_STALENESS_MS = 72 * 60 * 60 * 1000;
|
|
33
|
+
/** Name of the last-good snapshot directory inside cacheDir. */
|
|
34
|
+
const LAST_GOOD_DIR = 'last-good';
|
|
35
|
+
/** Timestamp marker file inside the last-good snapshot. */
|
|
36
|
+
const VERIFIED_AT_FILE = 'verified-at.txt';
|
|
37
|
+
/**
|
|
38
|
+
* The feed rejected our per-tenant token (HTTP 401/403). Distinct from tamper/verification
|
|
39
|
+
* failures so callers can show a calm "fix your credentials/subscription" message instead of
|
|
40
|
+
* a security alarm. Never carries the token value.
|
|
41
|
+
*/
|
|
42
|
+
export class FeedAuthError extends Error {
|
|
43
|
+
status;
|
|
44
|
+
code = 'auth-rejected';
|
|
45
|
+
constructor(status) {
|
|
46
|
+
super(`StrongKeep could not authenticate to the feed (HTTP ${status}). Your access token was ` +
|
|
47
|
+
'rejected: your subscription may be inactive, or the token is wrong. Run strongkeep-login ' +
|
|
48
|
+
'to re-enter it, or check your subscription. Existing local protection keeps working ' +
|
|
49
|
+
'until the cached feed goes stale.');
|
|
50
|
+
this.status = status;
|
|
51
|
+
this.name = 'FeedAuthError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Fetch bytes from the feed. file:// reads disk (local/test feeds); http(s) uses global fetch. */
|
|
55
|
+
async function fetchBytes(url, token) {
|
|
56
|
+
if (url.startsWith('file://')) {
|
|
57
|
+
return new Uint8Array(await readFile(fileURLToPath(url)));
|
|
58
|
+
}
|
|
59
|
+
// Only send an authorization header when we actually hold a token; a tokenless client
|
|
60
|
+
// (public-read feed) sends none rather than a bogus "Bearer undefined".
|
|
61
|
+
const headers = typeof token === 'string' && token !== '' ? { authorization: `Bearer ${token}` } : {};
|
|
62
|
+
const response = await fetch(url, { headers });
|
|
63
|
+
// 401/403 = the TOKEN was rejected (billing/credentials), not a broken feed — distinct error.
|
|
64
|
+
if (response.status === 401 || response.status === 403)
|
|
65
|
+
throw new FeedAuthError(response.status);
|
|
66
|
+
if (!response.ok)
|
|
67
|
+
throw new Error(`fetch failed: HTTP ${response.status} for feed resource`);
|
|
68
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Run the full verify flow over a manifest + entry-byte source. Shared by the remote poll and
|
|
72
|
+
* the cache load so the cache is held to the SAME verification bar as the live feed.
|
|
73
|
+
*/
|
|
74
|
+
async function verifyAndParse(manifestBytes, getEntryBytes, pinnedKey) {
|
|
75
|
+
const manifest = validateManifestShape(JSON.parse(new TextDecoder().decode(manifestBytes)));
|
|
76
|
+
const pinnedSet = Array.isArray(pinnedKey) ? pinnedKey : [pinnedKey];
|
|
77
|
+
const sig = verifyManifestSignatureAny(manifest, pinnedSet);
|
|
78
|
+
if (!sig.ok)
|
|
79
|
+
throw new Error(`verification failed: ${sig.code}: ${sig.reason}`);
|
|
80
|
+
const entries = [];
|
|
81
|
+
const entryBytes = new Map();
|
|
82
|
+
for (const record of manifest.entries) {
|
|
83
|
+
const bytes = await getEntryBytes(record.file);
|
|
84
|
+
const hash = verifyEntryHash(bytes, record);
|
|
85
|
+
if (!hash.ok)
|
|
86
|
+
throw new Error(`verification failed: ${hash.code}: ${hash.reason}`);
|
|
87
|
+
entries.push(parseEntry(bytes));
|
|
88
|
+
entryBytes.set(record.file, bytes);
|
|
89
|
+
}
|
|
90
|
+
return { manifest, entries, entryBytes };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Poll the feed once: fetch → verify signature → verify per-entry hashes → parse. On ANY
|
|
94
|
+
* verification/fetch failure, fall back to the last-good cache (D2 fail-safe) rather than
|
|
95
|
+
* dropping protection. Returns the entries currently in force + freshness.
|
|
96
|
+
*/
|
|
97
|
+
export async function pollFeed(config) {
|
|
98
|
+
const base = config.feedUrl.endsWith('/') ? config.feedUrl : `${config.feedUrl}/`;
|
|
99
|
+
try {
|
|
100
|
+
const manifestBytes = await fetchBytes(`${base}index.json`, config.token);
|
|
101
|
+
const verified = await verifyAndParse(manifestBytes, (file) => fetchBytes(`${base}${file}`, config.token), config.pinnedKey);
|
|
102
|
+
const verifiedAt = new Date((config.now ?? Date.now)()).toISOString();
|
|
103
|
+
await saveLastGood(config, manifestBytes, verified.entryBytes, verified.manifest, verifiedAt);
|
|
104
|
+
return { entries: verified.entries, freshness: { state: 'fresh', verifiedAt }, usedLastGood: false };
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
// FAIL-SAFE: never fail open. Enforce the last-good verified snapshot; apply nothing new.
|
|
108
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
109
|
+
const result = await loadLastGood(config, reason);
|
|
110
|
+
// Tag the credentials case so the CLI can show a calm billing/token message, not an alarm.
|
|
111
|
+
return err instanceof FeedAuthError ? { ...result, authRejected: true } : result;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Persist a freshly-verified snapshot (manifest + entry files + timestamp) as the new
|
|
116
|
+
* last-good cache. ONLY pollFeed calls this, and only after full verification succeeds, so the
|
|
117
|
+
* cache never holds unauthentic content — and it is re-verified again on load regardless.
|
|
118
|
+
*/
|
|
119
|
+
export async function saveLastGood(config, manifestBytes, entryBytes, manifest, verifiedAt) {
|
|
120
|
+
const dir = join(config.cacheDir, LAST_GOOD_DIR);
|
|
121
|
+
for (const record of manifest.entries) {
|
|
122
|
+
const path = join(dir, record.file);
|
|
123
|
+
await mkdir(dirname(path), { recursive: true });
|
|
124
|
+
await writeFile(path, entryBytes.get(record.file));
|
|
125
|
+
}
|
|
126
|
+
// Index written LAST: a crash mid-persist leaves the previous index (or none) pointing only
|
|
127
|
+
// at fully-written entries. ponytail: full temp-dir + rename swap is the upgrade path.
|
|
128
|
+
await writeFile(join(dir, 'index.json'), manifestBytes);
|
|
129
|
+
await writeFile(join(dir, VERIFIED_AT_FILE), verifiedAt + '\n', 'utf8');
|
|
130
|
+
}
|
|
131
|
+
/** Path of the last-good snapshot dir (feed-shaped: index.json + entries/) for the local MCP server. */
|
|
132
|
+
export function lastGoodDir(cacheDir) {
|
|
133
|
+
return join(cacheDir, LAST_GOOD_DIR);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Load the last-good cache during an outage. The snapshot is RE-VERIFIED against the pinned
|
|
137
|
+
* key on load (cache tampering fails closed too). Freshness is computed from the snapshot
|
|
138
|
+
* timestamp against maxStalenessMs; the cache is never reported fresh.
|
|
139
|
+
*/
|
|
140
|
+
export async function loadLastGood(config, reason = 'feed unavailable') {
|
|
141
|
+
const dir = join(config.cacheDir, LAST_GOOD_DIR);
|
|
142
|
+
let verifiedAtText;
|
|
143
|
+
let manifestBytes;
|
|
144
|
+
try {
|
|
145
|
+
verifiedAtText = (await readFile(join(dir, VERIFIED_AT_FILE), 'utf8')).trim();
|
|
146
|
+
manifestBytes = new Uint8Array(await readFile(join(dir, 'index.json')));
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// No last-good cache at all: fail CLOSED with zero new entries. Guards installed by prior
|
|
150
|
+
// applies remain in place on disk — nothing here removes protection.
|
|
151
|
+
return {
|
|
152
|
+
entries: [],
|
|
153
|
+
freshness: { state: 'stale-critical', lastGoodAt: 'never', reason: `${reason}; no last-good cache exists` },
|
|
154
|
+
usedLastGood: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const verified = await verifyAndParse(manifestBytes, async (file) => new Uint8Array(await readFile(join(dir, file))), config.pinnedKey);
|
|
159
|
+
const age = (config.now ?? Date.now)() - Date.parse(verifiedAtText);
|
|
160
|
+
const freshness = age > config.maxStalenessMs
|
|
161
|
+
? { state: 'stale-critical', lastGoodAt: verifiedAtText, reason }
|
|
162
|
+
: { state: 'stale', lastGoodAt: verifiedAtText, reason };
|
|
163
|
+
return { entries: verified.entries, freshness, usedLastGood: true };
|
|
164
|
+
}
|
|
165
|
+
catch (cacheErr) {
|
|
166
|
+
const cacheReason = cacheErr instanceof Error ? cacheErr.message : String(cacheErr);
|
|
167
|
+
return {
|
|
168
|
+
entries: [],
|
|
169
|
+
freshness: {
|
|
170
|
+
state: 'stale-critical',
|
|
171
|
+
lastGoodAt: verifiedAtText,
|
|
172
|
+
reason: `${reason}; last-good cache failed re-verification (${cacheReason})`,
|
|
173
|
+
},
|
|
174
|
+
usedLastGood: true,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=feed-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feed-client.js","sourceRoot":"","sources":["../src/feed-client.ts"],"names":[],"mappings":"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"}
|
package/dist/fires.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
/** Basename of the fire log inside `.claude/strongkeep`. */
|
|
17
|
+
export declare const FIRES_FILE = "fires.ndjson";
|
|
18
|
+
/** One recorded guardrail fire. Rule id + timestamp ONLY — no matched content. */
|
|
19
|
+
export interface FireRecord {
|
|
20
|
+
firedAt: string;
|
|
21
|
+
ruleId: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Append a fire to the log at `<strongkeepDir>/fires.ndjson`. Best-effort and non-throwing
|
|
25
|
+
* on a write failure (a guardrail must never crash the agent's tool call just because its
|
|
26
|
+
* telemetry line could not be written) — but it DOES create the directory if missing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function recordFire(strongkeepDir: string, ruleId: string, at?: Date): Promise<void>;
|