@scopebond/hook 0.8.1 → 0.9.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.
Files changed (102) hide show
  1. package/README.md +210 -0
  2. package/dist/budget-load.d.ts +68 -0
  3. package/dist/budget-load.d.ts.map +1 -0
  4. package/dist/budget-load.js +164 -0
  5. package/dist/budget-load.js.map +1 -0
  6. package/dist/capabilities.d.ts +103 -0
  7. package/dist/capabilities.d.ts.map +1 -0
  8. package/dist/capabilities.js +212 -0
  9. package/dist/capabilities.js.map +1 -0
  10. package/dist/classify.d.ts +17 -0
  11. package/dist/classify.d.ts.map +1 -0
  12. package/dist/classify.js +78 -0
  13. package/dist/classify.js.map +1 -0
  14. package/dist/cli.d.ts.map +1 -1
  15. package/dist/cli.js +523 -13
  16. package/dist/cli.js.map +1 -1
  17. package/dist/cloud.d.ts +8 -0
  18. package/dist/cloud.d.ts.map +1 -1
  19. package/dist/cloud.js.map +1 -1
  20. package/dist/dispatch-cli.d.ts +2 -0
  21. package/dist/dispatch-cli.d.ts.map +1 -0
  22. package/dist/dispatch-cli.js +128 -0
  23. package/dist/dispatch-cli.js.map +1 -0
  24. package/dist/explain.d.ts.map +1 -1
  25. package/dist/explain.js +2 -1
  26. package/dist/explain.js.map +1 -1
  27. package/dist/group.d.ts +12 -0
  28. package/dist/group.d.ts.map +1 -0
  29. package/dist/group.js +55 -0
  30. package/dist/group.js.map +1 -0
  31. package/dist/index.d.ts +25 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +17 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/install.d.ts +10 -0
  36. package/dist/install.d.ts.map +1 -1
  37. package/dist/install.js +56 -0
  38. package/dist/install.js.map +1 -1
  39. package/dist/map.d.ts +4 -0
  40. package/dist/map.d.ts.map +1 -1
  41. package/dist/map.js +29 -1
  42. package/dist/map.js.map +1 -1
  43. package/dist/obs-emitter.d.ts +132 -0
  44. package/dist/obs-emitter.d.ts.map +1 -0
  45. package/dist/obs-emitter.js +415 -0
  46. package/dist/obs-emitter.js.map +1 -0
  47. package/dist/obs-store.d.ts +160 -0
  48. package/dist/obs-store.d.ts.map +1 -0
  49. package/dist/obs-store.js +367 -0
  50. package/dist/obs-store.js.map +1 -0
  51. package/dist/obs-upload.d.ts +24 -0
  52. package/dist/obs-upload.d.ts.map +1 -0
  53. package/dist/obs-upload.js +231 -0
  54. package/dist/obs-upload.js.map +1 -0
  55. package/dist/observation.d.ts +232 -0
  56. package/dist/observation.d.ts.map +1 -0
  57. package/dist/observation.js +308 -0
  58. package/dist/observation.js.map +1 -0
  59. package/dist/paths.d.ts +15 -0
  60. package/dist/paths.d.ts.map +1 -0
  61. package/dist/paths.js +79 -0
  62. package/dist/paths.js.map +1 -0
  63. package/dist/policy-load.d.ts +60 -0
  64. package/dist/policy-load.d.ts.map +1 -0
  65. package/dist/policy-load.js +151 -0
  66. package/dist/policy-load.js.map +1 -0
  67. package/dist/proof.d.ts +30 -0
  68. package/dist/proof.d.ts.map +1 -0
  69. package/dist/proof.js +186 -0
  70. package/dist/proof.js.map +1 -0
  71. package/dist/rules.d.ts +20 -0
  72. package/dist/rules.d.ts.map +1 -1
  73. package/dist/rules.js +50 -0
  74. package/dist/rules.js.map +1 -1
  75. package/dist/runtime.d.ts +18 -1
  76. package/dist/runtime.d.ts.map +1 -1
  77. package/dist/runtime.js +74 -10
  78. package/dist/runtime.js.map +1 -1
  79. package/dist/scan.d.ts +7 -0
  80. package/dist/scan.d.ts.map +1 -0
  81. package/dist/scan.js +38 -0
  82. package/dist/scan.js.map +1 -0
  83. package/dist/shell.d.ts.map +1 -1
  84. package/dist/shell.js +12 -3
  85. package/dist/shell.js.map +1 -1
  86. package/dist/sql-classify.d.ts +11 -0
  87. package/dist/sql-classify.d.ts.map +1 -0
  88. package/dist/sql-classify.js +251 -0
  89. package/dist/sql-classify.js.map +1 -0
  90. package/dist/typed-infra.d.ts +73 -0
  91. package/dist/typed-infra.d.ts.map +1 -0
  92. package/dist/typed-infra.js +758 -0
  93. package/dist/typed-infra.js.map +1 -0
  94. package/dist/typed-ops.d.ts +140 -0
  95. package/dist/typed-ops.d.ts.map +1 -0
  96. package/dist/typed-ops.js +549 -0
  97. package/dist/typed-ops.js.map +1 -0
  98. package/dist/vectors.d.ts +40 -0
  99. package/dist/vectors.d.ts.map +1 -0
  100. package/dist/vectors.js +219 -0
  101. package/dist/vectors.js.map +1 -0
  102. package/package.json +4 -4
