@lannguyensi/harness 0.40.0 → 0.41.0
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/CHANGELOG.md +37 -0
- package/dist/cli/approve/understanding.js +28 -0
- package/dist/cli/approve/understanding.js.map +1 -1
- package/dist/cli/doctor/codex.d.ts +8 -0
- package/dist/cli/doctor/codex.js +8 -1
- package/dist/cli/doctor/codex.js.map +1 -1
- package/dist/cli/doctor/format.js +23 -11
- package/dist/cli/doctor/format.js.map +1 -1
- package/dist/cli/doctor/index.d.ts +36 -0
- package/dist/cli/doctor/index.js +131 -29
- package/dist/cli/doctor/index.js.map +1 -1
- package/dist/cli/doctor/types.d.ts +26 -0
- package/dist/cli/dry-run.js +5 -0
- package/dist/cli/dry-run.js.map +1 -1
- package/dist/cli/explain.js +16 -3
- package/dist/cli/explain.js.map +1 -1
- package/dist/cli/index.js +75 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/composer.js +10 -31
- package/dist/cli/init/composer.js.map +1 -1
- package/dist/cli/init/index.d.ts +18 -0
- package/dist/cli/init/index.js +28 -2
- package/dist/cli/init/index.js.map +1 -1
- package/dist/cli/init/interactive.d.ts +8 -0
- package/dist/cli/init/interactive.js +22 -1
- package/dist/cli/init/interactive.js.map +1 -1
- package/dist/cli/init/profiles.d.ts +2 -2
- package/dist/cli/init/profiles.js +9 -0
- package/dist/cli/init/profiles.js.map +1 -1
- package/dist/cli/init/templates.d.ts +1 -1
- package/dist/cli/init/templates.js +153 -3
- package/dist/cli/init/templates.js.map +1 -1
- package/dist/cli/list.js +3 -1
- package/dist/cli/list.js.map +1 -1
- package/dist/cli/pack/hook-bootstrap.d.ts +24 -0
- package/dist/cli/pack/hook-bootstrap.js +47 -1
- package/dist/cli/pack/hook-bootstrap.js.map +1 -1
- package/dist/cli/pack/hook-branch-protection.js +9 -1
- package/dist/cli/pack/hook-branch-protection.js.map +1 -1
- package/dist/cli/pack/hook-codex-post-tool-use.d.ts +25 -0
- package/dist/cli/pack/hook-codex-post-tool-use.js +160 -0
- package/dist/cli/pack/hook-codex-post-tool-use.js.map +1 -0
- package/dist/cli/pack/hook-codex-pre-tool-use.d.ts +1 -0
- package/dist/cli/pack/hook-codex-pre-tool-use.js +43 -10
- package/dist/cli/pack/hook-codex-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-codex-stop.js +1 -8
- package/dist/cli/pack/hook-codex-stop.js.map +1 -1
- package/dist/cli/pack/hook-post-tool-use.js +16 -131
- package/dist/cli/pack/hook-post-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-pre-tool-use.js +54 -2
- package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-stay-in-scope.js +63 -20
- package/dist/cli/pack/hook-stay-in-scope.js.map +1 -1
- package/dist/cli/pack/hook-track-active-claim.js +33 -9
- package/dist/cli/pack/hook-track-active-claim.js.map +1 -1
- package/dist/cli/pack/index.d.ts +2 -1
- package/dist/cli/pack/index.js +2 -1
- package/dist/cli/pack/index.js.map +1 -1
- package/dist/cli/pack/mutate.d.ts +19 -0
- package/dist/cli/pack/mutate.js +38 -0
- package/dist/cli/pack/mutate.js.map +1 -1
- package/dist/cli/pack/reseed.d.ts +19 -0
- package/dist/cli/pack/reseed.js +118 -0
- package/dist/cli/pack/reseed.js.map +1 -0
- package/dist/cli/pause/index.js +12 -3
- package/dist/cli/pause/index.js.map +1 -1
- package/dist/cli/session-start/index.d.ts +14 -0
- package/dist/cli/session-start/index.js +42 -2
- package/dist/cli/session-start/index.js.map +1 -1
- package/dist/cli/validate/checks.js +8 -0
- package/dist/cli/validate/checks.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection-runtime.d.ts +4 -2
- package/dist/policy-packs/builtin/branch-protection-runtime.js +4 -2
- package/dist/policy-packs/builtin/branch-protection-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/branch-protection.d.ts +10 -1
- package/dist/policy-packs/builtin/branch-protection.js +17 -0
- package/dist/policy-packs/builtin/branch-protection.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.d.ts +197 -24
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js +342 -40
- package/dist/policy-packs/builtin/understanding-before-execution-runtime.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution.d.ts +26 -1
- package/dist/policy-packs/builtin/understanding-before-execution.js +302 -17
- package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
- package/dist/policy-packs/registry.d.ts +22 -1
- package/dist/policy-packs/registry.js +19 -2
- package/dist/policy-packs/registry.js.map +1 -1
- package/dist/policy-packs/ux-compare.d.ts +14 -0
- package/dist/policy-packs/ux-compare.js +58 -0
- package/dist/policy-packs/ux-compare.js.map +1 -0
- package/dist/policy-packs/ux-drift-check.d.ts +27 -0
- package/dist/policy-packs/ux-drift-check.js +78 -0
- package/dist/policy-packs/ux-drift-check.js.map +1 -0
- package/dist/probes/mcp.d.ts +13 -0
- package/dist/probes/mcp.js +57 -0
- package/dist/probes/mcp.js.map +1 -1
- package/dist/runtime/approval-signing.d.ts +82 -0
- package/dist/runtime/approval-signing.js +275 -0
- package/dist/runtime/approval-signing.js.map +1 -0
- package/dist/runtime/intercept.js +62 -5
- package/dist/runtime/intercept.js.map +1 -1
- package/dist/runtime/pause-sentinel.js +14 -3
- package/dist/runtime/pause-sentinel.js.map +1 -1
- package/dist/runtime/recovery-git-commit.d.ts +22 -0
- package/dist/runtime/recovery-git-commit.js +284 -0
- package/dist/runtime/recovery-git-commit.js.map +1 -0
- package/dist/runtime/tool-name-aliases.d.ts +1 -0
- package/dist/runtime/tool-name-aliases.js +7 -1
- package/dist/runtime/tool-name-aliases.js.map +1 -1
- package/dist/schema/index.d.ts +53 -42
- package/dist/schema/policies.d.ts +56 -44
- package/dist/schema/policies.js +79 -9
- package/dist/schema/policies.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ux-compare.js","sourceRoot":"","sources":["../../src/policy-packs/ux-compare.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Manifest } from "../schema/index.js";
|
|
2
|
+
export type PolicyPackUxDriftField = "ux" | "producers";
|
|
3
|
+
export interface PolicyPackUxDrift {
|
|
4
|
+
packIndex: number;
|
|
5
|
+
packName: string;
|
|
6
|
+
/** Which sub-field(s) diverge from the shipped template. */
|
|
7
|
+
fields: PolicyPackUxDriftField[];
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Walks `manifest.policy_packs` in declared order. For each enabled
|
|
12
|
+
* builtin pack that has a registered shipped default (see
|
|
13
|
+
* `resolveBuiltinDefaultConfig`), compares the operator's declared
|
|
14
|
+
* `config.ux` / `config.producers` (when present) against that default
|
|
15
|
+
* and reports a divergence for each sub-field that differs.
|
|
16
|
+
*
|
|
17
|
+
* A pack that never declared `config.ux` (or `config.producers`) at all
|
|
18
|
+
* is NOT flagged here: that is a distinct, pre-existing gap (the pack
|
|
19
|
+
* falls back to its legacy engine-vocabulary deny text) rather than a
|
|
20
|
+
* stale COPY of the shipped text, and is out of scope for this check —
|
|
21
|
+
* see task 68b9ad9c's framing (an operator whose manifest predates a
|
|
22
|
+
* wording fix has a STALE `ux.run`, not a MISSING one). A malformed
|
|
23
|
+
* `config.ux` / `config.producers` (already reported separately by
|
|
24
|
+
* `checkPolicyPackConfigs`) is treated as diverging here too, rather
|
|
25
|
+
* than being silently skipped.
|
|
26
|
+
*/
|
|
27
|
+
export declare function checkPolicyPackUxDrift(manifest: Manifest): PolicyPackUxDrift[];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Per-pack `config.ux` / `config.producers` divergence check (task
|
|
2
|
+
// 68b9ad9c). Used by `harness doctor` to warn when an enabled builtin
|
|
3
|
+
// pack's agent-facing deny-message text no longer matches the shipped
|
|
4
|
+
// template for that pack.
|
|
5
|
+
//
|
|
6
|
+
// Motivation (dogfood 2026-07-10, harness 0.40.0): the understanding-gate
|
|
7
|
+
// deny message is entirely driven by `config.ux` when the operator has
|
|
8
|
+
// declared one — the init templates (Solo/Team/Full/Custom) were fixed to
|
|
9
|
+
// teach a new submission form, but that fix only reaches manifests
|
|
10
|
+
// generated AFTER the fix landed. An operator who installed before the fix
|
|
11
|
+
// has an on-disk `config.ux` that still teaches the old wording, and
|
|
12
|
+
// nothing short of a hand-edit ever updated it: `harness apply` projects
|
|
13
|
+
// the manifest OUT to settings.json, it never re-seeds pack config FROM
|
|
14
|
+
// the shipped template back INTO the manifest.
|
|
15
|
+
//
|
|
16
|
+
// This check is the read side of that gap (the warning); `harness pack
|
|
17
|
+
// reseed <name>` (src/cli/pack/reseed.ts) is the opt-in write side. Both
|
|
18
|
+
// consume the same canonical default via `resolveBuiltinDefaultConfig`,
|
|
19
|
+
// and the same comparison helpers (ux-compare.ts), so they can never
|
|
20
|
+
// independently drift on what "the shipped template" means.
|
|
21
|
+
//
|
|
22
|
+
// Deliberately doctor-only (not `harness validate`), mirroring
|
|
23
|
+
// `checkPolicyPackVersions`: this is an informational nudge about
|
|
24
|
+
// wording that still functions, not a schema violation.
|
|
25
|
+
import { resolveBuiltinDefaultConfig } from "./registry.js";
|
|
26
|
+
import { producersEqual, safeParseProducers, safeParseUx, uxEqual } from "./ux-compare.js";
|
|
27
|
+
/**
|
|
28
|
+
* Walks `manifest.policy_packs` in declared order. For each enabled
|
|
29
|
+
* builtin pack that has a registered shipped default (see
|
|
30
|
+
* `resolveBuiltinDefaultConfig`), compares the operator's declared
|
|
31
|
+
* `config.ux` / `config.producers` (when present) against that default
|
|
32
|
+
* and reports a divergence for each sub-field that differs.
|
|
33
|
+
*
|
|
34
|
+
* A pack that never declared `config.ux` (or `config.producers`) at all
|
|
35
|
+
* is NOT flagged here: that is a distinct, pre-existing gap (the pack
|
|
36
|
+
* falls back to its legacy engine-vocabulary deny text) rather than a
|
|
37
|
+
* stale COPY of the shipped text, and is out of scope for this check —
|
|
38
|
+
* see task 68b9ad9c's framing (an operator whose manifest predates a
|
|
39
|
+
* wording fix has a STALE `ux.run`, not a MISSING one). A malformed
|
|
40
|
+
* `config.ux` / `config.producers` (already reported separately by
|
|
41
|
+
* `checkPolicyPackConfigs`) is treated as diverging here too, rather
|
|
42
|
+
* than being silently skipped.
|
|
43
|
+
*/
|
|
44
|
+
export function checkPolicyPackUxDrift(manifest) {
|
|
45
|
+
const out = [];
|
|
46
|
+
manifest.policy_packs.forEach((pack, packIndex) => {
|
|
47
|
+
if (!pack.enabled)
|
|
48
|
+
return;
|
|
49
|
+
const canonical = resolveBuiltinDefaultConfig(pack);
|
|
50
|
+
if (!canonical)
|
|
51
|
+
return;
|
|
52
|
+
const fields = [];
|
|
53
|
+
const currentUx = pack.config["ux"];
|
|
54
|
+
if (canonical.ux && currentUx !== undefined) {
|
|
55
|
+
const parsed = safeParseUx(currentUx);
|
|
56
|
+
if (!parsed || !uxEqual(parsed, canonical.ux))
|
|
57
|
+
fields.push("ux");
|
|
58
|
+
}
|
|
59
|
+
const currentProducers = pack.config["producers"];
|
|
60
|
+
if (canonical.producers && currentProducers !== undefined) {
|
|
61
|
+
const parsed = safeParseProducers(currentProducers);
|
|
62
|
+
if (!parsed || !producersEqual(parsed, canonical.producers))
|
|
63
|
+
fields.push("producers");
|
|
64
|
+
}
|
|
65
|
+
if (fields.length === 0)
|
|
66
|
+
return;
|
|
67
|
+
out.push({
|
|
68
|
+
packIndex,
|
|
69
|
+
packName: pack.name,
|
|
70
|
+
fields,
|
|
71
|
+
message: `pack "${pack.name}" config.${fields.join(" / config.")} diverges from the shipped ` +
|
|
72
|
+
`builtin template; a supported update exists. Review with ` +
|
|
73
|
+
`\`harness pack reseed ${pack.name} --dry-run\` and apply with \`harness pack reseed ${pack.name}\`.`,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=ux-drift-check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ux-drift-check.js","sourceRoot":"","sources":["../../src/policy-packs/ux-drift-check.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,sEAAsE;AACtE,sEAAsE;AACtE,0BAA0B;AAC1B,EAAE;AACF,0EAA0E;AAC1E,uEAAuE;AACvE,0EAA0E;AAC1E,mEAAmE;AACnE,2EAA2E;AAC3E,qEAAqE;AACrE,yEAAyE;AACzE,wEAAwE;AACxE,+CAA+C;AAC/C,EAAE;AACF,uEAAuE;AACvE,yEAAyE;AACzE,wEAAwE;AACxE,qEAAqE;AACrE,4DAA4D;AAC5D,EAAE;AACF,+DAA+D;AAC/D,kEAAkE;AAClE,wDAAwD;AAExD,OAAO,EAAE,2BAA2B,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAa3F;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,sBAAsB,CAAC,QAAkB;IACvD,MAAM,GAAG,GAAwB,EAAE,CAAC;IACpC,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE;QAChD,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAC1B,MAAM,SAAS,GAAG,2BAA2B,CAAC,IAAI,CAAC,CAAC;QACpD,IAAI,CAAC,SAAS;YAAE,OAAO;QAEvB,MAAM,MAAM,GAA6B,EAAE,CAAC;QAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,SAAS,CAAC,EAAE,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5C,MAAM,MAAM,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;YACtC,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC;gBAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnE,CAAC;QACD,MAAM,gBAAgB,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QAClD,IAAI,SAAS,CAAC,SAAS,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;YAC1D,MAAM,MAAM,GAAG,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;YACpD,IAAI,CAAC,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,SAAS,CAAC,SAAS,CAAC;gBAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACxF,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAEhC,GAAG,CAAC,IAAI,CAAC;YACP,SAAS;YACT,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,MAAM;YACN,OAAO,EACL,SAAS,IAAI,CAAC,IAAI,YAAY,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,6BAA6B;gBACpF,2DAA2D;gBAC3D,yBAAyB,IAAI,CAAC,IAAI,qDAAqD,IAAI,CAAC,IAAI,KAAK;SACxG,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/probes/mcp.d.ts
CHANGED
|
@@ -6,6 +6,19 @@ export type McpProbeOutcome = {
|
|
|
6
6
|
kind: "error";
|
|
7
7
|
latencyMs: number;
|
|
8
8
|
message: string;
|
|
9
|
+
/**
|
|
10
|
+
* Set when the failure is a `spawn` `ENOENT` (the declared binary
|
|
11
|
+
* could not be resolved at all, as opposed to the process starting
|
|
12
|
+
* and then misbehaving). Doctor uses this to decide whether a
|
|
13
|
+
* PATH-shadow remediation hint applies (harness/7f8fb4bc).
|
|
14
|
+
*/
|
|
15
|
+
enoent?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* One-line PATH-shadow remediation, populated by the doctor layer
|
|
18
|
+
* (which knows the resolved npm global bin dir) when an `enoent`
|
|
19
|
+
* binary is found to actually exist there, just not on PATH.
|
|
20
|
+
*/
|
|
21
|
+
pathHint?: string;
|
|
9
22
|
}
|
|
10
23
|
/**
|
|
11
24
|
* Server exited with code 0 BEFORE the doctor received a JSON-RPC
|
package/dist/probes/mcp.js
CHANGED
|
@@ -15,6 +15,29 @@ function commandToArgs(server) {
|
|
|
15
15
|
const args = list.slice(1).map(expandHomePath);
|
|
16
16
|
return { exe, args };
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Build the `error` outcome for a spawn-time failure (as opposed to a
|
|
20
|
+
* failure after the process started). `ENOENT` — the binary is not
|
|
21
|
+
* resolvable at all — gets the "not found on PATH" wording the doctor
|
|
22
|
+
* summary and PATH-shadow hint (added by the doctor layer) key off of;
|
|
23
|
+
* any other errno (e.g. `EACCES`) still reports gracefully but without
|
|
24
|
+
* implying a PATH problem.
|
|
25
|
+
*/
|
|
26
|
+
function spawnErrorOutcome(exe, err, latencyMs) {
|
|
27
|
+
if (err?.code === "ENOENT") {
|
|
28
|
+
return {
|
|
29
|
+
kind: "error",
|
|
30
|
+
latencyMs,
|
|
31
|
+
message: `${exe} not found on PATH (spawn ENOENT)`,
|
|
32
|
+
enoent: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
kind: "error",
|
|
37
|
+
latencyMs,
|
|
38
|
+
message: `spawn ${exe} failed: ${err?.message ?? "unknown spawn error"}`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
18
41
|
async function runRealProbe(server, opts) {
|
|
19
42
|
if (server.enabled === false) {
|
|
20
43
|
return { name: server.name, outcome: { kind: "disabled" } };
|
|
@@ -49,6 +72,18 @@ async function runRealProbe(server, opts) {
|
|
|
49
72
|
let processExited = false;
|
|
50
73
|
let exitCode = null;
|
|
51
74
|
let exitSignal = null;
|
|
75
|
+
// A declared MCP whose binary cannot be resolved (spawn ENOENT, or any
|
|
76
|
+
// other spawn-time failure such as EACCES) emits an 'error' event on
|
|
77
|
+
// `child` ASYNCHRONOUSLY, not a synchronous throw from `spawn()` above.
|
|
78
|
+
// Node's EventEmitter treats an 'error' event with zero listeners as an
|
|
79
|
+
// unhandled exception that crashes the whole process — this is exactly
|
|
80
|
+
// the "spawn grounding-mcp ENOENT ... emitted 'error' event on
|
|
81
|
+
// ChildProcess" dogfood crash (harness/7f8fb4bc). Listening here turns
|
|
82
|
+
// it into a normal, per-server doctor outcome instead.
|
|
83
|
+
let spawnErr = null;
|
|
84
|
+
child.on("error", (err) => {
|
|
85
|
+
spawnErr = err;
|
|
86
|
+
});
|
|
52
87
|
child.stderr.on("data", (chunk) => {
|
|
53
88
|
stderrBuf += chunk.toString("utf8");
|
|
54
89
|
});
|
|
@@ -105,6 +140,14 @@ async function runRealProbe(server, opts) {
|
|
|
105
140
|
child.once("exit", () => resolve("exit"));
|
|
106
141
|
});
|
|
107
142
|
}
|
|
143
|
+
function waitForSpawnError() {
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
if (spawnErr)
|
|
146
|
+
resolve("spawn-error");
|
|
147
|
+
else
|
|
148
|
+
child.once("error", () => resolve("spawn-error"));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
108
151
|
const timers = new Set();
|
|
109
152
|
function timeoutPromise() {
|
|
110
153
|
return new Promise((resolve) => {
|
|
@@ -130,7 +173,14 @@ async function runRealProbe(server, opts) {
|
|
|
130
173
|
}),
|
|
131
174
|
waitForExit(),
|
|
132
175
|
timeoutPromise(),
|
|
176
|
+
waitForSpawnError(),
|
|
133
177
|
]);
|
|
178
|
+
if (initResult === "spawn-error") {
|
|
179
|
+
return {
|
|
180
|
+
name: server.name,
|
|
181
|
+
outcome: spawnErrorOutcome(exe, spawnErr, Date.now() - start),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
134
184
|
if (initResult === "exit") {
|
|
135
185
|
const latencyMs = Date.now() - start;
|
|
136
186
|
// Clean exit (code 0) before responding looks like a config issue
|
|
@@ -179,7 +229,14 @@ async function runRealProbe(server, opts) {
|
|
|
179
229
|
}),
|
|
180
230
|
waitForExit(),
|
|
181
231
|
timeoutPromise(),
|
|
232
|
+
waitForSpawnError(),
|
|
182
233
|
]);
|
|
234
|
+
if (verbResult === "spawn-error") {
|
|
235
|
+
return {
|
|
236
|
+
name: server.name,
|
|
237
|
+
outcome: spawnErrorOutcome(exe, spawnErr, Date.now() - start),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
183
240
|
if (verbResult === "exit") {
|
|
184
241
|
const latencyMs = Date.now() - start;
|
|
185
242
|
if (exitCode === 0) {
|
package/dist/probes/mcp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../src/probes/mcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAuC,MAAM,oBAAoB,CAAC;AAEhF,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../src/probes/mcp.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Versioned algorithm tag, so a future re-key scheme can reject mismatches explicitly rather than guess. */
|
|
2
|
+
export declare const SIGNING_ALG = "hmac-sha256-v1";
|
|
3
|
+
/** Basename of the signing-key file, a sibling of `.approvals/` under `generatedDir`. */
|
|
4
|
+
export declare const SIGNING_KEY_BASENAME = ".approval-signing.key";
|
|
5
|
+
/** Filesystem path of the signing key for a given `generatedDir`. */
|
|
6
|
+
export declare function signingKeyPathFor(generatedDir: string): string;
|
|
7
|
+
export interface SigningKeyHandle {
|
|
8
|
+
key: Buffer;
|
|
9
|
+
filePath: string;
|
|
10
|
+
/** True when this call generated a fresh key (first use / post-rotation). */
|
|
11
|
+
created: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Read the signing key, generating one (0600, `crypto.randomBytes(32)`) on
|
|
15
|
+
* first use. Race-safe-ish: a concurrent create loses the `wx` (exclusive
|
|
16
|
+
* create) race and falls back to reading whatever the winner wrote, rather
|
|
17
|
+
* than throwing or clobbering it. A key file shorter than `KEY_BYTES`
|
|
18
|
+
* (truncated / corrupted) is treated as absent and regenerated, since a
|
|
19
|
+
* short key would only weaken every future signature.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getOrCreateSigningKey(generatedDir: string): SigningKeyHandle;
|
|
22
|
+
/**
|
|
23
|
+
* Force a fresh key, overwriting any existing one. Every marker signed
|
|
24
|
+
* under the old key stops verifying immediately (see module doc). Not
|
|
25
|
+
* currently wired to a CLI verb (documented as "delete the file" for v1);
|
|
26
|
+
* exported so a future `harness approve rotate-key`-style verb, or a test,
|
|
27
|
+
* can call it directly without re-deriving the path convention.
|
|
28
|
+
*/
|
|
29
|
+
export declare function rotateSigningKey(generatedDir: string): SigningKeyHandle;
|
|
30
|
+
/** sha256 hex digest of a string, used to bind a marker to a persisted report's content. */
|
|
31
|
+
export declare function sha256Hex(content: string): string;
|
|
32
|
+
export interface SignedMarkerFields {
|
|
33
|
+
approvedAt: string;
|
|
34
|
+
approvedBy: string;
|
|
35
|
+
/** sha256 hex of the persisted-report content this approval is bound to; null when no report exists to bind (e.g. branch-protection, or a ledger-only approval). */
|
|
36
|
+
reportContentHash: string | null;
|
|
37
|
+
alg: string;
|
|
38
|
+
signature: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Sign a marker's fields for `markerId` (the marker's lookup key — a raw
|
|
42
|
+
* sessionId for the understanding-gate session marker, `task-<id>` for a
|
|
43
|
+
* task-scoped marker, `branch-protection-<sessionId>` for the
|
|
44
|
+
* branch-protection twin). Binding `markerId` into the signed payload is
|
|
45
|
+
* what stops a validly-signed marker from being copied/renamed onto a
|
|
46
|
+
* DIFFERENT id and still verifying — verification always recomputes the
|
|
47
|
+
* HMAC using the id the caller is checking, not one read back out of the
|
|
48
|
+
* marker body.
|
|
49
|
+
*/
|
|
50
|
+
export declare function signMarker(generatedDir: string, markerId: string, marker: {
|
|
51
|
+
approvedAt: string;
|
|
52
|
+
approvedBy: string;
|
|
53
|
+
reportContentHash?: string | null;
|
|
54
|
+
}): SignedMarkerFields;
|
|
55
|
+
export type SignatureVerification = {
|
|
56
|
+
ok: true;
|
|
57
|
+
} | {
|
|
58
|
+
ok: false;
|
|
59
|
+
reason: string;
|
|
60
|
+
/**
|
|
61
|
+
* `"key-unavailable"` when verification could not even run because
|
|
62
|
+
* obtaining the signing key itself failed (permission error, disk
|
|
63
|
+
* issue) — a fail-closed I/O problem, not evidence of tampering.
|
|
64
|
+
* Callers (`checkApprovalMarker`) use this to classify the outcome
|
|
65
|
+
* distinctly from an actual forged/invalid signature (review LOW 1,
|
|
66
|
+
* harness/f9485cc7): a broken key file must not read as an active
|
|
67
|
+
* forgery attempt in audit output. Omitted for every other failure
|
|
68
|
+
* (missing field, wrong alg, bad signature).
|
|
69
|
+
*/
|
|
70
|
+
kind?: "key-unavailable";
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Verify a parsed marker JSON body against `markerId`. Never throws: any
|
|
74
|
+
* failure to obtain the signing key (permission error, disk issue) is
|
|
75
|
+
* treated as a verification failure, not an exception — the gate-side
|
|
76
|
+
* caller must fail closed (not approved) rather than crash the hook
|
|
77
|
+
* process, which the surrounding PreToolUse blocker would otherwise turn
|
|
78
|
+
* into an uncontrolled failure mode. That specific failure mode is tagged
|
|
79
|
+
* `kind: "key-unavailable"` so the caller can tell "we couldn't even check"
|
|
80
|
+
* apart from "we checked and it's invalid".
|
|
81
|
+
*/
|
|
82
|
+
export declare function verifyMarkerSignature(generatedDir: string, markerId: string, payload: Record<string, unknown>): SignatureVerification;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// harness/f9485cc7 — HMAC signing for Understanding Gate / branch-protection
|
|
2
|
+
// approval markers.
|
|
3
|
+
//
|
|
4
|
+
// BACKGROUND: the marker at `harness.generated/.approvals/<id>` is the
|
|
5
|
+
// canonical "operator said go" signal (agent-tasks/88ca4bb3). Its integrity
|
|
6
|
+
// has rested entirely on an UNENFORCED invariant: "no configured MCP exposes
|
|
7
|
+
// a filesystem-write primitive" (understanding-before-execution.md
|
|
8
|
+
// "Approval state"). Edit / Write / Bash are gated by the same PreToolUse
|
|
9
|
+
// hook this marker satisfies, so today the agent has no path to WRITE the
|
|
10
|
+
// marker file. But nothing technical stops a future MCP tool with local
|
|
11
|
+
// file-write from silently reopening that hole: its tool name would not
|
|
12
|
+
// match the pack's `Edit|Write|Bash` blocker matcher, so a write through it
|
|
13
|
+
// would land un-gated. Before this module, the marker's contract was
|
|
14
|
+
// "existence is enough" (see the removed `checkApprovalMarker` comment) —
|
|
15
|
+
// exactly the shape a forger only needs a bare filesystem-write for. This
|
|
16
|
+
// module makes forging also require the operator-side HMAC key, so a marker
|
|
17
|
+
// planted through any write-primitive the blocker does not enumerate still
|
|
18
|
+
// fails the gate unless it carries a valid signature.
|
|
19
|
+
//
|
|
20
|
+
// HONEST TRUST MODEL (read this before assuming more than it delivers):
|
|
21
|
+
//
|
|
22
|
+
// - This does NOT create a read boundary. The `understanding-before-
|
|
23
|
+
// execution` PreToolUse blocker matches tool names `Edit|Write|Bash`
|
|
24
|
+
// only — it does not gate `Read`, and no permission profile in this
|
|
25
|
+
// repo scopes `Read` away from any path (see
|
|
26
|
+
// `src/policy-packs/permission-translator.ts`: `read` maps to
|
|
27
|
+
// `["Read", "Glob", "Grep"]` with `allow:true` in every v1 profile).
|
|
28
|
+
// So ANY file on local disk — including the signing key, wherever it
|
|
29
|
+
// lives — is readable by the ordinary, always-available `Read` tool,
|
|
30
|
+
// approved or not. Storing the key outside `harness.generated/` would
|
|
31
|
+
// not change this; the write barrier is per-tool-name, not per-path.
|
|
32
|
+
// - The residual this leaves: an agent (or anything running as the same
|
|
33
|
+
// OS user) that can (a) read arbitrary local files — true today, by
|
|
34
|
+
// design, via `Read` — AND (b) write to `harness.generated/.approvals/`
|
|
35
|
+
// through some primitive NOT covered by the `Edit|Write|Bash` blocker
|
|
36
|
+
// matcher, CAN forge a validly-signed marker: it reads the key with
|
|
37
|
+
// Read, computes the HMAC itself, and writes the result through the
|
|
38
|
+
// uncovered write path. Signing does not close this combined case; it
|
|
39
|
+
// closes the narrower (and, before this, the ONLY defended) case where
|
|
40
|
+
// the forger has a write primitive but no matching ability to also
|
|
41
|
+
// recompute a valid signature for it (e.g. a narrow single-purpose
|
|
42
|
+
// write channel, or a human/script that copies bytes without reading
|
|
43
|
+
// back the operator's key file), and it makes ANY forgery attempt one
|
|
44
|
+
// step more expensive and forensically visible (a key read is a
|
|
45
|
+
// distinguishable event; a bare unsigned-JSON write no longer is).
|
|
46
|
+
// - Given the agent and operator share one OS user account on this
|
|
47
|
+
// machine, this is deliberately pragmatic defense-in-depth, not a hard
|
|
48
|
+
// authorization boundary. It is documented here in full rather than
|
|
49
|
+
// oversold.
|
|
50
|
+
//
|
|
51
|
+
// KEY STORAGE: `<generatedDir>/.approval-signing.key` — a raw 32-byte
|
|
52
|
+
// secret, mode 0600, generated lazily on first use (by `harness approve
|
|
53
|
+
// understanding` / `harness approve branch-protection`, or by `harness
|
|
54
|
+
// init` — see src/cli/init/index.ts). Colocated with `harness.generated/`
|
|
55
|
+
// (not a separate home-dir path) so:
|
|
56
|
+
// - it inherits the SAME test-isolation guarantee every marker/report
|
|
57
|
+
// path already has (every call site here already threads an explicit,
|
|
58
|
+
// test-injectable `generatedDir` — no new parameter, no new way to
|
|
59
|
+
// leak into a real `~/.harness/` during a test run);
|
|
60
|
+
// - it is gitignored by the SAME existing convention as the rest of
|
|
61
|
+
// `harness.generated/` (docs/ARCHITECTURE.md: "harness.generated/ ...
|
|
62
|
+
// is .gitignore'd", unlike `harness.yaml` itself, which operators
|
|
63
|
+
// often check into a dotfiles repo) — a home-dir-anchored path could
|
|
64
|
+
// land inside a version-controlled directory when the operator
|
|
65
|
+
// resolves `harness.yaml` via `--config` into a repo tree; this can't;
|
|
66
|
+
// - `harness apply` never wipes it (apply only ever writes its own known
|
|
67
|
+
// files under `harness.generated/`; unknown siblings survive
|
|
68
|
+
// byte-for-byte — the same guarantee `.approvals/` and
|
|
69
|
+
// `.pending-approval` already rely on).
|
|
70
|
+
// `harness uninstall` DOES remove it (it rm -rf's the whole
|
|
71
|
+
// `generatedDir`) — a deliberate, rare, operator-initiated teardown, so
|
|
72
|
+
// losing the key (and therefore invalidating every marker it signed) is
|
|
73
|
+
// an acceptable side effect, not a silent regression.
|
|
74
|
+
//
|
|
75
|
+
// ROTATION: delete `<generatedDir>/.approval-signing.key` (or call
|
|
76
|
+
// `rotateSigningKey`). The next `writeApprovalMarker` /
|
|
77
|
+
// `writeBranchProtectionMarker` / any signature check lazily regenerates
|
|
78
|
+
// it. Every marker signed under the OLD key immediately fails verification
|
|
79
|
+
// (forged:true) and the gate demands re-approval — this is the intended,
|
|
80
|
+
// documented blast radius of a rotation, not a bug.
|
|
81
|
+
//
|
|
82
|
+
// BACK-COMPAT: strict, no migration window. A marker written before this
|
|
83
|
+
// feature shipped (or hand-written without the key) carries no `signature`
|
|
84
|
+
// field and is REJECTED (see `verifyMarkerSignature`), exactly like a
|
|
85
|
+
// forgery. Operator impact: upgrading harness on a machine with a live,
|
|
86
|
+
// previously-approved session invalidates that approval; the operator runs
|
|
87
|
+
// `harness approve understanding` (and/or `harness approve
|
|
88
|
+
// branch-protection`) once more. Chosen over a grace-period migration
|
|
89
|
+
// because a migration window is, by construction, a window where the
|
|
90
|
+
// exact vulnerability this task closes (an unsigned marker satisfies the
|
|
91
|
+
// gate) still holds.
|
|
92
|
+
import * as crypto from "node:crypto";
|
|
93
|
+
import * as fs from "node:fs";
|
|
94
|
+
import * as path from "node:path";
|
|
95
|
+
/** Versioned algorithm tag, so a future re-key scheme can reject mismatches explicitly rather than guess. */
|
|
96
|
+
export const SIGNING_ALG = "hmac-sha256-v1";
|
|
97
|
+
/** Basename of the signing-key file, a sibling of `.approvals/` under `generatedDir`. */
|
|
98
|
+
export const SIGNING_KEY_BASENAME = ".approval-signing.key";
|
|
99
|
+
const KEY_BYTES = 32;
|
|
100
|
+
/** Filesystem path of the signing key for a given `generatedDir`. */
|
|
101
|
+
export function signingKeyPathFor(generatedDir) {
|
|
102
|
+
return path.join(generatedDir, SIGNING_KEY_BASENAME);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Read the signing key, generating one (0600, `crypto.randomBytes(32)`) on
|
|
106
|
+
* first use. Race-safe-ish: a concurrent create loses the `wx` (exclusive
|
|
107
|
+
* create) race and falls back to reading whatever the winner wrote, rather
|
|
108
|
+
* than throwing or clobbering it. A key file shorter than `KEY_BYTES`
|
|
109
|
+
* (truncated / corrupted) is treated as absent and regenerated, since a
|
|
110
|
+
* short key would only weaken every future signature.
|
|
111
|
+
*/
|
|
112
|
+
export function getOrCreateSigningKey(generatedDir) {
|
|
113
|
+
const filePath = signingKeyPathFor(generatedDir);
|
|
114
|
+
fs.mkdirSync(generatedDir, { recursive: true });
|
|
115
|
+
let fileExisted = false;
|
|
116
|
+
try {
|
|
117
|
+
const existing = fs.readFileSync(filePath);
|
|
118
|
+
fileExisted = true;
|
|
119
|
+
if (existing.length >= KEY_BYTES) {
|
|
120
|
+
return { key: existing, filePath, created: false };
|
|
121
|
+
}
|
|
122
|
+
// Falls through: truncated/corrupt key file, regenerate below. `flag:
|
|
123
|
+
// "w"` (not "wx") is used below precisely BECAUSE the file already
|
|
124
|
+
// exists here — an exclusive create would collide with it and, on
|
|
125
|
+
// EEXIST, re-read the SAME truncated bytes back, silently failing to
|
|
126
|
+
// ever repair a corrupt key.
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (err.code !== "ENOENT")
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
const fresh = crypto.randomBytes(KEY_BYTES);
|
|
133
|
+
if (fileExisted) {
|
|
134
|
+
// Known-corrupt/truncated file: overwrite unconditionally. No
|
|
135
|
+
// meaningful create-race to defend against — the file is already
|
|
136
|
+
// known bad, so unconditionally replacing it can only improve on
|
|
137
|
+
// the previous (unusable) contents.
|
|
138
|
+
fs.writeFileSync(filePath, fresh, { mode: 0o600 });
|
|
139
|
+
try {
|
|
140
|
+
fs.chmodSync(filePath, 0o600);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
/* best-effort */
|
|
144
|
+
}
|
|
145
|
+
return { key: fresh, filePath, created: true };
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
fs.writeFileSync(filePath, fresh, { mode: 0o600, flag: "wx" });
|
|
149
|
+
return { key: fresh, filePath, created: true };
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
if (err.code === "EEXIST") {
|
|
153
|
+
// Lost the create race to a concurrent caller; use what they wrote.
|
|
154
|
+
return { key: fs.readFileSync(filePath), filePath, created: false };
|
|
155
|
+
}
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Force a fresh key, overwriting any existing one. Every marker signed
|
|
161
|
+
* under the old key stops verifying immediately (see module doc). Not
|
|
162
|
+
* currently wired to a CLI verb (documented as "delete the file" for v1);
|
|
163
|
+
* exported so a future `harness approve rotate-key`-style verb, or a test,
|
|
164
|
+
* can call it directly without re-deriving the path convention.
|
|
165
|
+
*/
|
|
166
|
+
export function rotateSigningKey(generatedDir) {
|
|
167
|
+
const filePath = signingKeyPathFor(generatedDir);
|
|
168
|
+
fs.mkdirSync(generatedDir, { recursive: true });
|
|
169
|
+
const fresh = crypto.randomBytes(KEY_BYTES);
|
|
170
|
+
fs.writeFileSync(filePath, fresh, { mode: 0o600 });
|
|
171
|
+
try {
|
|
172
|
+
fs.chmodSync(filePath, 0o600);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
/* best-effort on platforms where chmod is a no-op (e.g. some Windows fs) */
|
|
176
|
+
}
|
|
177
|
+
return { key: fresh, filePath, created: true };
|
|
178
|
+
}
|
|
179
|
+
/** sha256 hex digest of a string, used to bind a marker to a persisted report's content. */
|
|
180
|
+
export function sha256Hex(content) {
|
|
181
|
+
return crypto.createHash("sha256").update(content, "utf8").digest("hex");
|
|
182
|
+
}
|
|
183
|
+
function canonicalPayload(markerId, approvedAt, approvedBy, reportContentHash) {
|
|
184
|
+
// Fixed key order + JSON string-escaping makes this injective: there is
|
|
185
|
+
// no (markerId, approvedAt, approvedBy, reportContentHash) tuple that
|
|
186
|
+
// collides with a different tuple's encoding, unlike naive delimiter
|
|
187
|
+
// concatenation (e.g. "a"+"bc" vs "ab"+"c").
|
|
188
|
+
return JSON.stringify({ markerId, approvedAt, approvedBy, reportContentHash });
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Sign a marker's fields for `markerId` (the marker's lookup key — a raw
|
|
192
|
+
* sessionId for the understanding-gate session marker, `task-<id>` for a
|
|
193
|
+
* task-scoped marker, `branch-protection-<sessionId>` for the
|
|
194
|
+
* branch-protection twin). Binding `markerId` into the signed payload is
|
|
195
|
+
* what stops a validly-signed marker from being copied/renamed onto a
|
|
196
|
+
* DIFFERENT id and still verifying — verification always recomputes the
|
|
197
|
+
* HMAC using the id the caller is checking, not one read back out of the
|
|
198
|
+
* marker body.
|
|
199
|
+
*/
|
|
200
|
+
export function signMarker(generatedDir, markerId, marker) {
|
|
201
|
+
const { key } = getOrCreateSigningKey(generatedDir);
|
|
202
|
+
const reportContentHash = marker.reportContentHash ?? null;
|
|
203
|
+
const signature = crypto
|
|
204
|
+
.createHmac("sha256", key)
|
|
205
|
+
.update(canonicalPayload(markerId, marker.approvedAt, marker.approvedBy, reportContentHash))
|
|
206
|
+
.digest("hex");
|
|
207
|
+
return {
|
|
208
|
+
approvedAt: marker.approvedAt,
|
|
209
|
+
approvedBy: marker.approvedBy,
|
|
210
|
+
reportContentHash,
|
|
211
|
+
alg: SIGNING_ALG,
|
|
212
|
+
signature,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Verify a parsed marker JSON body against `markerId`. Never throws: any
|
|
217
|
+
* failure to obtain the signing key (permission error, disk issue) is
|
|
218
|
+
* treated as a verification failure, not an exception — the gate-side
|
|
219
|
+
* caller must fail closed (not approved) rather than crash the hook
|
|
220
|
+
* process, which the surrounding PreToolUse blocker would otherwise turn
|
|
221
|
+
* into an uncontrolled failure mode. That specific failure mode is tagged
|
|
222
|
+
* `kind: "key-unavailable"` so the caller can tell "we couldn't even check"
|
|
223
|
+
* apart from "we checked and it's invalid".
|
|
224
|
+
*/
|
|
225
|
+
export function verifyMarkerSignature(generatedDir, markerId, payload) {
|
|
226
|
+
const approvedAt = payload["approvedAt"];
|
|
227
|
+
const approvedBy = payload["approvedBy"];
|
|
228
|
+
const signature = payload["signature"];
|
|
229
|
+
const alg = payload["alg"];
|
|
230
|
+
if (typeof approvedAt !== "string" || approvedAt.length === 0) {
|
|
231
|
+
return { ok: false, reason: "missing approvedAt" };
|
|
232
|
+
}
|
|
233
|
+
if (typeof approvedBy !== "string" || approvedBy.length === 0) {
|
|
234
|
+
return { ok: false, reason: "missing approvedBy" };
|
|
235
|
+
}
|
|
236
|
+
if (typeof signature !== "string" || signature.length === 0) {
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
reason: "missing signature (legacy pre-signing marker, or forged file)",
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (alg !== SIGNING_ALG) {
|
|
243
|
+
return { ok: false, reason: `unrecognized or missing alg (got ${JSON.stringify(alg)})` };
|
|
244
|
+
}
|
|
245
|
+
const reportContentHash = typeof payload["reportContentHash"] === "string"
|
|
246
|
+
? payload["reportContentHash"]
|
|
247
|
+
: null;
|
|
248
|
+
let key;
|
|
249
|
+
try {
|
|
250
|
+
({ key } = getOrCreateSigningKey(generatedDir));
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
reason: `signing key unavailable (${err instanceof Error ? err.message : String(err)})`,
|
|
256
|
+
kind: "key-unavailable",
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const expected = crypto
|
|
260
|
+
.createHmac("sha256", key)
|
|
261
|
+
.update(canonicalPayload(markerId, approvedAt, approvedBy, reportContentHash))
|
|
262
|
+
.digest();
|
|
263
|
+
// No try/catch around Buffer.from(signature, "hex"): unlike JSON.parse,
|
|
264
|
+
// Buffer.from with a "hex" encoding never throws on malformed input — it
|
|
265
|
+
// silently stops decoding at the first invalid pair and returns whatever
|
|
266
|
+
// it managed to decode so far (review LOW 3). The length-mismatch check
|
|
267
|
+
// below is what actually rejects a truncated/malformed decode; relying on
|
|
268
|
+
// an exception here would have been dead code.
|
|
269
|
+
const actual = Buffer.from(signature, "hex");
|
|
270
|
+
if (actual.length !== expected.length || !crypto.timingSafeEqual(actual, expected)) {
|
|
271
|
+
return { ok: false, reason: "signature verification failed (tampered or forged)" };
|
|
272
|
+
}
|
|
273
|
+
return { ok: true };
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=approval-signing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-signing.js","sourceRoot":"","sources":["../../src/runtime/approval-signing.ts"],"names":[],"mappings":"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"}
|