@scopebond/hook 0.8.1 → 0.10.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 (110) hide show
  1. package/README.md +239 -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 +585 -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 +29 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +19 -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/managed.d.ts +69 -0
  40. package/dist/managed.d.ts.map +1 -0
  41. package/dist/managed.js +184 -0
  42. package/dist/managed.js.map +1 -0
  43. package/dist/map.d.ts +4 -0
  44. package/dist/map.d.ts.map +1 -1
  45. package/dist/map.js +29 -1
  46. package/dist/map.js.map +1 -1
  47. package/dist/obs-emitter.d.ts +132 -0
  48. package/dist/obs-emitter.d.ts.map +1 -0
  49. package/dist/obs-emitter.js +415 -0
  50. package/dist/obs-emitter.js.map +1 -0
  51. package/dist/obs-store.d.ts +160 -0
  52. package/dist/obs-store.d.ts.map +1 -0
  53. package/dist/obs-store.js +367 -0
  54. package/dist/obs-store.js.map +1 -0
  55. package/dist/obs-upload.d.ts +24 -0
  56. package/dist/obs-upload.d.ts.map +1 -0
  57. package/dist/obs-upload.js +231 -0
  58. package/dist/obs-upload.js.map +1 -0
  59. package/dist/observation.d.ts +232 -0
  60. package/dist/observation.d.ts.map +1 -0
  61. package/dist/observation.js +308 -0
  62. package/dist/observation.js.map +1 -0
  63. package/dist/paths.d.ts +15 -0
  64. package/dist/paths.d.ts.map +1 -0
  65. package/dist/paths.js +79 -0
  66. package/dist/paths.js.map +1 -0
  67. package/dist/policy-load.d.ts +60 -0
  68. package/dist/policy-load.d.ts.map +1 -0
  69. package/dist/policy-load.js +151 -0
  70. package/dist/policy-load.js.map +1 -0
  71. package/dist/policy-sync.d.ts +45 -0
  72. package/dist/policy-sync.d.ts.map +1 -0
  73. package/dist/policy-sync.js +171 -0
  74. package/dist/policy-sync.js.map +1 -0
  75. package/dist/proof.d.ts +30 -0
  76. package/dist/proof.d.ts.map +1 -0
  77. package/dist/proof.js +186 -0
  78. package/dist/proof.js.map +1 -0
  79. package/dist/rules.d.ts +20 -0
  80. package/dist/rules.d.ts.map +1 -1
  81. package/dist/rules.js +50 -0
  82. package/dist/rules.js.map +1 -1
  83. package/dist/runtime.d.ts +21 -1
  84. package/dist/runtime.d.ts.map +1 -1
  85. package/dist/runtime.js +77 -10
  86. package/dist/runtime.js.map +1 -1
  87. package/dist/scan.d.ts +7 -0
  88. package/dist/scan.d.ts.map +1 -0
  89. package/dist/scan.js +38 -0
  90. package/dist/scan.js.map +1 -0
  91. package/dist/shell.d.ts.map +1 -1
  92. package/dist/shell.js +12 -3
  93. package/dist/shell.js.map +1 -1
  94. package/dist/sql-classify.d.ts +11 -0
  95. package/dist/sql-classify.d.ts.map +1 -0
  96. package/dist/sql-classify.js +251 -0
  97. package/dist/sql-classify.js.map +1 -0
  98. package/dist/typed-infra.d.ts +73 -0
  99. package/dist/typed-infra.d.ts.map +1 -0
  100. package/dist/typed-infra.js +758 -0
  101. package/dist/typed-infra.js.map +1 -0
  102. package/dist/typed-ops.d.ts +140 -0
  103. package/dist/typed-ops.d.ts.map +1 -0
  104. package/dist/typed-ops.js +549 -0
  105. package/dist/typed-ops.js.map +1 -0
  106. package/dist/vectors.d.ts +40 -0
  107. package/dist/vectors.d.ts.map +1 -0
  108. package/dist/vectors.js +219 -0
  109. package/dist/vectors.js.map +1 -0
  110. package/package.json +5 -5