@@ -0,0 +1,151 @@
1
+ // Load a policy exported from a Scopebond Cloud workspace, and say honestly what happened.
2
+ //
3
+ // An export is a reviewed policy in a file. Nothing is enforcing it until a hook loads it, and
4
+ // the workspace shows it as "pending" until a signed acknowledgement echoes exactly what the
5
+ // export said: its policy digest, its policy id and version, its scope digest and the export
6
+ // id. This module checks an export and, when asked, installs it as the active policy; the
7
+ // caller then queues the acknowledgement (`ObservationEmitter.policyAck`).
8
+ //
9
+ // What is checked, and what is not:
10
+ // - the export's own integrity: its `policy_hash` must be the SHA-256 of the canonical policy
11
+ // it carries, and its `scope.scope_digest` must be the SHA-256 of
12
+ // `scopebond:policy-scope/v1\n` + the canonical `{agent_id, environment_id, export_id}`;
13
+ // - that it is for this workspace environment (when this machine is connected);
14
+ // - that the gateway can build a runtime from it.
15
+ // An export carries no signature of its own, so this cannot prove who wrote it: get the file
16
+ // from your workspace, over the workspace's own page or API.
17
+ //
18
+ // Loading replaces the active policy. The previous file is kept beside it, and the write is
19
+ // atomic (a temp file in the same directory, then a rename), so an interrupted load leaves the
20
+ // old policy in place. Nothing here touches an agent's settings.
21
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
22
+ import { tmpdir } from "node:os";
23
+ import { join } from "node:path";
24
+ import { createHash, randomBytes } from "node:crypto";
25
+ import { canonical } from "@scopebond/policy-schema/canonical";
26
+ import { scaffold } from "./init.js";
27
+ import { createHookRuntime } from "./runtime.js";
28
+ import { digestPolicy } from "./observation.js";
29
+ export const POLICY_SCOPE_DOMAIN = "scopebond:policy-scope/v1\n";
30
+ export const MAX_EXPORT_BYTES = 1024 * 1024;
31
+ export const LOADED_POLICY_FILE = "loaded-policy.json";
32
+ export const PREVIOUS_POLICY_FILE = "policy.previous.json";
33
+ /** SHA-256 of the domain plus the canonical scope an export was made for. */
34
+ export const policyScopeDigest = (scope) => createHash("sha256").update(POLICY_SCOPE_DOMAIN + canonical({ agent_id: scope.agent_id, environment_id: scope.environment_id, export_id: scope.export_id }), "utf8").digest("hex");
35
+ const HEX64 = /^[0-9a-f]{64}$/;
36
+ const OPAQUE = /^[\x21-\x7e]{1,200}$/;
37
+ const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
38
+ /** Accept the export itself, or the workspace's `{ id, configuration }` answer around it. */
39
+ export function unwrapExport(raw) {
40
+ if (!isObject(raw))
41
+ return null;
42
+ if (raw.type === undefined && isObject(raw.configuration))
43
+ return raw.configuration;
44
+ return raw;
45
+ }
46
+ /** Check an export without reading or changing anything on disk. Pure. */
47
+ export function inspectExport(raw, context = {}) {
48
+ const exp = unwrapExport(raw);
49
+ if (!exp || exp.type !== "scopebond:reviewed-policy-export")
50
+ return { ok: false, error: "schema_invalid", message: "this is not a Scopebond policy export" };
51
+ if (exp.version !== 1)
52
+ return { ok: false, error: "unsupported", message: `unsupported export version ${String(exp.version)}` };
53
+ const scope = exp.scope;
54
+ const policy = exp.policy;
55
+ if (!isObject(scope) || !isObject(policy) || typeof exp.policy_hash !== "string" || !HEX64.test(exp.policy_hash)) {
56
+ return { ok: false, error: "schema_invalid", message: "the export lacks a policy, a policy hash or a scope block (an export made before scope blocks existed cannot be acknowledged; export it again from the workspace)" };
57
+ }
58
+ const { export_id: exportId, policy_id: policyId, policy_version: policyVersion, agent_id: agentId, environment_id: environmentId, scope_digest: scopeDigest } = scope;
59
+ if (typeof exportId !== "string" || !OPAQUE.test(exportId) || typeof policyId !== "string" || !OPAQUE.test(policyId)
60
+ || !Number.isInteger(policyVersion) || policyVersion < 1 || policyVersion > 2_147_483_647
61
+ || typeof agentId !== "string" || !OPAQUE.test(agentId) || typeof environmentId !== "string" || !OPAQUE.test(environmentId)
62
+ || typeof scopeDigest !== "string" || !HEX64.test(scopeDigest)) {
63
+ return { ok: false, error: "schema_invalid", message: "the export's scope block is incomplete or malformed" };
64
+ }
65
+ // The values below are echoed exactly as the export states them, so a rejection still names the export.
66
+ const echo = { exportId, policyId, policyVersion: policyVersion, policyDigest: exp.policy_hash, scopeDigest };
67
+ if (digestPolicy(policy) !== exp.policy_hash) {
68
+ return { ok: false, error: "signature_invalid", message: "the policy does not match the export's policy hash; the file was changed or damaged", ack: echo };
69
+ }
70
+ if (policyScopeDigest({ export_id: exportId, agent_id: agentId, environment_id: environmentId }) !== scopeDigest) {
71
+ return { ok: false, error: "scope_mismatch", message: "the export's scope digest does not match its export, agent and environment", ack: echo };
72
+ }
73
+ if (context.environmentId !== undefined && context.environmentId !== environmentId) {
74
+ return { ok: false, error: "scope_mismatch", message: "this export is for a different environment than this machine is connected to", ack: echo };
75
+ }
76
+ return { ok: true, facts: { ...echo, agentId, environmentId, policy } };
77
+ }
78
+ /** Whether the gateway can build a runtime from this policy, in a throwaway home. Never touches a real one. */
79
+ export function policyBuilds(policy) {
80
+ const dir = mkdtempSync(join(tmpdir(), "scopebond-policy-check-"));
81
+ let runtime;
82
+ try {
83
+ scaffold(dir);
84
+ writeFileSync(join(dir, "policy.json"), `${JSON.stringify(policy)}\n`);
85
+ runtime = createHookRuntime({ policyPath: join(dir, "policy.json"), keyPath: join(dir, "agent.key"), attesterPath: join(dir, "attester.key"), dbPath: join(dir, "receipts.db") });
86
+ return true;
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ finally {
92
+ try {
93
+ runtime?.close();
94
+ }
95
+ catch { /* removed next */ }
96
+ try {
97
+ rmSync(dir, { recursive: true, force: true, maxRetries: 5 });
98
+ }
99
+ catch { /* the OS temp dir is cleaned later */ }
100
+ }
101
+ }
102
+ /** Write a file atomically: a temp file in the same directory, then a rename. */
103
+ function writeAtomic(file, text) {
104
+ const temp = `${file}.${randomBytes(6).toString("hex")}.tmp`;
105
+ try {
106
+ writeFileSync(temp, text, { mode: 0o600 });
107
+ renameSync(temp, file);
108
+ }
109
+ catch (error) {
110
+ try {
111
+ rmSync(temp, { force: true });
112
+ }
113
+ catch { /* nothing to remove */ }
114
+ throw error;
115
+ }
116
+ }
117
+ /** Read an export file, check it, and (with `apply`) install it as the active policy in `dir`. */
118
+ export function loadPolicyExport(dir, file, options) {
119
+ let raw;
120
+ try {
121
+ if (statSync(file).size > MAX_EXPORT_BYTES)
122
+ return { state: "rejected", error: "schema_invalid", message: "the export file is larger than 1 MiB" };
123
+ raw = JSON.parse(readFileSync(file, "utf8"));
124
+ }
125
+ catch (error) {
126
+ return { state: "rejected", error: "schema_invalid", message: `the export could not be read as JSON (${error.name})` };
127
+ }
128
+ const inspected = inspectExport(raw, { environmentId: options.environmentId });
129
+ if (!inspected.ok)
130
+ return { state: "rejected", error: inspected.error, message: inspected.message, ...(inspected.ack ? { ack: inspected.ack } : {}) };
131
+ const { facts } = inspected;
132
+ if (!policyBuilds(facts.policy)) {
133
+ return { state: "rejected", error: "unsupported", message: "this gateway cannot load that policy", ack: { exportId: facts.exportId, policyId: facts.policyId, policyVersion: facts.policyVersion, policyDigest: facts.policyDigest, scopeDigest: facts.scopeDigest } };
134
+ }
135
+ if (!options.apply)
136
+ return { state: "would_load", facts };
137
+ mkdirSync(dir, { recursive: true });
138
+ const policyPath = join(dir, "policy.json");
139
+ let previous = null;
140
+ if (existsSync(policyPath)) {
141
+ previous = join(dir, PREVIOUS_POLICY_FILE);
142
+ copyFileSync(policyPath, previous);
143
+ }
144
+ writeAtomic(policyPath, `${JSON.stringify(facts.policy, null, 2)}\n`);
145
+ writeAtomic(join(dir, LOADED_POLICY_FILE), `${JSON.stringify({
146
+ export_id: facts.exportId, policy_id: facts.policyId, policy_version: facts.policyVersion, policy_digest: facts.policyDigest,
147
+ scope_digest: facts.scopeDigest, environment_id: facts.environmentId, agent_id: facts.agentId, loaded_at: new Date().toISOString(),
148
+ }, null, 2)}\n`);
149
+ return { state: "loaded", facts, policyPath, previous };
150
+ }
151
+ //# sourceMappingURL=policy-load.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-load.js","sourceRoot":"","sources":["../src/policy-load.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,EAAE;AACF,+FAA+F;AAC/F,6FAA6F;AAC7F,6FAA6F;AAC7F,0FAA0F;AAC1F,2EAA2E;AAC3E,EAAE;AACF,oCAAoC;AACpC,gGAAgG;AAChG,sEAAsE;AACtE,6FAA6F;AAC7F,kFAAkF;AAClF,oDAAoD;AACpD,6FAA6F;AAC7F,6DAA6D;AAC7D,EAAE;AACF,4FAA4F;AAC5F,+FAA+F;AAC/F,iEAAiE;AAEjE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACtI,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;AAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,YAAY,EAA6C,MAAM,kBAAkB,CAAC;AAE3F,MAAM,CAAC,MAAM,mBAAmB,GAAG,6BAA6B,CAAC;AACjE,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,GAAG,IAAI,CAAC;AAC5C,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AACvD,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAC;AAE3D,6EAA6E;AAC7E,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAAsE,EAAU,EAAE,CAClH,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,mBAAmB,GAAG,SAAS,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAW,CAAC,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAE9L,MAAM,KAAK,GAAG,gBAAgB,CAAC;AAC/B,MAAM,MAAM,GAAG,sBAAsB,CAAC;AAkBtC,MAAM,QAAQ,GAAG,CAAC,CAAU,EAAgC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAExH,6FAA6F;AAC7F,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAChC,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC;QAAE,OAAO,GAAG,CAAC,aAAa,CAAC;IACpF,OAAO,GAAG,CAAC;AACb,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,aAAa,CAAC,GAAY,EAAE,UAAsC,EAAE;IAClF,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,KAAK,kCAAkC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,uCAAuC,EAAE,CAAC;IAC7J,IAAI,GAAG,CAAC,OAAO,KAAK,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,8BAA8B,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;IAChI,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC;IACxB,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;IAC1B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,OAAO,GAAG,CAAC,WAAW,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;QACjH,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,mKAAmK,EAAE,CAAC;IAC9N,CAAC;IACD,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,cAAc,EAAE,aAAa,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,GAAG,KAAK,CAAC;IACvK,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;WAC/G,CAAC,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,IAAK,aAAwB,GAAG,CAAC,IAAK,aAAwB,GAAG,aAAa;WAC9G,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,OAAO,aAAa,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC;WACxH,OAAO,WAAW,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QACjE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,qDAAqD,EAAE,CAAC;IAChH,CAAC;IACD,wGAAwG;IACxG,MAAM,IAAI,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAuB,EAAE,YAAY,EAAE,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,CAAC;IACxH,IAAI,YAAY,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,WAAW,EAAE,CAAC;QAC7C,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,qFAAqF,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IAC9J,CAAC;IACD,IAAI,iBAAiB,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAC,KAAK,WAAW,EAAE,CAAC;QACjH,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,4EAA4E,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IAClJ,CAAC;IACD,IAAI,OAAO,CAAC,aAAa,KAAK,SAAS,IAAI,OAAO,CAAC,aAAa,KAAK,aAAa,EAAE,CAAC;QACnF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,8EAA8E,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IACpJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,EAAE,CAAC;AAC1E,CAAC;AAED,+GAA+G;AAC/G,MAAM,UAAU,YAAY,CAAC,MAA+B;IAC1D,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,yBAAyB,CAAC,CAAC,CAAC;IACnE,IAAI,OAAyD,CAAC;IAC9D,IAAI,CAAC;QACH,QAAQ,CAAC,GAAG,CAAC,CAAC;QACd,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACvE,OAAO,GAAG,iBAAiB,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC;QAClL,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,KAAK,CAAC;IAAC,CAAC;YACjB,CAAC;QACP,IAAI,CAAC;YAAC,OAAO,EAAE,KAAK,EAAE,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,kBAAkB,CAAC,CAAC;QACtD,IAAI,CAAC;YAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,sCAAsC,CAAC,CAAC;IACxH,CAAC;AACH,CAAC;AAOD,iFAAiF;AACjF,SAAS,WAAW,CAAC,IAAY,EAAE,IAAY;IAC7C,MAAM,IAAI,GAAG,GAAG,IAAI,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC;IAC7D,IAAI,CAAC;QAAC,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAAC,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAAC,CAAC;IAC3E,OAAO,KAAK,EAAE,CAAC;QAAC,IAAI,CAAC;YAAC,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,uBAAuB,CAAC,CAAC;QAAC,MAAM,KAAK,CAAC;IAAC,CAAC;AACzG,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,IAAY,EAAE,OAAmD;IAC7G,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,gBAAgB;YAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,sCAAsC,EAAE,CAAC;QACnJ,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,yCAA0C,KAAe,CAAC,IAAI,GAAG,EAAE,CAAC;IACpI,CAAC;IACD,MAAM,SAAS,GAAG,aAAa,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IAC/E,IAAI,CAAC,SAAS,CAAC,EAAE;QAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IACtJ,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QAChC,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,sCAAsC,EAAE,GAAG,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa,EAAE,YAAY,EAAE,KAAK,CAAC,YAAY,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;IACzQ,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,KAAK;QAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;IAC1D,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;IAC5C,IAAI,QAAQ,GAAkB,IAAI,CAAC;IACnC,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAAC;QAAC,YAAY,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAAC,CAAC;IAC/G,WAAW,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IACtE,WAAW,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAkB,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC;QAC3D,SAAS,EAAE,KAAK,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,CAAC,aAAa,EAAE,aAAa,EAAE,KAAK,CAAC,YAAY;QAC5H,YAAY,EAAE,KAAK,CAAC,WAAW,EAAE,cAAc,EAAE,KAAK,CAAC,aAAa,EAAE,QAAQ,EAAE,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACnI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IACjB,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;AAC1D,CAAC","sourcesContent":["// Load a policy exported from a Scopebond Cloud workspace, and say honestly what happened.\n//\n// An export is a reviewed policy in a file. Nothing is enforcing it until a hook loads it, and\n// the workspace shows it as \"pending\" until a signed acknowledgement echoes exactly what the\n// export said: its policy digest, its policy id and version, its scope digest and the export\n// id. This module checks an export and, when asked, installs it as the active policy; the\n// caller then queues the acknowledgement (`ObservationEmitter.policyAck`).\n//\n// What is checked, and what is not:\n// - the export's own integrity: its `policy_hash` must be the SHA-256 of the canonical policy\n// it carries, and its `scope.scope_digest` must be the SHA-256 of\n// `scopebond:policy-scope/v1\\n` + the canonical `{agent_id, environment_id, export_id}`;\n// - that it is for this workspace environment (when this machine is connected);\n// - that the gateway can build a runtime from it.\n// An export carries no signature of its own, so this cannot prove who wrote it: get the file\n// from your workspace, over the workspace's own page or API.\n//\n// Loading replaces the active policy. The previous file is kept beside it, and the write is\n// atomic (a temp file in the same directory, then a rename), so an interrupted load leaves the\n// old policy in place. Nothing here touches an agent's settings.\n\nimport { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createHash, randomBytes } from \"node:crypto\";\nimport { canonical } from \"@scopebond/policy-schema/canonical\";\nimport { scaffold } from \"./init.js\";\nimport { createHookRuntime } from \"./runtime.js\";\nimport { digestPolicy, type PolicyAckInput, type PolicyLoadError } from \"./observation.js\";\n\nexport const POLICY_SCOPE_DOMAIN = \"scopebond:policy-scope/v1\\n\";\nexport const MAX_EXPORT_BYTES = 1024 * 1024;\nexport const LOADED_POLICY_FILE = \"loaded-policy.json\";\nexport const PREVIOUS_POLICY_FILE = \"policy.previous.json\";\n\n/** SHA-256 of the domain plus the canonical scope an export was made for. */\nexport const policyScopeDigest = (scope: { export_id: string; agent_id: string; environment_id: string }): string =>\n createHash(\"sha256\").update(POLICY_SCOPE_DOMAIN + canonical({ agent_id: scope.agent_id, environment_id: scope.environment_id, export_id: scope.export_id } as never), \"utf8\").digest(\"hex\");\n\nconst HEX64 = /^[0-9a-f]{64}$/;\nconst OPAQUE = /^[\\x21-\\x7e]{1,200}$/;\n\nexport interface ExportFacts {\n exportId: string;\n policyId: string;\n policyVersion: number;\n policyDigest: string;\n scopeDigest: string;\n agentId: string;\n environmentId: string;\n policy: Record<string, unknown>;\n}\n\nexport type Inspection =\n | { ok: true; facts: ExportFacts }\n /** `ack` is set when the export names enough to echo in a rejection; otherwise nothing is acknowledged. */\n | { ok: false; error: PolicyLoadError; message: string; ack?: Omit<PolicyAckInput, \"error\"> };\n\nconst isObject = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n\n/** Accept the export itself, or the workspace's `{ id, configuration }` answer around it. */\nexport function unwrapExport(raw: unknown): Record<string, unknown> | null {\n if (!isObject(raw)) return null;\n if (raw.type === undefined && isObject(raw.configuration)) return raw.configuration;\n return raw;\n}\n\n/** Check an export without reading or changing anything on disk. Pure. */\nexport function inspectExport(raw: unknown, context: { environmentId?: string } = {}): Inspection {\n const exp = unwrapExport(raw);\n if (!exp || exp.type !== \"scopebond:reviewed-policy-export\") return { ok: false, error: \"schema_invalid\", message: \"this is not a Scopebond policy export\" };\n if (exp.version !== 1) return { ok: false, error: \"unsupported\", message: `unsupported export version ${String(exp.version)}` };\n const scope = exp.scope;\n const policy = exp.policy;\n if (!isObject(scope) || !isObject(policy) || typeof exp.policy_hash !== \"string\" || !HEX64.test(exp.policy_hash)) {\n return { ok: false, error: \"schema_invalid\", message: \"the export lacks a policy, a policy hash or a scope block (an export made before scope blocks existed cannot be acknowledged; export it again from the workspace)\" };\n }\n const { export_id: exportId, policy_id: policyId, policy_version: policyVersion, agent_id: agentId, environment_id: environmentId, scope_digest: scopeDigest } = scope;\n if (typeof exportId !== \"string\" || !OPAQUE.test(exportId) || typeof policyId !== \"string\" || !OPAQUE.test(policyId)\n || !Number.isInteger(policyVersion) || (policyVersion as number) < 1 || (policyVersion as number) > 2_147_483_647\n || typeof agentId !== \"string\" || !OPAQUE.test(agentId) || typeof environmentId !== \"string\" || !OPAQUE.test(environmentId)\n || typeof scopeDigest !== \"string\" || !HEX64.test(scopeDigest)) {\n return { ok: false, error: \"schema_invalid\", message: \"the export's scope block is incomplete or malformed\" };\n }\n // The values below are echoed exactly as the export states them, so a rejection still names the export.\n const echo = { exportId, policyId, policyVersion: policyVersion as number, policyDigest: exp.policy_hash, scopeDigest };\n if (digestPolicy(policy) !== exp.policy_hash) {\n return { ok: false, error: \"signature_invalid\", message: \"the policy does not match the export's policy hash; the file was changed or damaged\", ack: echo };\n }\n if (policyScopeDigest({ export_id: exportId, agent_id: agentId, environment_id: environmentId }) !== scopeDigest) {\n return { ok: false, error: \"scope_mismatch\", message: \"the export's scope digest does not match its export, agent and environment\", ack: echo };\n }\n if (context.environmentId !== undefined && context.environmentId !== environmentId) {\n return { ok: false, error: \"scope_mismatch\", message: \"this export is for a different environment than this machine is connected to\", ack: echo };\n }\n return { ok: true, facts: { ...echo, agentId, environmentId, policy } };\n}\n\n/** Whether the gateway can build a runtime from this policy, in a throwaway home. Never touches a real one. */\nexport function policyBuilds(policy: Record<string, unknown>): boolean {\n const dir = mkdtempSync(join(tmpdir(), \"scopebond-policy-check-\"));\n let runtime: ReturnType<typeof createHookRuntime> | undefined;\n try {\n scaffold(dir);\n writeFileSync(join(dir, \"policy.json\"), `${JSON.stringify(policy)}\\n`);\n runtime = createHookRuntime({ policyPath: join(dir, \"policy.json\"), keyPath: join(dir, \"agent.key\"), attesterPath: join(dir, \"attester.key\"), dbPath: join(dir, \"receipts.db\") });\n return true;\n } catch { return false; }\n finally {\n try { runtime?.close(); } catch { /* removed next */ }\n try { rmSync(dir, { recursive: true, force: true, maxRetries: 5 }); } catch { /* the OS temp dir is cleaned later */ }\n }\n}\n\nexport type LoadOutcome =\n | { state: \"loaded\"; facts: ExportFacts; policyPath: string; previous: string | null }\n | { state: \"would_load\"; facts: ExportFacts }\n | { state: \"rejected\"; error: PolicyLoadError; message: string; ack?: Omit<PolicyAckInput, \"error\"> };\n\n/** Write a file atomically: a temp file in the same directory, then a rename. */\nfunction writeAtomic(file: string, text: string): void {\n const temp = `${file}.${randomBytes(6).toString(\"hex\")}.tmp`;\n try { writeFileSync(temp, text, { mode: 0o600 }); renameSync(temp, file); }\n catch (error) { try { rmSync(temp, { force: true }); } catch { /* nothing to remove */ } throw error; }\n}\n\n/** Read an export file, check it, and (with `apply`) install it as the active policy in `dir`. */\nexport function loadPolicyExport(dir: string, file: string, options: { apply: boolean; environmentId?: string }): LoadOutcome {\n let raw: unknown;\n try {\n if (statSync(file).size > MAX_EXPORT_BYTES) return { state: \"rejected\", error: \"schema_invalid\", message: \"the export file is larger than 1 MiB\" };\n raw = JSON.parse(readFileSync(file, \"utf8\"));\n } catch (error) {\n return { state: \"rejected\", error: \"schema_invalid\", message: `the export could not be read as JSON (${(error as Error).name})` };\n }\n const inspected = inspectExport(raw, { environmentId: options.environmentId });\n if (!inspected.ok) return { state: \"rejected\", error: inspected.error, message: inspected.message, ...(inspected.ack ? { ack: inspected.ack } : {}) };\n const { facts } = inspected;\n if (!policyBuilds(facts.policy)) {\n return { state: \"rejected\", error: \"unsupported\", message: \"this gateway cannot load that policy\", ack: { exportId: facts.exportId, policyId: facts.policyId, policyVersion: facts.policyVersion, policyDigest: facts.policyDigest, scopeDigest: facts.scopeDigest } };\n }\n if (!options.apply) return { state: \"would_load\", facts };\n mkdirSync(dir, { recursive: true });\n const policyPath = join(dir, \"policy.json\");\n let previous: string | null = null;\n if (existsSync(policyPath)) { previous = join(dir, PREVIOUS_POLICY_FILE); copyFileSync(policyPath, previous); }\n writeAtomic(policyPath, `${JSON.stringify(facts.policy, null, 2)}\\n`);\n writeAtomic(join(dir, LOADED_POLICY_FILE), `${JSON.stringify({\n export_id: facts.exportId, policy_id: facts.policyId, policy_version: facts.policyVersion, policy_digest: facts.policyDigest,\n scope_digest: facts.scopeDigest, environment_id: facts.environmentId, agent_id: facts.agentId, loaded_at: new Date().toISOString(),\n }, null, 2)}\\n`);\n return { state: \"loaded\", facts, policyPath, previous };\n}\n"]}
@@ -0,0 +1,30 @@
1
+ import { type CapabilityCell, type ProofRecord } from "./capabilities.js";
2
+ import type { HookConnection } from "./cloud.js";
3
+ export declare const PROOF_FILE = "capability-proof.json";
4
+ /** Options for a fixture run. */
5
+ export interface ProofRunOptions {
6
+ /** A real hook config dir whose machine and countersigning keys the fixtures should be
7
+ * signed with (copied into the temp home, never modified), so the workspace can accept
8
+ * the fixture receipts and resolve a proof's digests against them. Without it the
9
+ * fixtures use throwaway keys and produce no digests a workspace could resolve. */
10
+ identityDir?: string;
11
+ /** Receives every fixture receipt produced, once each, for delivery. */
12
+ collect?: unknown[];
13
+ }
14
+ /** Run every supported cell's fixtures once per adapter/action/phase, and record the
15
+ * result under each host variant's key. A fixture exercises the adapter, so it cannot
16
+ * tell Codex desktop from Codex CLI: both keys get the same fixture proof, and neither
17
+ * becomes verified (see `cellState`). */
18
+ export declare function runProofFixtures(adapterVersion: string, options?: ProofRunOptions): Promise<Record<string, ProofRecord>>;
19
+ export declare function loadProofs(configDir: string): Record<string, ProofRecord>;
20
+ export declare function saveProofs(configDir: string, proofs: Record<string, ProofRecord>): string;
21
+ /** Whether a cell's fixture proof passed, for a summary line. */
22
+ export declare const proofPassed: (proof: ProofRecord, cell: CapabilityCell) => boolean;
23
+ /** Deliver fixture receipts to the workspace's receipt route and report whether every one was
24
+ * accepted. A proof names these receipts by digest, so it is only worth sending after they
25
+ * landed; anything short of full delivery leaves the proof unsent, never half-claimed. */
26
+ export declare function deliverProofReceipts(connection: HookConnection, receipts: unknown[], options?: {
27
+ fetch?: typeof fetch;
28
+ timeoutMs?: number;
29
+ }): Promise<boolean>;
30
+ //# sourceMappingURL=proof.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proof.d.ts","sourceRoot":"","sources":["../src/proof.ts"],"names":[],"mappings":"AA4BA,OAAO,EAA4E,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAKpJ,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,eAAO,MAAM,UAAU,0BAA0B,CAAC;AAKlD,iCAAiC;AACjC,MAAM,WAAW,eAAe;IAC9B;;;wFAGoF;IACpF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;CACrB;AAyED;;;0CAG0C;AAC1C,wBAAsB,gBAAgB,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAkBlI;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAOzE;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAIzF;AAED,iEAAiE;AACjE,eAAO,MAAM,WAAW,GAAI,OAAO,WAAW,EAAE,MAAM,cAAc,KAAG,OAC6E,CAAC;AAErJ;;2FAE2F;AAC3F,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAkBxK"}
package/dist/proof.js ADDED
@@ -0,0 +1,186 @@
1
+ // Safe proof fixtures.
2
+ //
3
+ // For each supported capability cell this runs its tagged vectors end to end against a
4
+ // throwaway hook home: a safe action must be allowed and countersigned; a violating one
5
+ // must be denied at the hook boundary; every receipt must verify against the
6
+ // countersigning key; and every receipt of one tool call must carry one action group.
7
+ //
8
+ // Nothing here touches a developer's real settings, policy, keys or receipts. Every run
9
+ // creates its own directory under the OS temp dir, scaffolds a fresh machine key, policy
10
+ // and store there, and deletes it afterwards. No network is used, so a fixture can never
11
+ // produce a Cloud acknowledgement: `cloud_ack` is always `not_checked`, and `origin` is
12
+ // always `fixture`. A cell therefore cannot reach `verified_reporting` from this runner
13
+ // (see capabilities.ts); it can only show `configured_unverified` with a passing
14
+ // fixture, or `degraded` with a failing one.
15
+ //
16
+ // Observation-only cells (Cursor's after-edit event, and monitor-mode fetch and MCP
17
+ // calls) are proven with a known successful after-action fixture, labelled
18
+ // observation-only. No deny fixture is run for them and none is claimed.
19
+ import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
20
+ import { tmpdir } from "node:os";
21
+ import { join } from "node:path";
22
+ import { createCloudExporter, createMemoryCloudOutbox, verifyReceipt } from "@scopebond/gateway";
23
+ import { loadOrCreateAttester } from "@scopebond/gateway/node";
24
+ import { fillPushBranch } from "./map.js";
25
+ import { scaffold } from "./init.js";
26
+ import { createHookRuntime } from "./runtime.js";
27
+ import { ACTION_GROUP_PARAM } from "./group.js";
28
+ import { computeManifest, specForCell, vectorDigest, vectorsForCell } from "./capabilities.js";
29
+ import { mapVector } from "./vectors.js";
30
+ import { bindingKeyFromHex, operationsForCall, sourceReceiptHash } from "./observation.js";
31
+ import { callRequestOf, fixtureProbe } from "./typed-ops.js";
32
+ import { fixtureFiles } from "./typed-infra.js";
33
+ export const PROOF_FILE = "capability-proof.json";
34
+ const receiptActionType = (receipt) => receipt?.payload?.intent?.action_type;
35
+ const receiptParams = (receipt) => {
36
+ const payload = receipt?.payload;
37
+ return payload?.intent?.params ?? {};
38
+ };
39
+ const FIXTURE_KEY = bindingKeyFromHex("00".repeat(32));
40
+ /** Whether the adapter derives the typed operation a vector expects, from the same dispatched
41
+ * actions and raw request the hook uses, against a deterministic fixture repository. */
42
+ function typedDerived(vector, dispatched) {
43
+ if (!vector.typed)
44
+ return true;
45
+ const request = callRequestOf(vector.input);
46
+ const operations = operationsForCall({ dispatched: dispatched ?? [], request }, { key: FIXTURE_KEY, cwd: "/fixture", repositoryId: "sbr_fixture", probe: fixtureProbe, packageManagerVersion: () => undefined, files: fixtureFiles, env: () => undefined });
47
+ return operations.some((op) => op?.type === vector.typed.type && (vector.typed.verb === undefined || op.verb === vector.typed.verb));
48
+ }
49
+ async function run(vector, runtime, attesterPem) {
50
+ const decision = await runtime.evaluate(fillPushBranch(mapVector(vector), "feature/work"), { groupKey: `proof:${vector.id}` });
51
+ const receipts = decision.receipts ?? (decision.receipt ? [decision.receipt] : []);
52
+ const valid = receipts.length > 0 && receipts.every((r) => verifyReceipt(r, attesterPem).valid);
53
+ const groups = new Set(receipts.map((r) => receiptParams(r)[ACTION_GROUP_PARAM]));
54
+ const grouped = receipts.length > 0 && groups.size === 1 && typeof [...groups][0] === "string" && String([...groups][0]) !== "";
55
+ return { decision: decision.decision, receipts, valid, grouped, typed: typedDerived(vector, decision.dispatched) };
56
+ }
57
+ /** Run one cell's fixtures in a temp hook home. Returns null when the cell has no vectors. */
58
+ async function proveCell(adapter, actionType, phase, observationOnly, adapterVersion, options) {
59
+ const vectors = vectorsForCell(adapter, actionType, phase);
60
+ const digest = vectorDigest(vectors);
61
+ if (digest === null)
62
+ return null;
63
+ const dir = mkdtempSync(join(tmpdir(), "scopebond-proof-"));
64
+ let runtime;
65
+ try {
66
+ if (options.identityDir) {
67
+ for (const name of ["agent.key", "attester.key"]) {
68
+ const source = join(options.identityDir, name);
69
+ if (existsSync(source))
70
+ copyFileSync(source, join(dir, name));
71
+ }
72
+ }
73
+ scaffold(dir);
74
+ const attesterPath = join(dir, "attester.key");
75
+ const { attester } = loadOrCreateAttester({ file: attesterPath });
76
+ runtime = createHookRuntime({ policyPath: join(dir, "policy.json"), keyPath: join(dir, "agent.key"), attesterPath, dbPath: join(dir, "receipts.db") });
77
+ const allow = vectors.filter((v) => v.cell?.role === "allow" || v.cell?.role === "after_action");
78
+ const deny = vectors.filter((v) => v.cell?.role === "deny");
79
+ const allowed = [];
80
+ for (const v of allow)
81
+ allowed.push(await run(v, runtime, attester.publicKeyPem));
82
+ const denied = [];
83
+ if (!observationOnly)
84
+ for (const v of deny)
85
+ denied.push(await run(v, runtime, attester.publicKeyPem));
86
+ const all = [...allowed, ...denied];
87
+ const receipts = all.flatMap((o) => o.receipts);
88
+ // Only receipts of the cell's own action type stand as its proof (a compound command can
89
+ // also produce receipts of other types), and each is named by the workspace's projection.
90
+ const proofDigests = [...new Set(receipts.filter((r) => receiptActionType(r) === actionType).map((r) => sourceReceiptHash(r)))];
91
+ return { receipts, proof: {
92
+ ran_at: new Date().toISOString(), adapter_version: adapterVersion, test_vector_digest: digest, origin: "fixture",
93
+ safe_allow: allowed.length > 0 && allowed.every((o) => o.decision !== "deny" && o.receipts.length > 0),
94
+ safe_deny: observationOnly ? "not_applicable" : denied.length > 0 && denied.every((o) => o.decision === "deny"),
95
+ signature: all.length > 0 && all.every((o) => o.valid),
96
+ grouping: all.length > 0 && all.every((o) => o.grouped),
97
+ cloud_ack: "not_checked", observation_only: observationOnly, proof_digests: proofDigests,
98
+ ...(vectors.some((v) => v.typed) ? { typed_operation: all.length > 0 && all.every((o) => o.typed !== false) } : {}),
99
+ } };
100
+ }
101
+ finally {
102
+ try {
103
+ runtime?.close();
104
+ }
105
+ catch { /* the temp home is removed next */ }
106
+ try {
107
+ rmSync(dir, { recursive: true, force: true, maxRetries: 5 });
108
+ }
109
+ catch { /* a handle still open on Windows; the OS temp dir is cleaned later */ }
110
+ }
111
+ }
112
+ /** Run every supported cell's fixtures once per adapter/action/phase, and record the
113
+ * result under each host variant's key. A fixture exercises the adapter, so it cannot
114
+ * tell Codex desktop from Codex CLI: both keys get the same fixture proof, and neither
115
+ * becomes verified (see `cellState`). */
116
+ export async function runProofFixtures(adapterVersion, options = {}) {
117
+ const cells = computeManifest({ adapterVersion, configured: { claude: true, codex: true, cursor: true } }).cells;
118
+ const records = {};
119
+ const cache = new Map();
120
+ for (const cell of cells) {
121
+ if (cell.state === "unsupported")
122
+ continue;
123
+ const spec = specForCell(cell);
124
+ if (!spec)
125
+ continue;
126
+ const id = `${spec.adapter}/${spec.action_type}/${spec.phase}`;
127
+ if (!cache.has(id)) {
128
+ const run = await proveCell(spec.adapter, spec.action_type, spec.phase, cell.observation_only, adapterVersion, options);
129
+ if (run)
130
+ options.collect?.push(...run.receipts);
131
+ cache.set(id, run?.proof ?? null);
132
+ }
133
+ const proof = cache.get(id);
134
+ if (proof)
135
+ records[cell.key] = { cell: cell.key, ...proof };
136
+ }
137
+ return records;
138
+ }
139
+ export function loadProofs(configDir) {
140
+ const file = join(configDir, PROOF_FILE);
141
+ if (!existsSync(file))
142
+ return {};
143
+ try {
144
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
145
+ return parsed.proofs && typeof parsed.proofs === "object" ? parsed.proofs : {};
146
+ }
147
+ catch {
148
+ return {};
149
+ }
150
+ }
151
+ export function saveProofs(configDir, proofs) {
152
+ const file = join(configDir, PROOF_FILE);
153
+ writeFileSync(file, `${JSON.stringify({ version: 1, proofs }, null, 2)}\n`);
154
+ return file;
155
+ }
156
+ /** Whether a cell's fixture proof passed, for a summary line. */
157
+ export const proofPassed = (proof, cell) => proof.safe_allow && (cell.observation_only ? proof.safe_deny === "not_applicable" : proof.safe_deny === true) && proof.signature && proof.grouping;
158
+ /** Deliver fixture receipts to the workspace's receipt route and report whether every one was
159
+ * accepted. A proof names these receipts by digest, so it is only worth sending after they
160
+ * landed; anything short of full delivery leaves the proof unsent, never half-claimed. */
161
+ export async function deliverProofReceipts(connection, receipts, options = {}) {
162
+ if (receipts.length === 0)
163
+ return true;
164
+ const exporter = createCloudExporter({
165
+ url: connection.url, credential: connection.credential, outbox: createMemoryCloudOutbox({ maxPending: Math.max(1000, receipts.length) }),
166
+ fetch: options.fetch, flushMs: 60_000,
167
+ });
168
+ try {
169
+ for (const receipt of receipts)
170
+ exporter.enqueue(receipt);
171
+ const deadline = Date.now() + (options.timeoutMs ?? 15_000);
172
+ while (exporter.pending() > 0 && Date.now() < deadline) {
173
+ await exporter.flush();
174
+ if (exporter.pending() > 0) {
175
+ if (exporter.status().consecutiveFailures > 0)
176
+ return false;
177
+ await new Promise((resolve) => setTimeout(resolve, 25));
178
+ }
179
+ }
180
+ return exporter.pending() === 0;
181
+ }
182
+ finally {
183
+ exporter.stop();
184
+ }
185
+ }
186
+ //# sourceMappingURL=proof.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proof.js","sourceRoot":"","sources":["../src/proof.ts"],"names":[],"mappings":"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","sourcesContent":["// Safe proof fixtures.\n//\n// For each supported capability cell this runs its tagged vectors end to end against a\n// throwaway hook home: a safe action must be allowed and countersigned; a violating one\n// must be denied at the hook boundary; every receipt must verify against the\n// countersigning key; and every receipt of one tool call must carry one action group.\n//\n// Nothing here touches a developer's real settings, policy, keys or receipts. Every run\n// creates its own directory under the OS temp dir, scaffolds a fresh machine key, policy\n// and store there, and deletes it afterwards. No network is used, so a fixture can never\n// produce a Cloud acknowledgement: `cloud_ack` is always `not_checked`, and `origin` is\n// always `fixture`. A cell therefore cannot reach `verified_reporting` from this runner\n// (see capabilities.ts); it can only show `configured_unverified` with a passing\n// fixture, or `degraded` with a failing one.\n//\n// Observation-only cells (Cursor's after-edit event, and monitor-mode fetch and MCP\n// calls) are proven with a known successful after-action fixture, labelled\n// observation-only. No deny fixture is run for them and none is claimed.\n\nimport { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createCloudExporter, createMemoryCloudOutbox, verifyReceipt, type SignedReceipt } from \"@scopebond/gateway\";\nimport { loadOrCreateAttester } from \"@scopebond/gateway/node\";\nimport { fillPushBranch } from \"./map.js\";\nimport { scaffold } from \"./init.js\";\nimport { createHookRuntime } from \"./runtime.js\";\nimport { ACTION_GROUP_PARAM } from \"./group.js\";\nimport { computeManifest, specForCell, vectorDigest, vectorsForCell, type Adapter, type CapabilityCell, type ProofRecord } from \"./capabilities.js\";\nimport { mapVector, type Vector } from \"./vectors.js\";\nimport { bindingKeyFromHex, operationsForCall, sourceReceiptHash } from \"./observation.js\";\nimport { callRequestOf, fixtureProbe } from \"./typed-ops.js\";\nimport { fixtureFiles } from \"./typed-infra.js\";\nimport type { HookConnection } from \"./cloud.js\";\n\nexport const PROOF_FILE = \"capability-proof.json\";\n\nconst receiptActionType = (receipt: unknown): string | undefined =>\n (receipt as { payload?: { intent?: { action_type?: unknown } } } | undefined)?.payload?.intent?.action_type as string | undefined;\n\n/** Options for a fixture run. */\nexport interface ProofRunOptions {\n /** A real hook config dir whose machine and countersigning keys the fixtures should be\n * signed with (copied into the temp home, never modified), so the workspace can accept\n * the fixture receipts and resolve a proof's digests against them. Without it the\n * fixtures use throwaway keys and produce no digests a workspace could resolve. */\n identityDir?: string;\n /** Receives every fixture receipt produced, once each, for delivery. */\n collect?: unknown[];\n}\n\nconst receiptParams = (receipt: unknown): Record<string, unknown> => {\n const payload = (receipt as { payload?: { intent?: { params?: Record<string, unknown> } } } | undefined)?.payload;\n return payload?.intent?.params ?? {};\n};\n\ninterface Outcome { decision: string; receipts: unknown[]; valid: boolean; grouped: boolean; typed?: boolean }\n\nconst FIXTURE_KEY = bindingKeyFromHex(\"00\".repeat(32));\n\n/** Whether the adapter derives the typed operation a vector expects, from the same dispatched\n * actions and raw request the hook uses, against a deterministic fixture repository. */\nfunction typedDerived(vector: Vector, dispatched: Array<{ action: { action_type: string; params: Record<string, unknown> } }> | undefined): boolean {\n if (!vector.typed) return true;\n const request = callRequestOf(vector.input);\n const operations = operationsForCall({ dispatched: dispatched ?? [], request }, { key: FIXTURE_KEY, cwd: \"/fixture\", repositoryId: \"sbr_fixture\", probe: fixtureProbe, packageManagerVersion: () => undefined, files: fixtureFiles, env: () => undefined });\n return operations.some((op) => op?.type === vector.typed!.type && (vector.typed!.verb === undefined || op.verb === vector.typed!.verb));\n}\n\nasync function run(vector: Vector, runtime: ReturnType<typeof createHookRuntime>, attesterPem: string): Promise<Outcome> {\n const decision = await runtime.evaluate(fillPushBranch(mapVector(vector), \"feature/work\"), { groupKey: `proof:${vector.id}` });\n const receipts = decision.receipts ?? (decision.receipt ? [decision.receipt] : []);\n const valid = receipts.length > 0 && receipts.every((r) => (verifyReceipt(r as Parameters<typeof verifyReceipt>[0], attesterPem) as unknown as { valid: boolean }).valid);\n const groups = new Set(receipts.map((r) => receiptParams(r)[ACTION_GROUP_PARAM]));\n const grouped = receipts.length > 0 && groups.size === 1 && typeof [...groups][0] === \"string\" && String([...groups][0]) !== \"\";\n return { decision: decision.decision, receipts, valid, grouped, typed: typedDerived(vector, decision.dispatched) };\n}\n\n/** Run one cell's fixtures in a temp hook home. Returns null when the cell has no vectors. */\nasync function proveCell(adapter: Adapter, actionType: string, phase: \"pre_action\" | \"after_action\", observationOnly: boolean, adapterVersion: string, options: ProofRunOptions): Promise<{ proof: Omit<ProofRecord, \"cell\">; receipts: unknown[] } | null> {\n const vectors = vectorsForCell(adapter, actionType, phase);\n const digest = vectorDigest(vectors);\n if (digest === null) return null;\n const dir = mkdtempSync(join(tmpdir(), \"scopebond-proof-\"));\n let runtime: ReturnType<typeof createHookRuntime> | undefined;\n try {\n if (options.identityDir) {\n for (const name of [\"agent.key\", \"attester.key\"]) {\n const source = join(options.identityDir, name);\n if (existsSync(source)) copyFileSync(source, join(dir, name));\n }\n }\n scaffold(dir);\n const attesterPath = join(dir, \"attester.key\");\n const { attester } = loadOrCreateAttester({ file: attesterPath });\n runtime = createHookRuntime({ policyPath: join(dir, \"policy.json\"), keyPath: join(dir, \"agent.key\"), attesterPath, dbPath: join(dir, \"receipts.db\") });\n const allow = vectors.filter((v) => v.cell?.role === \"allow\" || v.cell?.role === \"after_action\");\n const deny = vectors.filter((v) => v.cell?.role === \"deny\");\n const allowed: Outcome[] = [];\n for (const v of allow) allowed.push(await run(v, runtime, attester.publicKeyPem));\n const denied: Outcome[] = [];\n if (!observationOnly) for (const v of deny) denied.push(await run(v, runtime, attester.publicKeyPem));\n const all = [...allowed, ...denied];\n const receipts = all.flatMap((o) => o.receipts);\n // Only receipts of the cell's own action type stand as its proof (a compound command can\n // also produce receipts of other types), and each is named by the workspace's projection.\n const proofDigests = [...new Set(receipts.filter((r) => receiptActionType(r) === actionType).map((r) => sourceReceiptHash(r)))];\n return { receipts, proof: {\n ran_at: new Date().toISOString(), adapter_version: adapterVersion, test_vector_digest: digest, origin: \"fixture\",\n safe_allow: allowed.length > 0 && allowed.every((o) => o.decision !== \"deny\" && o.receipts.length > 0),\n safe_deny: observationOnly ? \"not_applicable\" : denied.length > 0 && denied.every((o) => o.decision === \"deny\"),\n signature: all.length > 0 && all.every((o) => o.valid),\n grouping: all.length > 0 && all.every((o) => o.grouped),\n cloud_ack: \"not_checked\", observation_only: observationOnly, proof_digests: proofDigests,\n ...(vectors.some((v) => v.typed) ? { typed_operation: all.length > 0 && all.every((o) => o.typed !== false) } : {}),\n } };\n } finally {\n try { runtime?.close(); } catch { /* the temp home is removed next */ }\n try { rmSync(dir, { recursive: true, force: true, maxRetries: 5 }); } catch { /* a handle still open on Windows; the OS temp dir is cleaned later */ }\n }\n}\n\n/** Run every supported cell's fixtures once per adapter/action/phase, and record the\n * result under each host variant's key. A fixture exercises the adapter, so it cannot\n * tell Codex desktop from Codex CLI: both keys get the same fixture proof, and neither\n * becomes verified (see `cellState`). */\nexport async function runProofFixtures(adapterVersion: string, options: ProofRunOptions = {}): Promise<Record<string, ProofRecord>> {\n const cells = computeManifest({ adapterVersion, configured: { claude: true, codex: true, cursor: true } }).cells;\n const records: Record<string, ProofRecord> = {};\n const cache = new Map<string, Omit<ProofRecord, \"cell\"> | null>();\n for (const cell of cells) {\n if (cell.state === \"unsupported\") continue;\n const spec = specForCell(cell);\n if (!spec) continue;\n const id = `${spec.adapter}/${spec.action_type}/${spec.phase}`;\n if (!cache.has(id)) {\n const run = await proveCell(spec.adapter, spec.action_type, spec.phase, cell.observation_only, adapterVersion, options);\n if (run) options.collect?.push(...run.receipts);\n cache.set(id, run?.proof ?? null);\n }\n const proof = cache.get(id);\n if (proof) records[cell.key] = { cell: cell.key, ...proof };\n }\n return records;\n}\n\nexport function loadProofs(configDir: string): Record<string, ProofRecord> {\n const file = join(configDir, PROOF_FILE);\n if (!existsSync(file)) return {};\n try {\n const parsed = JSON.parse(readFileSync(file, \"utf8\")) as { proofs?: Record<string, ProofRecord> };\n return parsed.proofs && typeof parsed.proofs === \"object\" ? parsed.proofs : {};\n } catch { return {}; }\n}\n\nexport function saveProofs(configDir: string, proofs: Record<string, ProofRecord>): string {\n const file = join(configDir, PROOF_FILE);\n writeFileSync(file, `${JSON.stringify({ version: 1, proofs }, null, 2)}\\n`);\n return file;\n}\n\n/** Whether a cell's fixture proof passed, for a summary line. */\nexport const proofPassed = (proof: ProofRecord, cell: CapabilityCell): boolean =>\n proof.safe_allow && (cell.observation_only ? proof.safe_deny === \"not_applicable\" : proof.safe_deny === true) && proof.signature && proof.grouping;\n\n/** Deliver fixture receipts to the workspace's receipt route and report whether every one was\n * accepted. A proof names these receipts by digest, so it is only worth sending after they\n * landed; anything short of full delivery leaves the proof unsent, never half-claimed. */\nexport async function deliverProofReceipts(connection: HookConnection, receipts: unknown[], options: { fetch?: typeof fetch; timeoutMs?: number } = {}): Promise<boolean> {\n if (receipts.length === 0) return true;\n const exporter = createCloudExporter({\n url: connection.url, credential: connection.credential, outbox: createMemoryCloudOutbox({ maxPending: Math.max(1000, receipts.length) }),\n fetch: options.fetch, flushMs: 60_000,\n });\n try {\n for (const receipt of receipts) exporter.enqueue(receipt as SignedReceipt);\n const deadline = Date.now() + (options.timeoutMs ?? 15_000);\n while (exporter.pending() > 0 && Date.now() < deadline) {\n await exporter.flush();\n if (exporter.pending() > 0) {\n if (exporter.status().consecutiveFailures > 0) return false;\n await new Promise((resolve) => setTimeout(resolve, 25));\n }\n }\n return exporter.pending() === 0;\n } finally { exporter.stop(); }\n}\n"]}
package/dist/rules.d.ts CHANGED
@@ -28,6 +28,16 @@ export interface RuleSet {
28
28
  protected_read: PathRule[];
29
29
  /** Action types recorded but not blocked. */
30
30
  observe: string[];
31
+ /** Optional workspace scope. When set, a write whose physical target (symlinks and
32
+ * junctions followed, rename and link destinations included) is outside these roots,
33
+ * or cannot be resolved, is denied. `.` means the project directory. Absent = not
34
+ * enforced, which is the default. */
35
+ allowed_roots?: string[];
36
+ /** Optional. When true, a shell command that runs SQL against a remote database (a Wrangler
37
+ * D1 command without --local, or psql to a host other than this machine) is denied before it
38
+ * runs if the SQL drops a table, deletes or updates every row, drops or renames in an ALTER, or
39
+ * cannot be read. Absent = not enforced, which is the default. */
40
+ protect_remote_database?: boolean;
31
41
  }
32
42
  export declare const RULES_FILE = "rules.json";
33
43
  /** A protected-path rule from a plain path the user typed. A trailing `/` (or a path with
@@ -46,6 +56,16 @@ export declare function defaultRules(): RuleSet;
46
56
  export declare const rulesPath: (dir: string) => string;
47
57
  export declare function loadRules(configDir: string): RuleSet | null;
48
58
  export declare function saveRules(configDir: string, rules: RuleSet): string;
59
+ /** Whether `path` is inside the location a protected-path rule names. This is the same
60
+ * pattern the compiled policy evaluates (the rule allows a path when the pattern's
61
+ * negative lookahead matches), tested rule by rule so a classification can say which
62
+ * kind of location it was. */
63
+ export declare function pathRuleMatches(rule: PathRule, path: string): boolean;
64
+ /** Whether a shell program is on the destructive list, folded the way the compiled
65
+ * policy folds it: case-insensitive, without an executable suffix. */
66
+ export declare function isDestructiveProgram(rules: RuleSet, program: string): boolean;
67
+ /** Whether a push destination is one of the protected refs (`release/*` is a prefix). */
68
+ export declare function isProtectedBranch(rules: RuleSet, ref: string): boolean;
49
69
  /** Plain English, for `scopebond-hook rules`. */
50
70
  export declare function describeRules(rules: RuleSet): string;
51
71
  //# sourceMappingURL=rules.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAsBA;;6EAE6E;AAC7E,MAAM,MAAM,QAAQ,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,CAAC,CAAC;IACX,8EAA8E;IAC9E,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,2EAA2E;IAC3E,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,eAAe,EAAE,QAAQ,EAAE,CAAC;IAC5B,cAAc,EAAE,QAAQ,EAAE,CAAC;IAC3B,6CAA6C;IAC7C,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,eAAO,MAAM,UAAU,eAAe,CAAC;AAiBvC;;;;;;oCAMoC;AACpC,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAenD;AA6BD,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgCjF;AAED;wDACwD;AACxD,wBAAgB,YAAY,IAAI,OAAO,CA0DtC;AAED,eAAO,MAAM,SAAS,GAAI,KAAK,MAAM,KAAG,MAA+B,CAAC;AAExE,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAQ3D;AAED,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAInE;AAED,iDAAiD;AACjD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAiBpD"}
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAsBA;;6EAE6E;AAC7E,MAAM,MAAM,QAAQ,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,CAAC,CAAC;IACX,8EAA8E;IAC9E,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,2EAA2E;IAC3E,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,eAAe,EAAE,QAAQ,EAAE,CAAC;IAC5B,cAAc,EAAE,QAAQ,EAAE,CAAC;IAC3B,6CAA6C;IAC7C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB;;;0CAGsC;IACtC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB;;;uEAGmE;IACnE,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAED,eAAO,MAAM,UAAU,eAAe,CAAC;AAiBvC;;;;;;oCAMoC;AACpC,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAenD;AA6BD,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0CjF;AAED;wDACwD;AACxD,wBAAgB,YAAY,IAAI,OAAO,CAmEtC;AAED,eAAO,MAAM,SAAS,GAAI,KAAK,MAAM,KAAG,MAA+B,CAAC;AAExE,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAQ3D;AAED,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAInE;AAED;;;+BAG+B;AAC/B,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAGrE;AAED;uEACuE;AACvE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAG7E;AAED,yFAAyF;AACzF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAMtE;AAED,iDAAiD;AACjD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAyBpD"}
package/dist/rules.js CHANGED
@@ -107,6 +107,16 @@ export function compile(rules, agentKid) {
107
107
  action_types: rules.observe,
108
108
  description: `Observe ${sentence(rules.observe)} — recorded, not blocked. Add bounds to enforce. Change this list in .scopebond/${RULES_FILE}.`,
109
109
  },
110
+ ...(rules.allowed_roots?.length ? [{
111
+ id: "protect-root", type: "action_allowlist", mode: "enforce", action_types: ["file.write"],
112
+ param_bounds: { root_scope: { pattern: "^inside$" } },
113
+ description: `Deny writes whose physical target is outside ${sentence(rules.allowed_roots)} — symlinks, junctions, rename and link destinations followed — and writes whose target cannot be resolved. Change these in .scopebond/${RULES_FILE}.`,
114
+ }] : []),
115
+ ...(rules.protect_remote_database ? [{
116
+ id: "protect-remote-database", type: "action_allowlist", mode: "enforce", action_types: ["db.exec"],
117
+ param_bounds: { risk: { pattern: "^ordinary$" } },
118
+ description: `Deny shell commands that run SQL against a remote database (a wrangler d1 command without --local, or psql to a host other than this machine) when the SQL drops a table, deletes or updates every row, drops or renames inside an ALTER, or cannot be read. The hook cannot tell which database is production, so every remote database is covered. Change this in .scopebond/${RULES_FILE}.`,
119
+ }] : []),
110
120
  { id: "keys", type: "key_policy", active_keys: [agentKid], description: "Only the enrolled machine key may sign." },
111
121
  ],