@@ -0,0 +1,308 @@
1
+ // Signed observations (scopebond:observation v1): the envelope, its signature and the
2
+ // closed payloads this hook may produce.
3
+ //
4
+ // An observation is a separate, versioned record beside the receipt. Receipts, their
5
+ // canonicalization and their signatures are unchanged. The signature covers the literal
6
+ // domain `scopebond:observation/v1\n` followed by the RFC 8785 canonical payload; the
7
+ // observation hash is the SHA-256 of the same bytes. The wire wrapper is
8
+ // `{ payload, signature: { alg, kid, value } }` and nothing else.
9
+ //
10
+ // This hook holds an agent-adapter key, so it may emit only these kinds: session,
11
+ // capability, health, policy_ack, tool_intent and tool_outcome. Verification, platform
12
+ // outcomes and integrity results come from other sources and are not producible here
13
+ // (`assertAgentKind` refuses them).
14
+ //
15
+ // Nothing here carries raw content. Every path, host-local target, command and session
16
+ // identifier is reduced to a keyed opaque id or a closed enum before it is placed in a
17
+ // payload; the key stays on this machine and is never uploaded.
18
+ import { createHash, createHmac, createPrivateKey, randomBytes, randomUUID, sign as edSign } from "node:crypto";
19
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { redactCommand, scrubParam } from "./minimize.js";
22
+ import { deriveTypedOperations, gitPushOperation } from "./typed-ops.js";
23
+ import { canonical } from "@scopebond/policy-schema/canonical";
24
+ import { OBSERVATION_DOMAIN, OBSERVATION_LIMITS, OBSERVATION_TYPE, OBSERVATION_VERSION, SOURCE_RECEIPT_DOMAIN, observationSigningInput, } from "@scopebond/policy-schema";
25
+ /** The kinds an agent-adapter key may emit. Anything else is another source's to send. */
26
+ export const AGENT_KINDS = ["session", "capability", "health", "policy_ack", "tool_intent", "tool_outcome"];
27
+ export function assertAgentKind(kind) {
28
+ if (!AGENT_KINDS.includes(kind)) {
29
+ throw new Error(`an agent-adapter key may not emit "${kind}" observations`);
30
+ }
31
+ }
32
+ export const MAX_OBSERVATION_BYTES = OBSERVATION_LIMITS.maxObservationBytes;
33
+ export const MAX_BATCH_ITEMS = OBSERVATION_LIMITS.maxBatchItems;
34
+ export const MAX_BATCH_BODY_BYTES = OBSERVATION_LIMITS.maxBatchBodyBytes;
35
+ const KID = /^[A-Za-z0-9:._-]{1,200}$/;
36
+ /** A signer from the enrolled agent key's PEM. `kid` is the id the workspace registered
37
+ * for this key at enrollment; it is never derived or guessed here. */
38
+ export function observationSigner(privateKeyPem, kid) {
39
+ if (!KID.test(kid))
40
+ throw new TypeError("the enrolled key id is not a valid observation key id");
41
+ const key = createPrivateKey(privateKeyPem);
42
+ return {
43
+ kid,
44
+ sign: (text) => edSign(null, Buffer.from(text, "utf8"), key).toString("base64url"),
45
+ };
46
+ }
47
+ /** The bytes a signature (and the observation hash) cover: domain, then canonical payload. */
48
+ export const signingBytes = (payload) => Buffer.from(observationSigningInput(payload), "utf8");
49
+ /** SHA-256 of the signing bytes: 64 lowercase hex. Not proof of signature validity. */
50
+ export const observationHash = (payload) => createHash("sha256").update(signingBytes(payload)).digest("hex");
51
+ /** SHA-256 of `scopebond:source-receipt/v1\n` + canonical full signed receipt envelope. */
52
+ export const sourceReceiptHash = (receipt) => createHash("sha256").update(SOURCE_RECEIPT_DOMAIN + canonical(receipt), "utf8").digest("hex");
53
+ /** Sign a payload into its wire wrapper. Throws on a payload the server would refuse
54
+ * outright: the wrong kind, or larger than one observation may be. */
55
+ export function signObservation(payload, signer) {
56
+ assertAgentKind(payload.kind);
57
+ const wrapper = {
58
+ payload,
59
+ signature: { alg: "Ed25519", kid: signer.kid, value: signer.sign(OBSERVATION_DOMAIN + canonical(payload)) },
60
+ };
61
+ if (Buffer.byteLength(JSON.stringify(wrapper), "utf8") > MAX_OBSERVATION_BYTES) {
62
+ throw new RangeError(`observation exceeds ${MAX_OBSERVATION_BYTES} bytes`);
63
+ }
64
+ return wrapper;
65
+ }
66
+ /** RFC 3339 UTC with millisecond precision, as the schema requires (`...Z`). */
67
+ export const utc = (ms) => new Date(ms).toISOString();
68
+ export const newObservationId = () => randomUUID();
69
+ // ---- installation-local keyed ids and the request binding ------------------------------
70
+ /** The installation-local correlation key: 32 random bytes that never leave this machine.
71
+ * Its generation id is derived from the key, so rotating the key changes it and ids from
72
+ * two keys can never be joined. */
73
+ export const BINDING_KEY_FILE = "observation-binding.key";
74
+ export const REQUEST_BINDING_DOMAIN = "scopebond:request-binding/v1\n";
75
+ const RESOURCE_ID_DOMAIN = "scopebond:resource-id/v1\n";
76
+ const SESSION_ID_DOMAIN = "scopebond:session-id/v1\n";
77
+ const KEY_GENERATION_DOMAIN = "scopebond:request-binding-key/v1\n";
78
+ export function bindingKeyFromHex(hex) {
79
+ if (!/^[0-9a-f]{64}$/.test(hex))
80
+ throw new TypeError("invalid request-binding key");
81
+ const key = Buffer.from(hex, "hex");
82
+ const mac = (text) => createHmac("sha256", key).update(text, "utf8").digest("hex");
83
+ return {
84
+ generation: `bk_${createHash("sha256").update(KEY_GENERATION_DOMAIN).update(key).digest("hex").slice(0, 16)}`,
85
+ requestDigest: (request) => mac(REQUEST_BINDING_DOMAIN + canonical(request)),
86
+ resourceId: (kind, value) => `sbr_${mac(`${RESOURCE_ID_DOMAIN}${kind}\0${value}`).slice(0, 32)}`,
87
+ sessionId: (id) => `sbs_${mac(SESSION_ID_DOMAIN + id).slice(0, 32)}`,
88
+ };
89
+ }
90
+ /** Load (or create, 0600) the installation-local key beside the other hook keys. */
91
+ export function loadOrCreateBindingKey(dir) {
92
+ const file = join(dir, BINDING_KEY_FILE);
93
+ if (existsSync(file)) {
94
+ const hex = readFileSync(file, "utf8").trim();
95
+ if (/^[0-9a-f]{64}$/.test(hex))
96
+ return bindingKeyFromHex(hex);
97
+ }
98
+ const hex = randomBytes(32).toString("hex");
99
+ writeFileSync(file, hex + "\n", { mode: 0o600 });
100
+ return bindingKeyFromHex(hex);
101
+ }
102
+ /** Drop undefined so the payload is a closed object with absent optionals omitted. */
103
+ export function compact(value) {
104
+ const out = {};
105
+ for (const [k, v] of Object.entries(value))
106
+ if (v !== undefined)
107
+ out[k] = v;
108
+ return out;
109
+ }
110
+ /** Build the payload for a draft at an allocated sequence. Repeated fields in `data`
111
+ * (session id, generation, sequence, linkage) are set from the envelope, so they cannot
112
+ * disagree with it. */
113
+ export function buildPayload(draft, context, sequence) {
114
+ assertAgentKind(draft.kind);
115
+ const data = { ...draft.data };
116
+ if (draft.kind === "session") {
117
+ data.session_id = draft.sessionId;
118
+ data.sequence = sequence;
119
+ }
120
+ if (draft.kind === "health" && data.event === "heartbeat")
121
+ data.session_id = draft.sessionId;
122
+ if (draft.kind === "policy_ack")
123
+ data.installation_generation = context.generation;
124
+ if (draft.kind === "tool_intent" || draft.kind === "tool_outcome") {
125
+ data.parent_action_id = draft.parentActionId;
126
+ data.source_receipt_hash = draft.sourceReceiptHash;
127
+ }
128
+ return compact({
129
+ type: OBSERVATION_TYPE,
130
+ version: OBSERVATION_VERSION,
131
+ observation_id: draft.observationId ?? newObservationId(),
132
+ installation_id: context.installationId,
133
+ installation_generation: context.generation,
134
+ session_id: draft.sessionId,
135
+ parent_action_id: draft.parentActionId,
136
+ source_receipt_hash: draft.sourceReceiptHash,
137
+ kind: draft.kind,
138
+ occurred_at: utc(draft.occurredAt),
139
+ sequence,
140
+ data: compact(data),
141
+ });
142
+ }
143
+ // ---- kind-specific data builders -----------------------------------------------------------
144
+ export const SESSION_STOP_REASONS = ["completed", "failed", "cancelled", "sleep", "unknown"];
145
+ export const sessionStartData = (repositoryId) => compact({ event: "start", repository_id: repositoryId });
146
+ export const sessionStopData = (reason, repositoryId) => compact({ event: "stop", stop_reason: reason, repository_id: repositoryId });
147
+ export const heartbeatData = (leaseActive, extra = {}) => compact({
148
+ event: "heartbeat", lease_active: leaseActive, policy_digest: extra.policyDigest,
149
+ clock_offset_ms: extra.clockOffsetMs === undefined ? undefined : Math.max(-86_400_000, Math.min(86_400_000, Math.round(extra.clockOffsetMs))),
150
+ });
151
+ /** Oldest pending receipt time and backlog size. `count` 0 reports a drained queue
152
+ * (the timestamp is then the report time: the schema requires one). */
153
+ export const queueData = (oldestPendingAtMs, pendingCount, policyDigest) => compact({ event: "queue", oldest_pending_at: utc(oldestPendingAtMs), pending_count: Math.max(0, Math.min(1_000_000_000, Math.trunc(pendingCount))), policy_digest: policyDigest });
154
+ export const POLICY_LOAD_ERRORS = ["schema_invalid", "signature_invalid", "scope_mismatch", "unsupported"];
155
+ export const policyAckData = (input) => compact({
156
+ event: input.error ? "rejected" : "loaded",
157
+ export_id: input.exportId, policy_id: input.policyId, policy_version: input.policyVersion,
158
+ policy_digest: input.policyDigest, scope_digest: input.scopeDigest,
159
+ load_result: input.error ? "rejected" : "loaded",
160
+ error: input.error,
161
+ });
162
+ /** SHA-256 hex of a policy's canonical JSON. */
163
+ export const digestPolicy = (policy) => createHash("sha256").update(canonical(policy), "utf8").digest("hex");
164
+ export const capabilityProofData = (input) => ({
165
+ event: "proof", connector: "scopebond-hook", adapter_version: input.adapterVersion, host_variant: input.hostVariant,
166
+ action_type: input.actionType, phase: input.phase === "pre_action" ? "pre" : "after",
167
+ required_fields: input.requiredFields.slice(0, 100).map((f) => f.slice(0, 200)),
168
+ fixture_version: input.fixtureVersion.slice(0, 200), proof_result: input.passed ? "verified" : "failed",
169
+ ...(input.proofDigests && input.proofDigests.length > 0
170
+ ? { proof_digests: [...new Set(input.proofDigests.filter((d) => /^[0-9a-f]{64}$/.test(d)))].slice(0, 100) } : {}),
171
+ });
172
+ export const REFERENCE_SET_VERSION = "hook-1";
173
+ const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,39}$/;
174
+ export const HOSTNAME = /^(?=.{1,253}$)[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$/;
175
+ const PROGRAM_DESTRUCTIVE = new Set([
176
+ "rm", "sudo", "doas", "shutdown", "reboot", "halt", "poweroff", "mkfs", "dd", "shred", "truncate", "unlink", "wipe", "srm",
177
+ "del", "rd", "rmdir", "erase", "deltree", "format", "diskpart", "remove-item", "ri", "clear-content", "clc", "stop-computer", "restart-computer",
178
+ ]);
179
+ /** A short identifier the scrubber leaves untouched: not a credential shape, not a blob. */
180
+ const plain = (value) => SAFE_NAME.test(value) && scrubParam(value) === value;
181
+ const stringParam = (params, key) => typeof params[key] === "string" ? params[key] : undefined;
182
+ const CREDENTIAL_DIRS = new Set([".ssh", ".aws", ".gnupg", ".kube", ".docker"]);
183
+ const CREDENTIAL_FILES = new Set([".npmrc", ".pypirc", ".netrc", ".git-credentials"]);
184
+ const CI_FILES = new Set([".gitlab-ci.yml", ".gitlab-ci.yaml", "jenkinsfile", "azure-pipelines.yml", "azure-pipelines.yaml"]);
185
+ const GUARDRAIL_DIRS = new Set([".scopebond", ".claude", ".cursor", ".codex", ".husky", ".githooks"]);
186
+ const CREDENTIAL_EXT = /\.(key|pem|p12|pfx|jks|keystore)$/;
187
+ /** Trailing slashes removed by a scan, so no backtracking regex runs over a caller-supplied path. */
188
+ const trimTrailingSlashes = (s) => {
189
+ let end = s.length;
190
+ while (end > 0 && s.charCodeAt(end - 1) === 47)
191
+ end--;
192
+ return s.slice(0, end);
193
+ };
194
+ // Classified by path segment rather than by one large pattern: a path is caller-supplied, so
195
+ // nothing here can backtrack on it.
196
+ const pathClass = (path) => {
197
+ const p = path.replace(/\\/g, "/").toLowerCase();
198
+ if (p === "")
199
+ return "unknown";
200
+ const segs = p.split("/");
201
+ const base = segs[segs.length - 1];
202
+ if (base === ".env" || base.startsWith(".env.") || CREDENTIAL_EXT.test(base) || CREDENTIAL_FILES.has(base) || segs.some((s) => CREDENTIAL_DIRS.has(s)))
203
+ return "credential";
204
+ if (CI_FILES.has(base) || segs.some((s, i) => s === ".circleci" || s === ".buildkite" || (s === ".github" && (segs[i + 1] === "workflows" || segs[i + 1] === "actions"))))
205
+ return "ci";
206
+ if (base === ".mcp.json" || segs.some((s, i) => GUARDRAIL_DIRS.has(s) || (s === ".git" && (segs[i + 1]?.startsWith("hooks") === true || segs[i + 1]?.startsWith("config") === true))))
207
+ return "guardrail";
208
+ return "ordinary";
209
+ };
210
+ const isAbsolutePath = (p) => /^([A-Za-z]:[\\/]|\/|\\\\|~)/.test(p);
211
+ /** The context the typed-operation builders take, from an operation context. */
212
+ export const typedContext = (context) => ({
213
+ key: context.key, cwd: context.cwd, repositoryId: context.repositoryId ?? "", referenceSetVersion: REFERENCE_SET_VERSION,
214
+ ...(context.probe ? { probe: context.probe } : {}), ...(context.isProtectedRef ? { isProtectedRef: context.isProtectedRef } : {}),
215
+ ...(context.files ? { files: context.files } : {}), ...(context.env ? { env: context.env } : {}),
216
+ });
217
+ /**
218
+ * Reduce one dispatched action to its closed typed operation, or null when this hook
219
+ * cannot describe it honestly (an unmapped tool, a git push whose HEAD is unreadable, a
220
+ * host or tool name that is not a plain identifier). The request digest is the request
221
+ * binding over the actual dispatch request, `{action_type, params}` exactly as evaluated.
222
+ */
223
+ export function buildOperation(action, context) {
224
+ const { key } = context;
225
+ const request_digest = key.requestDigest({ action_type: action.action_type, params: action.params });
226
+ const common = { environment_class: "unknown", reference_set_version: REFERENCE_SET_VERSION, request_digest, digest_key_generation: key.generation };
227
+ const params = action.params;
228
+ switch (action.action_type) {
229
+ case "shell.exec": {
230
+ // Judge the name as typed: a credential shape is recognised by its case.
231
+ const typed = (stringParam(params, "program") ?? "").replace(/^.*[\\/]/, "").replace(/\.(exe|cmd|bat|com|ps1)$/i, "");
232
+ const raw = typed.toLowerCase();
233
+ const resolved = typed !== "" && plain(typed);
234
+ return {
235
+ type: "shell", resource_id: key.resourceId("shell", raw), ...common,
236
+ canonical_program: resolved ? raw : "unresolved",
237
+ destructive_class: !resolved ? "unknown" : PROGRAM_DESTRUCTIVE.has(raw) ? "unknown" : "none",
238
+ resolution: resolved ? "resolved" : "unresolved",
239
+ };
240
+ }
241
+ case "file.read":
242
+ case "file.write": {
243
+ const path = stringParam(params, "path") ?? "";
244
+ const resolved = path !== "";
245
+ const id = key.resourceId("file", path);
246
+ return {
247
+ type: "file", resource_id: id, ...common,
248
+ verb: action.action_type === "file.read" ? "read" : "write",
249
+ target_ids: [id], root_id: key.resourceId("root", context.cwd),
250
+ resolution: resolved ? "resolved" : "unresolved", path_class: pathClass(path),
251
+ ...(resolved ? { outside_root: isAbsolutePath(path) && !isInside(context.cwd, path) || /(^|[\\/])\.\.([\\/]|$)/.test(path) } : {}),
252
+ };
253
+ }
254
+ case "git.push": {
255
+ if (!context.repositoryId)
256
+ return null;
257
+ return gitPushOperation(params, typedContext(context), context.headSha, { action_type: action.action_type, params });
258
+ }
259
+ // A WebFetch reaches the mapper as host, path and method only. The typed network operation
260
+ // needs the scheme and port, which only the raw tool call carries, so it is derived from that
261
+ // request (deriveTypedOperations) and never guessed here: no request, no operation.
262
+ case "net.fetch": return null;
263
+ case "mcp.tool.call": {
264
+ const server = stringParam(params, "server") ?? "";
265
+ const tool = stringParam(params, "tool") ?? "";
266
+ if (!plain(server) || !plain(tool))
267
+ return null;
268
+ return {
269
+ type: "mcp", resource_id: key.resourceId("mcp", `${server}\0${tool}`), ...common,
270
+ server_id: server, tool_name: tool, manifest_version: "unversioned", operation_class: "unknown", resource_ids: [],
271
+ };
272
+ }
273
+ default:
274
+ return null;
275
+ }
276
+ }
277
+ function isInside(root, path) {
278
+ const norm = (s) => trimTrailingSlashes(s.replace(/\\/g, "/")).toLowerCase();
279
+ const r = norm(root);
280
+ const p = norm(path);
281
+ return p === r || p.startsWith(`${r}/`);
282
+ }
283
+ /**
284
+ * One operation (or null) per dispatched item of a tool call. A typed operation derived from
285
+ * the actual request (git commit, package install, GitHub pull request or release) replaces
286
+ * the plain one for the same item; every other item keeps the operation `buildOperation` gives it.
287
+ */
288
+ export function operationsForCall(input, context) {
289
+ let typed = new Map();
290
+ try {
291
+ typed = deriveTypedOperations({ ...input.request, dispatched: input.dispatched, redact: redactCommand }, {
292
+ ...typedContext(context),
293
+ ...(context.requiredCheckPolicyVersion ? { requiredCheckPolicyVersion: context.requiredCheckPolicyVersion } : {}),
294
+ ...(context.resolvePullRequest ? { resolvePullRequest: context.resolvePullRequest } : {}),
295
+ ...(context.packageManagerVersion ? { packageManagerVersion: context.packageManagerVersion } : {}),
296
+ });
297
+ }
298
+ catch { /* a derivation that fails leaves the plain operations */ }
299
+ return input.dispatched.map((item, index) => typed.get(index) ?? buildOperation(item.action, context));
300
+ }
301
+ export const intentData = (operation) => ({
302
+ event: "requested", operation, request_digest: operation.request_digest,
303
+ });
304
+ export const outcomeData = (operation, exit, adapterVersion, durationMs) => compact({
305
+ event: exit === "ok" ? "completed" : "failed", operation, exit_category: exit, adapter_version: adapterVersion.slice(0, 200),
306
+ duration_ms: durationMs === undefined ? undefined : Math.max(0, Math.min(86_400_000, Math.round(durationMs))),
307
+ });
308
+ //# sourceMappingURL=observation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observation.js","sourceRoot":"","sources":["../src/observation.ts"],"names":[],"mappings":"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","sourcesContent":["// Signed observations (scopebond:observation v1): the envelope, its signature and the\n// closed payloads this hook may produce.\n//\n// An observation is a separate, versioned record beside the receipt. Receipts, their\n// canonicalization and their signatures are unchanged. The signature covers the literal\n// domain `scopebond:observation/v1\\n` followed by the RFC 8785 canonical payload; the\n// observation hash is the SHA-256 of the same bytes. The wire wrapper is\n// `{ payload, signature: { alg, kid, value } }` and nothing else.\n//\n// This hook holds an agent-adapter key, so it may emit only these kinds: session,\n// capability, health, policy_ack, tool_intent and tool_outcome. Verification, platform\n// outcomes and integrity results come from other sources and are not producible here\n// (`assertAgentKind` refuses them).\n//\n// Nothing here carries raw content. Every path, host-local target, command and session\n// identifier is reduced to a keyed opaque id or a closed enum before it is placed in a\n// payload; the key stays on this machine and is never uploaded.\n\nimport { createHash, createHmac, createPrivateKey, randomBytes, randomUUID, sign as edSign, type KeyObject } from \"node:crypto\";\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { redactCommand, scrubParam } from \"./minimize.js\";\nimport type { FileProbe } from \"./typed-infra.js\";\nimport { deriveTypedOperations, gitPushOperation, type CallRequest, type GitProbe, type TypedContext } from \"./typed-ops.js\";\nexport type { CallRequest } from \"./typed-ops.js\";\nimport { canonical } from \"@scopebond/policy-schema/canonical\";\nimport {\n OBSERVATION_DOMAIN, OBSERVATION_LIMITS, OBSERVATION_TYPE, OBSERVATION_VERSION, SOURCE_RECEIPT_DOMAIN,\n observationSigningInput,\n} from \"@scopebond/policy-schema\";\n\n/** The kinds an agent-adapter key may emit. Anything else is another source's to send. */\nexport const AGENT_KINDS = [\"session\", \"capability\", \"health\", \"policy_ack\", \"tool_intent\", \"tool_outcome\"] as const;\nexport type AgentKind = (typeof AGENT_KINDS)[number];\n\nexport function assertAgentKind(kind: string): asserts kind is AgentKind {\n if (!(AGENT_KINDS as readonly string[]).includes(kind)) {\n throw new Error(`an agent-adapter key may not emit \"${kind}\" observations`);\n }\n}\n\nexport interface ObservationPayload {\n type: typeof OBSERVATION_TYPE;\n version: typeof OBSERVATION_VERSION;\n observation_id: string;\n installation_id: string;\n installation_generation: number;\n session_id?: string;\n parent_action_id?: string;\n source_receipt_hash?: string;\n kind: AgentKind;\n occurred_at: string;\n sequence: number;\n data: Record<string, unknown>;\n}\n\nexport interface SignedObservation {\n payload: ObservationPayload;\n signature: { alg: \"Ed25519\"; kid: string; value: string };\n}\n\nexport const MAX_OBSERVATION_BYTES = OBSERVATION_LIMITS.maxObservationBytes;\nexport const MAX_BATCH_ITEMS = OBSERVATION_LIMITS.maxBatchItems;\nexport const MAX_BATCH_BODY_BYTES = OBSERVATION_LIMITS.maxBatchBodyBytes;\n\nconst KID = /^[A-Za-z0-9:._-]{1,200}$/;\n\nexport interface ObservationSigner {\n kid: string;\n /** Ed25519 over the given UTF-8 text; unpadded base64url. */\n sign(text: string): string;\n}\n\n/** A signer from the enrolled agent key's PEM. `kid` is the id the workspace registered\n * for this key at enrollment; it is never derived or guessed here. */\nexport function observationSigner(privateKeyPem: string, kid: string): ObservationSigner {\n if (!KID.test(kid)) throw new TypeError(\"the enrolled key id is not a valid observation key id\");\n const key: KeyObject = createPrivateKey(privateKeyPem);\n return {\n kid,\n sign: (text) => edSign(null, Buffer.from(text, \"utf8\"), key).toString(\"base64url\"),\n };\n}\n\n/** The bytes a signature (and the observation hash) cover: domain, then canonical payload. */\nexport const signingBytes = (payload: ObservationPayload): Buffer =>\n Buffer.from(observationSigningInput(payload as unknown as Record<string, unknown>), \"utf8\");\n\n/** SHA-256 of the signing bytes: 64 lowercase hex. Not proof of signature validity. */\nexport const observationHash = (payload: ObservationPayload): string =>\n createHash(\"sha256\").update(signingBytes(payload)).digest(\"hex\");\n\n/** SHA-256 of `scopebond:source-receipt/v1\\n` + canonical full signed receipt envelope. */\nexport const sourceReceiptHash = (receipt: unknown): string =>\n createHash(\"sha256\").update(SOURCE_RECEIPT_DOMAIN + canonical(receipt as never), \"utf8\").digest(\"hex\");\n\n/** Sign a payload into its wire wrapper. Throws on a payload the server would refuse\n * outright: the wrong kind, or larger than one observation may be. */\nexport function signObservation(payload: ObservationPayload, signer: ObservationSigner): SignedObservation {\n assertAgentKind(payload.kind);\n const wrapper: SignedObservation = {\n payload,\n signature: { alg: \"Ed25519\", kid: signer.kid, value: signer.sign(OBSERVATION_DOMAIN + canonical(payload as never)) },\n };\n if (Buffer.byteLength(JSON.stringify(wrapper), \"utf8\") > MAX_OBSERVATION_BYTES) {\n throw new RangeError(`observation exceeds ${MAX_OBSERVATION_BYTES} bytes`);\n }\n return wrapper;\n}\n\n/** RFC 3339 UTC with millisecond precision, as the schema requires (`...Z`). */\nexport const utc = (ms: number): string => new Date(ms).toISOString();\n\nexport const newObservationId = (): string => randomUUID();\n\n// ---- installation-local keyed ids and the request binding ------------------------------\n\n/** The installation-local correlation key: 32 random bytes that never leave this machine.\n * Its generation id is derived from the key, so rotating the key changes it and ids from\n * two keys can never be joined. */\nexport const BINDING_KEY_FILE = \"observation-binding.key\";\nexport const REQUEST_BINDING_DOMAIN = \"scopebond:request-binding/v1\\n\";\nconst RESOURCE_ID_DOMAIN = \"scopebond:resource-id/v1\\n\";\nconst SESSION_ID_DOMAIN = \"scopebond:session-id/v1\\n\";\nconst KEY_GENERATION_DOMAIN = \"scopebond:request-binding-key/v1\\n\";\n\nexport interface BindingKey {\n /** Opaque id of this key generation; safe to upload (it reveals nothing of the key). */\n generation: string;\n /** HMAC-SHA-256 request binding: domain + canonical actual dispatch request. */\n requestDigest(request: unknown): string;\n /** A keyed opaque id for a resource (a path, a ref, a host-local target). */\n resourceId(kind: string, value: string): string;\n /** A keyed opaque id for a harness session id, stable across hook processes. */\n sessionId(harnessSessionId: string): string;\n}\n\nexport function bindingKeyFromHex(hex: string): BindingKey {\n if (!/^[0-9a-f]{64}$/.test(hex)) throw new TypeError(\"invalid request-binding key\");\n const key = Buffer.from(hex, \"hex\");\n const mac = (text: string): string => createHmac(\"sha256\", key).update(text, \"utf8\").digest(\"hex\");\n return {\n generation: `bk_${createHash(\"sha256\").update(KEY_GENERATION_DOMAIN).update(key).digest(\"hex\").slice(0, 16)}`,\n requestDigest: (request) => mac(REQUEST_BINDING_DOMAIN + canonical(request as never)),\n resourceId: (kind, value) => `sbr_${mac(`${RESOURCE_ID_DOMAIN}${kind}\\0${value}`).slice(0, 32)}`,\n sessionId: (id) => `sbs_${mac(SESSION_ID_DOMAIN + id).slice(0, 32)}`,\n };\n}\n\n/** Load (or create, 0600) the installation-local key beside the other hook keys. */\nexport function loadOrCreateBindingKey(dir: string): BindingKey {\n const file = join(dir, BINDING_KEY_FILE);\n if (existsSync(file)) {\n const hex = readFileSync(file, \"utf8\").trim();\n if (/^[0-9a-f]{64}$/.test(hex)) return bindingKeyFromHex(hex);\n }\n const hex = randomBytes(32).toString(\"hex\");\n writeFileSync(file, hex + \"\\n\", { mode: 0o600 });\n return bindingKeyFromHex(hex);\n}\n\n// ---- payload construction ----------------------------------------------------------------\n\nexport interface EnvelopeContext {\n installationId: string;\n generation: number;\n}\n\nexport interface ObservationDraft {\n kind: AgentKind;\n occurredAt: number;\n /** Already-derived opaque session id, when the observation belongs to one. */\n sessionId?: string;\n parentActionId?: string;\n sourceReceiptHash?: string;\n /** Kind-specific data. `sequence` is filled in by the outbox where the union repeats it. */\n data: Record<string, unknown>;\n /** Give a retry the same id. Defaults to a new UUID. */\n observationId?: string;\n}\n\n/** Drop undefined so the payload is a closed object with absent optionals omitted. */\nexport function compact<T extends Record<string, unknown>>(value: T): T {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) if (v !== undefined) out[k] = v;\n return out as T;\n}\n\n/** Build the payload for a draft at an allocated sequence. Repeated fields in `data`\n * (session id, generation, sequence, linkage) are set from the envelope, so they cannot\n * disagree with it. */\nexport function buildPayload(draft: ObservationDraft, context: EnvelopeContext, sequence: number): ObservationPayload {\n assertAgentKind(draft.kind);\n const data: Record<string, unknown> = { ...draft.data };\n if (draft.kind === \"session\") { data.session_id = draft.sessionId; data.sequence = sequence; }\n if (draft.kind === \"health\" && data.event === \"heartbeat\") data.session_id = draft.sessionId;\n if (draft.kind === \"policy_ack\") data.installation_generation = context.generation;\n if (draft.kind === \"tool_intent\" || draft.kind === \"tool_outcome\") {\n data.parent_action_id = draft.parentActionId;\n data.source_receipt_hash = draft.sourceReceiptHash;\n }\n return compact({\n type: OBSERVATION_TYPE,\n version: OBSERVATION_VERSION,\n observation_id: draft.observationId ?? newObservationId(),\n installation_id: context.installationId,\n installation_generation: context.generation,\n session_id: draft.sessionId,\n parent_action_id: draft.parentActionId,\n source_receipt_hash: draft.sourceReceiptHash,\n kind: draft.kind,\n occurred_at: utc(draft.occurredAt),\n sequence,\n data: compact(data),\n }) as ObservationPayload;\n}\n\n// ---- kind-specific data builders -----------------------------------------------------------\n\nexport const SESSION_STOP_REASONS = [\"completed\", \"failed\", \"cancelled\", \"sleep\", \"unknown\"] as const;\nexport type SessionStopReason = (typeof SESSION_STOP_REASONS)[number];\n\nexport const sessionStartData = (repositoryId?: string) => compact({ event: \"start\", repository_id: repositoryId });\nexport const sessionStopData = (reason: SessionStopReason, repositoryId?: string) =>\n compact({ event: \"stop\", stop_reason: reason, repository_id: repositoryId });\n\nexport const heartbeatData = (leaseActive: boolean, extra: { policyDigest?: string; clockOffsetMs?: number } = {}) =>\n compact({\n event: \"heartbeat\", lease_active: leaseActive, policy_digest: extra.policyDigest,\n clock_offset_ms: extra.clockOffsetMs === undefined ? undefined : Math.max(-86_400_000, Math.min(86_400_000, Math.round(extra.clockOffsetMs))),\n });\n\n/** Oldest pending receipt time and backlog size. `count` 0 reports a drained queue\n * (the timestamp is then the report time: the schema requires one). */\nexport const queueData = (oldestPendingAtMs: number, pendingCount: number, policyDigest?: string) =>\n compact({ event: \"queue\", oldest_pending_at: utc(oldestPendingAtMs), pending_count: Math.max(0, Math.min(1_000_000_000, Math.trunc(pendingCount))), policy_digest: policyDigest });\n\nexport const POLICY_LOAD_ERRORS = [\"schema_invalid\", \"signature_invalid\", \"scope_mismatch\", \"unsupported\"] as const;\nexport type PolicyLoadError = (typeof POLICY_LOAD_ERRORS)[number];\n\nexport interface PolicyAckInput {\n exportId: string;\n policyId: string;\n policyVersion: number;\n policyDigest: string;\n scopeDigest: string;\n /** Absent means loaded; a value means the hook refused the export for that reason. */\n error?: PolicyLoadError;\n}\n\nexport const policyAckData = (input: PolicyAckInput) => compact({\n event: input.error ? \"rejected\" : \"loaded\",\n export_id: input.exportId, policy_id: input.policyId, policy_version: input.policyVersion,\n policy_digest: input.policyDigest, scope_digest: input.scopeDigest,\n load_result: input.error ? \"rejected\" : \"loaded\",\n error: input.error,\n});\n\n/** SHA-256 hex of a policy's canonical JSON. */\nexport const digestPolicy = (policy: unknown): string => createHash(\"sha256\").update(canonical(policy as never), \"utf8\").digest(\"hex\");\n\nexport interface CapabilityProofInput {\n adapterVersion: string;\n hostVariant: string;\n actionType: string;\n phase: \"pre_action\" | \"after_action\";\n requiredFields: string[];\n /** `fixture/<digest>` for a local fixture run, so a fixture is never read as live. */\n fixtureVersion: string;\n passed: boolean;\n /** `source_receipt_hash` of each fixture receipt (the allow fixture and, for a before-action\n * cell, the deny fixture) this proof stands on. The workspace resolves them against the\n * receipts it accepted for this installation, so they are sent only once those receipts\n * were delivered. */\n proofDigests?: string[];\n}\n\nexport const capabilityProofData = (input: CapabilityProofInput) => ({\n event: \"proof\", connector: \"scopebond-hook\", adapter_version: input.adapterVersion, host_variant: input.hostVariant,\n action_type: input.actionType, phase: input.phase === \"pre_action\" ? \"pre\" : \"after\",\n required_fields: input.requiredFields.slice(0, 100).map((f) => f.slice(0, 200)),\n fixture_version: input.fixtureVersion.slice(0, 200), proof_result: input.passed ? \"verified\" : \"failed\",\n ...(input.proofDigests && input.proofDigests.length > 0\n ? { proof_digests: [...new Set(input.proofDigests.filter((d) => /^[0-9a-f]{64}$/.test(d)))].slice(0, 100) } : {}),\n});\n\n// ---- typed operations for tool_intent / tool_outcome -------------------------------------------\n\n/** What the hook saw about one dispatched action, before reduction to an operation. */\nexport interface DispatchedAction {\n action_type: string;\n params: Record<string, unknown>;\n}\n\nexport const REFERENCE_SET_VERSION = \"hook-1\";\nconst SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,39}$/;\nexport const HOSTNAME = /^(?=.{1,253}$)[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$/;\nconst PROGRAM_DESTRUCTIVE = new Set([\n \"rm\", \"sudo\", \"doas\", \"shutdown\", \"reboot\", \"halt\", \"poweroff\", \"mkfs\", \"dd\", \"shred\", \"truncate\", \"unlink\", \"wipe\", \"srm\",\n \"del\", \"rd\", \"rmdir\", \"erase\", \"deltree\", \"format\", \"diskpart\", \"remove-item\", \"ri\", \"clear-content\", \"clc\", \"stop-computer\", \"restart-computer\",\n]);\n\n/** A short identifier the scrubber leaves untouched: not a credential shape, not a blob. */\nconst plain = (value: string): boolean => SAFE_NAME.test(value) && scrubParam(value) === value;\n\nconst stringParam = (params: Record<string, unknown>, key: string): string | undefined =>\n typeof params[key] === \"string\" ? (params[key] as string) : undefined;\n\nconst CREDENTIAL_DIRS = new Set([\".ssh\", \".aws\", \".gnupg\", \".kube\", \".docker\"]);\nconst CREDENTIAL_FILES = new Set([\".npmrc\", \".pypirc\", \".netrc\", \".git-credentials\"]);\nconst CI_FILES = new Set([\".gitlab-ci.yml\", \".gitlab-ci.yaml\", \"jenkinsfile\", \"azure-pipelines.yml\", \"azure-pipelines.yaml\"]);\nconst GUARDRAIL_DIRS = new Set([\".scopebond\", \".claude\", \".cursor\", \".codex\", \".husky\", \".githooks\"]);\nconst CREDENTIAL_EXT = /\\.(key|pem|p12|pfx|jks|keystore)$/;\n\n/** Trailing slashes removed by a scan, so no backtracking regex runs over a caller-supplied path. */\nconst trimTrailingSlashes = (s: string): string => {\n let end = s.length;\n while (end > 0 && s.charCodeAt(end - 1) === 47) end--;\n return s.slice(0, end);\n};\n\n// Classified by path segment rather than by one large pattern: a path is caller-supplied, so\n// nothing here can backtrack on it.\nconst pathClass = (path: string): \"ordinary\" | \"credential\" | \"ci\" | \"guardrail\" | \"unknown\" => {\n const p = path.replace(/\\\\/g, \"/\").toLowerCase();\n if (p === \"\") return \"unknown\";\n const segs = p.split(\"/\");\n const base = segs[segs.length - 1];\n if (base === \".env\" || base.startsWith(\".env.\") || CREDENTIAL_EXT.test(base) || CREDENTIAL_FILES.has(base) || segs.some((s) => CREDENTIAL_DIRS.has(s))) return \"credential\";\n if (CI_FILES.has(base) || segs.some((s, i) => s === \".circleci\" || s === \".buildkite\" || (s === \".github\" && (segs[i + 1] === \"workflows\" || segs[i + 1] === \"actions\")))) return \"ci\";\n if (base === \".mcp.json\" || segs.some((s, i) => GUARDRAIL_DIRS.has(s) || (s === \".git\" && (segs[i + 1]?.startsWith(\"hooks\") === true || segs[i + 1]?.startsWith(\"config\") === true)))) return \"guardrail\";\n return \"ordinary\";\n};\n\nconst isAbsolutePath = (p: string): boolean => /^([A-Za-z]:[\\\\/]|\\/|\\\\\\\\|~)/.test(p);\n\nexport interface OperationContext {\n key: BindingKey;\n /** Working directory, used only to derive a keyed workspace-root id. */\n cwd: string;\n /** HEAD commit of the workspace repository, when readable; required for a git operation. */\n headSha?: string | null;\n /** Keyed id of the workspace repository. */\n repositoryId?: string;\n /** Read-only view of the local repository; the system git when absent. */\n probe?: GitProbe;\n /** Whether a pushed ref is protected under the loaded rules; the default names main, master and release/*. */\n isProtectedRef?: (ref: string) => boolean;\n /** Local files and environment the Cloudflare, database and network derivations read; the real ones when absent. */\n files?: FileProbe;\n env?: (name: string) => string | undefined;\n}\n\n/** The context the typed-operation builders take, from an operation context. */\nexport const typedContext = (context: OperationContext): TypedContext => ({\n key: context.key, cwd: context.cwd, repositoryId: context.repositoryId ?? \"\", referenceSetVersion: REFERENCE_SET_VERSION,\n ...(context.probe ? { probe: context.probe } : {}), ...(context.isProtectedRef ? { isProtectedRef: context.isProtectedRef } : {}),\n ...(context.files ? { files: context.files } : {}), ...(context.env ? { env: context.env } : {}),\n});\n\n/**\n * Reduce one dispatched action to its closed typed operation, or null when this hook\n * cannot describe it honestly (an unmapped tool, a git push whose HEAD is unreadable, a\n * host or tool name that is not a plain identifier). The request digest is the request\n * binding over the actual dispatch request, `{action_type, params}` exactly as evaluated.\n */\nexport function buildOperation(action: DispatchedAction, context: OperationContext): Record<string, unknown> | null {\n const { key } = context;\n const request_digest = key.requestDigest({ action_type: action.action_type, params: action.params });\n const common = { environment_class: \"unknown\", reference_set_version: REFERENCE_SET_VERSION, request_digest, digest_key_generation: key.generation };\n const params = action.params;\n switch (action.action_type) {\n case \"shell.exec\": {\n // Judge the name as typed: a credential shape is recognised by its case.\n const typed = (stringParam(params, \"program\") ?? \"\").replace(/^.*[\\\\/]/, \"\").replace(/\\.(exe|cmd|bat|com|ps1)$/i, \"\");\n const raw = typed.toLowerCase();\n const resolved = typed !== \"\" && plain(typed);\n return {\n type: \"shell\", resource_id: key.resourceId(\"shell\", raw), ...common,\n canonical_program: resolved ? raw : \"unresolved\",\n destructive_class: !resolved ? \"unknown\" : PROGRAM_DESTRUCTIVE.has(raw) ? \"unknown\" : \"none\",\n resolution: resolved ? \"resolved\" : \"unresolved\",\n };\n }\n case \"file.read\": case \"file.write\": {\n const path = stringParam(params, \"path\") ?? \"\";\n const resolved = path !== \"\";\n const id = key.resourceId(\"file\", path);\n return {\n type: \"file\", resource_id: id, ...common,\n verb: action.action_type === \"file.read\" ? \"read\" : \"write\",\n target_ids: [id], root_id: key.resourceId(\"root\", context.cwd),\n resolution: resolved ? \"resolved\" : \"unresolved\", path_class: pathClass(path),\n ...(resolved ? { outside_root: isAbsolutePath(path) && !isInside(context.cwd, path) || /(^|[\\\\/])\\.\\.([\\\\/]|$)/.test(path) } : {}),\n };\n }\n case \"git.push\": {\n if (!context.repositoryId) return null;\n return gitPushOperation(params, typedContext(context), context.headSha, { action_type: action.action_type, params });\n }\n // A WebFetch reaches the mapper as host, path and method only. The typed network operation\n // needs the scheme and port, which only the raw tool call carries, so it is derived from that\n // request (deriveTypedOperations) and never guessed here: no request, no operation.\n case \"net.fetch\": return null;\n case \"mcp.tool.call\": {\n const server = stringParam(params, \"server\") ?? \"\";\n const tool = stringParam(params, \"tool\") ?? \"\";\n if (!plain(server) || !plain(tool)) return null;\n return {\n type: \"mcp\", resource_id: key.resourceId(\"mcp\", `${server}\\0${tool}`), ...common,\n server_id: server, tool_name: tool, manifest_version: \"unversioned\", operation_class: \"unknown\", resource_ids: [],\n };\n }\n default:\n return null;\n }\n}\n\nfunction isInside(root: string, path: string): boolean {\n const norm = (s: string): string => trimTrailingSlashes(s.replace(/\\\\/g, \"/\")).toLowerCase();\n const r = norm(root);\n const p = norm(path);\n return p === r || p.startsWith(`${r}/`);\n}\n\n/**\n * One operation (or null) per dispatched item of a tool call. A typed operation derived from\n * the actual request (git commit, package install, GitHub pull request or release) replaces\n * the plain one for the same item; every other item keeps the operation `buildOperation` gives it.\n */\nexport function operationsForCall(\n input: { dispatched: Array<{ action: DispatchedAction }>; request?: CallRequest },\n context: OperationContext & { requiredCheckPolicyVersion?: string; resolvePullRequest?: TypedContext[\"resolvePullRequest\"]; packageManagerVersion?: TypedContext[\"packageManagerVersion\"] },\n): Array<Record<string, unknown> | null> {\n let typed = new Map<number, Record<string, unknown>>();\n try {\n typed = deriveTypedOperations({ ...input.request, dispatched: input.dispatched, redact: redactCommand }, {\n ...typedContext(context),\n ...(context.requiredCheckPolicyVersion ? { requiredCheckPolicyVersion: context.requiredCheckPolicyVersion } : {}),\n ...(context.resolvePullRequest ? { resolvePullRequest: context.resolvePullRequest } : {}),\n ...(context.packageManagerVersion ? { packageManagerVersion: context.packageManagerVersion } : {}),\n });\n } catch { /* a derivation that fails leaves the plain operations */ }\n return input.dispatched.map((item, index) => typed.get(index) ?? buildOperation(item.action, context));\n}\n\nexport const intentData = (operation: Record<string, unknown>) => ({\n event: \"requested\", operation, request_digest: operation.request_digest,\n});\n\nexport type ExitCategory = \"ok\" | \"error\" | \"timeout\" | \"cancelled\" | \"unknown\";\n\nexport const outcomeData = (operation: Record<string, unknown>, exit: ExitCategory, adapterVersion: string, durationMs?: number) => compact({\n event: exit === \"ok\" ? \"completed\" : \"failed\", operation, exit_category: exit, adapter_version: adapterVersion.slice(0, 200),\n duration_ms: durationMs === undefined ? undefined : Math.max(0, Math.min(86_400_000, Math.round(durationMs))),\n});\n"]}
@@ -0,0 +1,15 @@
1
+ import type { Mapped } from "./map.js";
2
+ export type RootScope = "inside" | "outside" | "unresolved";
3
+ export interface RootOptions {
4
+ cwd: string;
5
+ /** Allowed roots. `.` (or an empty string) means the working directory. */
6
+ roots: string[];
7
+ /** Injected for tests; defaults to `fs.realpathSync.native`. */
8
+ realpath?: (path: string) => string;
9
+ }
10
+ /** Classify where a write to `path` lands relative to the allowed roots. */
11
+ export declare function classifyRoot(path: string, options: RootOptions): RootScope;
12
+ /** Set `root_scope` on every file.write intent (the file, a rename destination and a
13
+ * link target alike). Returns new objects; the input is not modified. */
14
+ export declare function applyRootScope(mapped: Mapped[], options: RootOptions): Mapped[];
15
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,YAAY,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,2EAA2E;IAC3E,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACrC;AAyBD,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,SAAS,CAqB1E;AAED;0EAC0E;AAC1E,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,WAAW,GAAG,MAAM,EAAE,CAQ/E"}
package/dist/paths.js ADDED
@@ -0,0 +1,79 @@
1
+ // Workspace-root scope for mutating file intents.
2
+ //
3
+ // A lexical check ("does the path start with the workspace?") is not a boundary: `..`
4
+ // segments, a symlink or an NTFS junction inside the workspace, and a rename whose
5
+ // destination lands elsewhere all leave the text looking in-scope. This module resolves
6
+ // the path physically — the nearest existing ancestor through the filesystem's own
7
+ // realpath, so links and junctions are followed — and compares the result with the
8
+ // allowed roots.
9
+ //
10
+ // Unknown is not clean: a path that cannot be resolved (permission error, loop, a
11
+ // component that is not a directory) is `unresolved`, and the compiled policy denies it
12
+ // like an outside path. Nothing here is consulted unless the rule set lists
13
+ // `allowed_roots`, so the default policy is unchanged.
14
+ import { realpathSync } from "node:fs";
15
+ import { posix, win32 } from "node:path";
16
+ const looksWindows = (p) => /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\") || (p.includes("\\") && !p.startsWith("/"));
17
+ /** Resolve the physical location of `path` (which may not exist yet). Follows links for
18
+ * the nearest existing ancestor and re-attaches the not-yet-created remainder. */
19
+ function physical(path, api, realpath) {
20
+ let probe = path;
21
+ const rest = [];
22
+ for (let guard = 0; guard < 4096; guard++) {
23
+ try {
24
+ const real = realpath(probe);
25
+ return rest.length ? api.join(real, ...rest.reverse()) : real;
26
+ }
27
+ catch (error) {
28
+ const code = error.code;
29
+ if (code !== "ENOENT")
30
+ return null;
31
+ const parent = api.dirname(probe);
32
+ if (parent === probe)
33
+ return null;
34
+ rest.push(api.basename(probe));
35
+ probe = parent;
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ /** Classify where a write to `path` lands relative to the allowed roots. */
41
+ export function classifyRoot(path, options) {
42
+ const realpath = options.realpath ?? ((p) => realpathSync.native(p));
43
+ const windows = looksWindows(options.cwd) || looksWindows(path);
44
+ const api = windows ? win32 : posix;
45
+ if (path === "" || path.includes("\0"))
46
+ return "unresolved";
47
+ // The mapper writes paths with "/"; give the platform API the spelling it expects.
48
+ const spelled = windows ? path.replace(/\//g, "\\") : path;
49
+ const absolute = api.resolve(options.cwd, spelled);
50
+ const target = physical(absolute, api, realpath);
51
+ if (target === null)
52
+ return "unresolved";
53
+ const fold = (p) => (windows ? p.toLowerCase() : p);
54
+ for (const root of options.roots.length ? options.roots : ["."]) {
55
+ const rootAbs = api.resolve(options.cwd, root === "" ? "." : windows ? root.replace(/\//g, "\\") : root);
56
+ const rootPhysical = physical(rootAbs, api, realpath) ?? rootAbs;
57
+ const a = fold(target);
58
+ const b = fold(rootPhysical);
59
+ const sep = windows ? "\\" : "/";
60
+ const prefix = b.endsWith(sep) ? b : b + sep;
61
+ if (a === b || a.startsWith(prefix))
62
+ return "inside";
63
+ }
64
+ return "outside";
65
+ }
66
+ /** Set `root_scope` on every file.write intent (the file, a rename destination and a
67
+ * link target alike). Returns new objects; the input is not modified. */
68
+ export function applyRootScope(mapped, options) {
69
+ return mapped.map((m) => {
70
+ if (m.intent.action_type !== "file.write")
71
+ return m;
72
+ const path = String(m.intent.params.path ?? "");
73
+ // A write whose target the mapper could not name is already not evaluated; keep it.
74
+ if (!m.evaluated)
75
+ return m;
76
+ return { ...m, intent: { ...m.intent, params: { ...m.intent.params, root_scope: classifyRoot(path, options) } } };
77
+ });
78
+ }
79
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"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","sourcesContent":["// Workspace-root scope for mutating file intents.\n//\n// A lexical check (\"does the path start with the workspace?\") is not a boundary: `..`\n// segments, a symlink or an NTFS junction inside the workspace, and a rename whose\n// destination lands elsewhere all leave the text looking in-scope. This module resolves\n// the path physically — the nearest existing ancestor through the filesystem's own\n// realpath, so links and junctions are followed — and compares the result with the\n// allowed roots.\n//\n// Unknown is not clean: a path that cannot be resolved (permission error, loop, a\n// component that is not a directory) is `unresolved`, and the compiled policy denies it\n// like an outside path. Nothing here is consulted unless the rule set lists\n// `allowed_roots`, so the default policy is unchanged.\n\nimport { realpathSync } from \"node:fs\";\nimport { posix, win32 } from \"node:path\";\nimport type { Mapped } from \"./map.js\";\n\nexport type RootScope = \"inside\" | \"outside\" | \"unresolved\";\n\nexport interface RootOptions {\n cwd: string;\n /** Allowed roots. `.` (or an empty string) means the working directory. */\n roots: string[];\n /** Injected for tests; defaults to `fs.realpathSync.native`. */\n realpath?: (path: string) => string;\n}\n\nconst looksWindows = (p: string): boolean => /^[A-Za-z]:[\\\\/]/.test(p) || p.startsWith(\"\\\\\\\\\") || (p.includes(\"\\\\\") && !p.startsWith(\"/\"));\n\n/** Resolve the physical location of `path` (which may not exist yet). Follows links for\n * the nearest existing ancestor and re-attaches the not-yet-created remainder. */\nfunction physical(path: string, api: typeof posix | typeof win32, realpath: (p: string) => string): string | null {\n let probe = path;\n const rest: string[] = [];\n for (let guard = 0; guard < 4096; guard++) {\n try {\n const real = realpath(probe);\n return rest.length ? api.join(real, ...rest.reverse()) : real;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\") return null;\n const parent = api.dirname(probe);\n if (parent === probe) return null;\n rest.push(api.basename(probe));\n probe = parent;\n }\n }\n return null;\n}\n\n/** Classify where a write to `path` lands relative to the allowed roots. */\nexport function classifyRoot(path: string, options: RootOptions): RootScope {\n const realpath = options.realpath ?? ((p: string) => realpathSync.native(p));\n const windows = looksWindows(options.cwd) || looksWindows(path);\n const api = windows ? win32 : posix;\n if (path === \"\" || path.includes(\"\\0\")) return \"unresolved\";\n // The mapper writes paths with \"/\"; give the platform API the spelling it expects.\n const spelled = windows ? path.replace(/\\//g, \"\\\\\") : path;\n const absolute = api.resolve(options.cwd, spelled);\n const target = physical(absolute, api, realpath);\n if (target === null) return \"unresolved\";\n const fold = (p: string): string => (windows ? p.toLowerCase() : p);\n for (const root of options.roots.length ? options.roots : [\".\"]) {\n const rootAbs = api.resolve(options.cwd, root === \"\" ? \".\" : windows ? root.replace(/\\//g, \"\\\\\") : root);\n const rootPhysical = physical(rootAbs, api, realpath) ?? rootAbs;\n const a = fold(target);\n const b = fold(rootPhysical);\n const sep = windows ? \"\\\\\" : \"/\";\n const prefix = b.endsWith(sep) ? b : b + sep;\n if (a === b || a.startsWith(prefix)) return \"inside\";\n }\n return \"outside\";\n}\n\n/** Set `root_scope` on every file.write intent (the file, a rename destination and a\n * link target alike). Returns new objects; the input is not modified. */\nexport function applyRootScope(mapped: Mapped[], options: RootOptions): Mapped[] {\n return mapped.map((m) => {\n if (m.intent.action_type !== \"file.write\") return m;\n const path = String(m.intent.params.path ?? \"\");\n // A write whose target the mapper could not name is already not evaluated; keep it.\n if (!m.evaluated) return m;\n return { ...m, intent: { ...m.intent, params: { ...m.intent.params, root_scope: classifyRoot(path, options) } } };\n });\n}\n"]}
@@ -0,0 +1,60 @@
1
+ import { type PolicyAckInput, type PolicyLoadError } from "./observation.js";
2
+ export declare const POLICY_SCOPE_DOMAIN = "scopebond:policy-scope/v1\n";
3
+ export declare const MAX_EXPORT_BYTES: number;
4
+ export declare const LOADED_POLICY_FILE = "loaded-policy.json";
5
+ export declare const PREVIOUS_POLICY_FILE = "policy.previous.json";
6
+ /** SHA-256 of the domain plus the canonical scope an export was made for. */
7
+ export declare const policyScopeDigest: (scope: {
8
+ export_id: string;
9
+ agent_id: string;
10
+ environment_id: string;
11
+ }) => string;
12
+ export interface ExportFacts {
13
+ exportId: string;
14
+ policyId: string;
15
+ policyVersion: number;
16
+ policyDigest: string;
17
+ scopeDigest: string;
18
+ agentId: string;
19
+ environmentId: string;
20
+ policy: Record<string, unknown>;
21
+ }
22
+ export type Inspection = {
23
+ ok: true;
24
+ facts: ExportFacts;
25
+ }
26
+ /** `ack` is set when the export names enough to echo in a rejection; otherwise nothing is acknowledged. */
27
+ | {
28
+ ok: false;
29
+ error: PolicyLoadError;
30
+ message: string;
31
+ ack?: Omit<PolicyAckInput, "error">;
32
+ };
33
+ /** Accept the export itself, or the workspace's `{ id, configuration }` answer around it. */
34
+ export declare function unwrapExport(raw: unknown): Record<string, unknown> | null;
35
+ /** Check an export without reading or changing anything on disk. Pure. */
36
+ export declare function inspectExport(raw: unknown, context?: {
37
+ environmentId?: string;
38
+ }): Inspection;
39
+ /** Whether the gateway can build a runtime from this policy, in a throwaway home. Never touches a real one. */
40
+ export declare function policyBuilds(policy: Record<string, unknown>): boolean;
41
+ export type LoadOutcome = {
42
+ state: "loaded";
43
+ facts: ExportFacts;
44
+ policyPath: string;
45
+ previous: string | null;
46
+ } | {
47
+ state: "would_load";
48
+ facts: ExportFacts;
49
+ } | {
50
+ state: "rejected";
51
+ error: PolicyLoadError;
52
+ message: string;
53
+ ack?: Omit<PolicyAckInput, "error">;
54
+ };
55
+ /** Read an export file, check it, and (with `apply`) install it as the active policy in `dir`. */
56
+ export declare function loadPolicyExport(dir: string, file: string, options: {
57
+ apply: boolean;
58
+ environmentId?: string;
59
+ }): LoadOutcome;
60
+ //# sourceMappingURL=policy-load.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy-load.d.ts","sourceRoot":"","sources":["../src/policy-load.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAgB,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAE3F,eAAO,MAAM,mBAAmB,gCAAgC,CAAC;AACjE,eAAO,MAAM,gBAAgB,QAAc,CAAC;AAC5C,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AACvD,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAE3D,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,GAAI,OAAO;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,KAAG,MACkF,CAAC;AAK9L,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE;AAClC,2GAA2G;GACzG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC;AAIhG,6FAA6F;AAC7F,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAIzE;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,UAAU,CA4BhG;AAED,+GAA+G;AAC/G,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAarE;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,WAAW,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACpF;IAAE,KAAK,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,GAC3C;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC;AASxG,kGAAkG;AAClG,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,WAAW,CAyB5H"}