112
122
  };
@@ -130,6 +140,8 @@ export function defaultRules() {
130
140
  { kind: "under", value: "\\.git/hooks", label: "git hooks" },
131
141
  { kind: "named", value: "\\.git/config", label: "git config" },
132
142
  { kind: "under", value: "\\.husky", label: "Husky hooks" },
143
+ { kind: "named", value: "\\.cursor/mcp\\.json", label: "Cursor MCP config" },
144
+ { kind: "under", value: "\\.githooks", label: "git hooks (.githooks)" },
133
145
  { kind: "under", value: "\\.github/workflows", label: "GitHub workflows" },
134
146
  { kind: "under", value: "\\.github/actions", label: "GitHub actions" },
135
147
  { kind: "named", value: "\\.gitlab-ci\\.yml", label: ".gitlab-ci.yml" },
@@ -137,6 +149,13 @@ export function defaultRules() {
137
149
  { kind: "under", value: "\\.circleci", label: "CircleCI config" },
138
150
  { kind: "named", value: "azure-pipelines\\.yml", label: "azure-pipelines.yml" },
139
151
  { kind: "named", value: "Jenkinsfile", label: "Jenkinsfile" },
152
+ { kind: "named", value: "azure-pipelines\\.yaml", label: "azure-pipelines.yaml" },
153
+ { kind: "named", value: "bitbucket-pipelines\\.yml", label: "bitbucket-pipelines.yml" },
154
+ { kind: "named", value: "\\.travis\\.yml", label: ".travis.yml" },
155
+ { kind: "named", value: "\\.drone\\.yml", label: ".drone.yml" },
156
+ { kind: "named", value: "cloudbuild\\.yaml", label: "cloudbuild.yaml" },
157
+ { kind: "named", value: "cloudbuild\\.yml", label: "cloudbuild.yml" },
158
+ { kind: "under", value: "\\.buildkite", label: "Buildkite pipelines" },
140
159
  ],
141
160
  protected_read: [
142
161
  { kind: "under", value: "\\.scopebond", label: "the hook's own policy and keys (.scopebond)" },
@@ -192,6 +211,29 @@ export function saveRules(configDir, rules) {
192
211
  writeFileSync(file, `${JSON.stringify(rules, null, 2)}\n`);
193
212
  return file;
194
213
  }
214
+ /** Whether `path` is inside the location a protected-path rule names. This is the same
215
+ * pattern the compiled policy evaluates (the rule allows a path when the pattern's
216
+ * negative lookahead matches), tested rule by rule so a classification can say which
217
+ * kind of location it was. */
218
+ export function pathRuleMatches(rule, path) {
219
+ if (path === "")
220
+ return false;
221
+ return !new RegExp(`^${pathPattern(rule)}.+`).test(path);
222
+ }
223
+ /** Whether a shell program is on the destructive list, folded the way the compiled
224
+ * policy folds it: case-insensitive, without an executable suffix. */
225
+ export function isDestructiveProgram(rules, program) {
226
+ const name = program.toLowerCase().replace(/\.(?:exe|cmd|bat|com|ps1)$/, "");
227
+ return name !== "" && rules.destructive_programs.some((p) => p.toLowerCase() === name);
228
+ }
229
+ /** Whether a push destination is one of the protected refs (`release/*` is a prefix). */
230
+ export function isProtectedBranch(rules, ref) {
231
+ const name = ref.toLowerCase();
232
+ return rules.protected_branches.some((b) => {
233
+ const p = b.toLowerCase();
234
+ return p.endsWith("/*") ? name.startsWith(p.slice(0, -1)) : name === p;
235
+ });
236
+ }
195
237
  /** Plain English, for `scopebond-hook rules`. */
196
238
  export function describeRules(rules) {
197
239
  const lines = [];
@@ -209,6 +251,14 @@ export function describeRules(rules) {
209
251
  for (const rule of rules.protected_read)
210
252
  lines.push(` ${rule.label}`);
211
253
  lines.push("");
254
+ if (rules.protect_remote_database) {
255
+ lines.push(` remote SQL that drops a table, deletes or updates every row, or cannot be read`);
256
+ lines.push("");
257
+ }
258
+ if (rules.allowed_roots?.length) {
259
+ lines.push(` writes outside ${rules.allowed_roots.join(", ")} (physical target; unresolved targets too)`);
260
+ lines.push("");
261
+ }
212
262
  lines.push("Recorded, not blocked:");
213
263
  lines.push(` ${rules.observe.join(", ")}`);
214
264
  return lines.join("\n");