@scopebond/hook 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -211,6 +211,8 @@ removed), `status` says **NOT DELIVERING** with the time it stopped and the one
211
211
  it, which is signing in again. `doctor` also checks that the workspace still accepts the connection,
212
212
  not only that it is reachable, and fails when it does not.
213
213
 
214
+ The connection renews itself: in the last 30 days of its 90-day credential the hook renews it during its rules check, proving it still holds the key it enrolled with. With each rules check it also tells the workspace how many records wait to send (counts and one error line, never a record), so the portal can show a computer that checks in but is not delivering.
215
+
214
216
  Signing in puts the hook in your user-level agent settings (`~/.claude/settings.json`, `~/.cursor/hooks.json` or `~/.codex/hooks.json`), so every project on this computer is checked; `--project` connects one project instead. Signing in again never rewrites a settings file that already holds the right hook entry.
215
217
 
216
218
  Enrollment registers both the gateway attester and the hook's separate agent signing key with proof of possession, so Cloud can verify authenticated receipts.
@@ -0,0 +1,11 @@
1
+ import { type HookConnection } from "./cloud.js";
2
+ export declare const REFRESH_WINDOW_MS: number;
3
+ export declare function refreshProof(credentialId: string, gatewayId: string): string;
4
+ export type RefreshOutcome = "not_due" | "renewed" | "unchanged" | "refused" | "unavailable";
5
+ /** Renew the credential when it expires within 30 days. Never throws. */
6
+ export declare function refreshIfDue(dir: string, connection: HookConnection, options?: {
7
+ fetchImpl?: typeof fetch;
8
+ now?: number;
9
+ timeoutMs?: number;
10
+ }): Promise<RefreshOutcome>;
11
+ //# sourceMappingURL=credential-refresh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-refresh.d.ts","sourceRoot":"","sources":["../src/credential-refresh.ts"],"names":[],"mappings":"AASA,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjE,eAAO,MAAM,iBAAiB,QAA2B,CAAC;AAE1D,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,aAAa,CAAC;AAE7F,yEAAyE;AACzE,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EACvC,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3E,OAAO,CAAC,cAAc,CAAC,CAgCzB"}
@@ -0,0 +1,55 @@
1
+ // A machine credential lasts 90 days. In its last 30 the hook renews it during the rules check,
2
+ // proving it still holds the signing key it enrolled with, so a computer never stops delivering
3
+ // because nobody signed it in again. The workspace keeps the old credential valid for a day, so
4
+ // an answer lost on the way back is harmless: the next check tries again.
5
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { canonical } from "@scopebond/gateway";
8
+ import { loadOrCreateAttester } from "@scopebond/gateway/node";
9
+ import { connectionPath } from "./cloud.js";
10
+ export const REFRESH_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
11
+ export function refreshProof(credentialId, gatewayId) {
12
+ return canonical({ type: "scopebond:credential-refresh", version: 1, credential_id: credentialId, gateway_id: gatewayId });
13
+ }
14
+ /** Renew the credential when it expires within 30 days. Never throws. */
15
+ export async function refreshIfDue(dir, connection, options = {}) {
16
+ try {
17
+ const now = options.now ?? Date.now();
18
+ const expires = Date.parse(connection.expires_at ?? "");
19
+ if (!Number.isFinite(expires) || expires - now > REFRESH_WINDOW_MS)
20
+ return "not_due";
21
+ const keyFile = join(dir, "attester.key");
22
+ if (!existsSync(keyFile))
23
+ return "refused";
24
+ const { attester } = loadOrCreateAttester({ file: keyFile });
25
+ const signature = await attester.sign(refreshProof(connection.credential_id, connection.gateway_id));
26
+ const response = await (options.fetchImpl ?? fetch)(new URL("/v1/credential/refresh", connection.url).toString(), {
27
+ method: "POST",
28
+ headers: { authorization: `Bearer ${connection.credential}`, "content-type": "application/json" },
29
+ body: JSON.stringify({ signature }),
30
+ redirect: "error",
31
+ signal: AbortSignal.timeout(options.timeoutMs ?? 5_000),
32
+ });
33
+ if (!response.ok)
34
+ return response.status === 401 || response.status === 409 ? "refused" : "unavailable";
35
+ const body = await response.json();
36
+ if (!body.refreshed)
37
+ return "unchanged";
38
+ if (typeof body.credential !== "string" || !body.credential.startsWith("sbm_") || typeof body.id !== "string" || typeof body.expires_at !== "string")
39
+ return "unavailable";
40
+ // Re-read before writing: another hook process may have renewed it in the meantime.
41
+ const path = connectionPath(dir);
42
+ const current = JSON.parse(readFileSync(path, "utf8"));
43
+ if (current.credential !== connection.credential)
44
+ return "unchanged";
45
+ const next = { ...current, credential: body.credential, credential_id: body.id, expires_at: body.expires_at };
46
+ const temp = `${path}.${process.pid}.tmp`;
47
+ writeFileSync(temp, JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
48
+ renameSync(temp, path);
49
+ return "renewed";
50
+ }
51
+ catch {
52
+ return "unavailable";
53
+ }
54
+ }
55
+ //# sourceMappingURL=credential-refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-refresh.js","sourceRoot":"","sources":["../src/credential-refresh.ts"],"names":[],"mappings":"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","sourcesContent":["// A machine credential lasts 90 days. In its last 30 the hook renews it during the rules check,\n// proving it still holds the signing key it enrolled with, so a computer never stops delivering\n// because nobody signed it in again. The workspace keeps the old credential valid for a day, so\n// an answer lost on the way back is harmless: the next check tries again.\n\nimport { existsSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { canonical } from \"@scopebond/gateway\";\nimport { loadOrCreateAttester } from \"@scopebond/gateway/node\";\nimport { connectionPath, type HookConnection } from \"./cloud.js\";\n\nexport const REFRESH_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;\n\nexport function refreshProof(credentialId: string, gatewayId: string): string {\n return canonical({ type: \"scopebond:credential-refresh\", version: 1, credential_id: credentialId, gateway_id: gatewayId });\n}\n\nexport type RefreshOutcome = \"not_due\" | \"renewed\" | \"unchanged\" | \"refused\" | \"unavailable\";\n\n/** Renew the credential when it expires within 30 days. Never throws. */\nexport async function refreshIfDue(\n dir: string, connection: HookConnection,\n options: { fetchImpl?: typeof fetch; now?: number; timeoutMs?: number } = {},\n): Promise<RefreshOutcome> {\n try {\n const now = options.now ?? Date.now();\n const expires = Date.parse(connection.expires_at ?? \"\");\n if (!Number.isFinite(expires) || expires - now > REFRESH_WINDOW_MS) return \"not_due\";\n const keyFile = join(dir, \"attester.key\");\n if (!existsSync(keyFile)) return \"refused\";\n const { attester } = loadOrCreateAttester({ file: keyFile });\n const signature = await attester.sign(refreshProof(connection.credential_id, connection.gateway_id));\n const response = await (options.fetchImpl ?? fetch)(new URL(\"/v1/credential/refresh\", connection.url).toString(), {\n method: \"POST\",\n headers: { authorization: `Bearer ${connection.credential}`, \"content-type\": \"application/json\" },\n body: JSON.stringify({ signature }),\n redirect: \"error\",\n signal: AbortSignal.timeout(options.timeoutMs ?? 5_000),\n });\n if (!response.ok) return response.status === 401 || response.status === 409 ? \"refused\" : \"unavailable\";\n const body = await response.json() as { refreshed?: boolean; id?: string; credential?: string; expires_at?: string };\n if (!body.refreshed) return \"unchanged\";\n if (typeof body.credential !== \"string\" || !body.credential.startsWith(\"sbm_\") || typeof body.id !== \"string\" || typeof body.expires_at !== \"string\") return \"unavailable\";\n // Re-read before writing: another hook process may have renewed it in the meantime.\n const path = connectionPath(dir);\n const current = JSON.parse(readFileSync(path, \"utf8\")) as HookConnection;\n if (current.credential !== connection.credential) return \"unchanged\";\n const next: HookConnection = { ...current, credential: body.credential, credential_id: body.id, expires_at: body.expires_at };\n const temp = `${path}.${process.pid}.tmp`;\n writeFileSync(temp, JSON.stringify(next, null, 2) + \"\\n\", { mode: 0o600 });\n renameSync(temp, path);\n return \"renewed\";\n } catch {\n return \"unavailable\";\n }\n}\n"]}
@@ -6,6 +6,11 @@ export declare const LOSSLESS_OUTBOX: {
6
6
  readonly maxBytes: number;
7
7
  readonly maxAgeMs: number;
8
8
  };
9
+ /** How many records wait to send and since when, read without changing the queue. */
10
+ export declare function queueStatus(dir: string): {
11
+ pending: number;
12
+ oldest: number | null;
13
+ };
9
14
  export interface DeliveryReport {
10
15
  lines: string[];
11
16
  problems: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"delivery-report.d.ts","sourceRoot":"","sources":["../src/delivery-report.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAIjD,eAAO,MAAM,WAAW,gCAAgC,CAAC;AAEzD,sFAAsF;AACtF,eAAO,MAAM,eAAe;;;;CAAyH,CAAC;AAEtJ,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,wFAAwF;IACxF,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,KAAK,CAAC,EAAE,GAAG,SAAa,GAAG,cAAc,CAmCvH"}
1
+ {"version":3,"file":"delivery-report.d.ts","sourceRoot":"","sources":["../src/delivery-report.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAIjD,eAAO,MAAM,WAAW,gCAAgC,CAAC;AAEzD,sFAAsF;AACtF,eAAO,MAAM,eAAe;;;;CAAyH,CAAC;AAEtJ,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAUnF;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,wFAAwF;IACxF,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,KAAK,CAAC,EAAE,GAAG,SAAa,GAAG,cAAc,CAuBvH"}
@@ -8,27 +8,30 @@ import { cliCommand } from "./version.js";
8
8
  export const OUTBOX_FILE = "receipts.db.cloud-outbox.db";
9
9
  /** The queue opened without limits, so reading it never expires or drops anything. */
10
10
  export const LOSSLESS_OUTBOX = { maxPending: Number.MAX_SAFE_INTEGER, maxBytes: Number.MAX_SAFE_INTEGER, maxAgeMs: Number.MAX_SAFE_INTEGER };
11
- export function describeDelivery(dir, connection, now = Date.now()) {
12
- const state = readDeliveryState(dir);
13
- const lines = [];
14
- const problems = [];
15
- let pending = 0;
16
- let oldest = null;
11
+ /** How many records wait to send and since when, read without changing the queue. */
12
+ export function queueStatus(dir) {
17
13
  const outboxPath = join(dir, OUTBOX_FILE);
18
- if (existsSync(outboxPath)) {
14
+ if (!existsSync(outboxPath))
15
+ return { pending: 0, oldest: null };
16
+ try {
17
+ const outbox = new SqliteCloudOutbox(outboxPath, LOSSLESS_OUTBOX);
19
18
  try {
20
- const outbox = new SqliteCloudOutbox(outboxPath, LOSSLESS_OUTBOX);
21
- try {
22
- const status = outbox.status();
23
- pending = status.pending;
24
- oldest = status.oldestEnqueuedAt;
25
- }
26
- finally {
27
- outbox.close();
28
- }
19
+ const status = outbox.status();
20
+ return { pending: status.pending, oldest: status.oldestEnqueuedAt };
21
+ }
22
+ finally {
23
+ outbox.close();
29
24
  }
30
- catch { /* an unreadable queue leaves the count at zero */ }
31
25
  }
26
+ catch {
27
+ return { pending: 0, oldest: null };
28
+ }
29
+ }
30
+ export function describeDelivery(dir, connection, now = Date.now()) {
31
+ const state = readDeliveryState(dir);
32
+ const lines = [];
33
+ const problems = [];
34
+ const { pending, oldest } = queueStatus(dir);
32
35
  let fix = null;
33
36
  if (state.invalid_since !== null) {
34
37
  fix = cliCommand(`login ${connection.url}`);
@@ -1 +1 @@
1
- {"version":3,"file":"delivery-report.js","sourceRoot":"","sources":["../src/delivery-report.ts"],"names":[],"mappings":"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","sourcesContent":["// SB273/SB274: what delivery to the workspace looks like from this computer — the persisted\n// delivery state plus the queue — in the lines `status` prints and the problems `doctor` reports.\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { SqliteCloudOutbox } from \"@scopebond/gateway/node\";\nimport type { HookConnection } from \"./cloud.js\";\nimport { ago, readDeliveryState } from \"./delivery-state.js\";\nimport { cliCommand } from \"./version.js\";\n\nexport const OUTBOX_FILE = \"receipts.db.cloud-outbox.db\";\n\n/** The queue opened without limits, so reading it never expires or drops anything. */\nexport const LOSSLESS_OUTBOX = { maxPending: Number.MAX_SAFE_INTEGER, maxBytes: Number.MAX_SAFE_INTEGER, maxAgeMs: Number.MAX_SAFE_INTEGER } as const;\n\nexport interface DeliveryReport {\n lines: string[];\n problems: string[];\n /** The one command that fixes a refused connection, when the connection was refused. */\n fix: string | null;\n}\n\nexport function describeDelivery(dir: string, connection: Pick<HookConnection, \"url\">, now = Date.now()): DeliveryReport {\n const state = readDeliveryState(dir);\n const lines: string[] = [];\n const problems: string[] = [];\n let pending = 0;\n let oldest: number | null = null;\n const outboxPath = join(dir, OUTBOX_FILE);\n if (existsSync(outboxPath)) {\n try {\n const outbox = new SqliteCloudOutbox(outboxPath, LOSSLESS_OUTBOX);\n try {\n const status = outbox.status();\n pending = status.pending;\n oldest = status.oldestEnqueuedAt;\n } finally { outbox.close(); }\n } catch { /* an unreadable queue leaves the count at zero */ }\n }\n let fix: string | null = null;\n if (state.invalid_since !== null) {\n fix = cliCommand(`login ${connection.url}`);\n lines.push(`NOT DELIVERING since ${new Date(state.invalid_since).toISOString()}: the workspace refused this computer's connection (it was revoked, replaced or removed).`);\n lines.push(`fix: run ${fix}`);\n problems.push(`the workspace no longer accepts this computer's connection; run ${fix}`);\n }\n const tried = state.last_attempt_at !== null ? `; last tried ${ago(state.last_attempt_at, now)}` : \"\";\n lines.push(`last delivered ${ago(state.last_success_at, now)}${tried}`);\n const age = oldest !== null ? `, oldest from ${ago(oldest, now)}` : \"\";\n lines.push(`waiting to send ${pending} record(s)${age}`);\n if (state.last_error && state.invalid_since === null) {\n lines.push(`last problem ${state.last_error}`);\n if (pending > 0 && oldest !== null && now - oldest > 60 * 60 * 1000) {\n problems.push(`${pending} record(s) have waited more than an hour to send (${state.last_error}); they stay queued and send once the workspace accepts them`);\n }\n }\n return { lines, problems, fix };\n}\n"]}
1
+ {"version":3,"file":"delivery-report.js","sourceRoot":"","sources":["../src/delivery-report.ts"],"names":[],"mappings":"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","sourcesContent":["// SB273/SB274: what delivery to the workspace looks like from this computer — the persisted\n// delivery state plus the queue — in the lines `status` prints and the problems `doctor` reports.\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { SqliteCloudOutbox } from \"@scopebond/gateway/node\";\nimport type { HookConnection } from \"./cloud.js\";\nimport { ago, readDeliveryState } from \"./delivery-state.js\";\nimport { cliCommand } from \"./version.js\";\n\nexport const OUTBOX_FILE = \"receipts.db.cloud-outbox.db\";\n\n/** The queue opened without limits, so reading it never expires or drops anything. */\nexport const LOSSLESS_OUTBOX = { maxPending: Number.MAX_SAFE_INTEGER, maxBytes: Number.MAX_SAFE_INTEGER, maxAgeMs: Number.MAX_SAFE_INTEGER } as const;\n\n/** How many records wait to send and since when, read without changing the queue. */\nexport function queueStatus(dir: string): { pending: number; oldest: number | null } {\n const outboxPath = join(dir, OUTBOX_FILE);\n if (!existsSync(outboxPath)) return { pending: 0, oldest: null };\n try {\n const outbox = new SqliteCloudOutbox(outboxPath, LOSSLESS_OUTBOX);\n try {\n const status = outbox.status();\n return { pending: status.pending, oldest: status.oldestEnqueuedAt };\n } finally { outbox.close(); }\n } catch { return { pending: 0, oldest: null }; }\n}\n\nexport interface DeliveryReport {\n lines: string[];\n problems: string[];\n /** The one command that fixes a refused connection, when the connection was refused. */\n fix: string | null;\n}\n\nexport function describeDelivery(dir: string, connection: Pick<HookConnection, \"url\">, now = Date.now()): DeliveryReport {\n const state = readDeliveryState(dir);\n const lines: string[] = [];\n const problems: string[] = [];\n const { pending, oldest } = queueStatus(dir);\n let fix: string | null = null;\n if (state.invalid_since !== null) {\n fix = cliCommand(`login ${connection.url}`);\n lines.push(`NOT DELIVERING since ${new Date(state.invalid_since).toISOString()}: the workspace refused this computer's connection (it was revoked, replaced or removed).`);\n lines.push(`fix: run ${fix}`);\n problems.push(`the workspace no longer accepts this computer's connection; run ${fix}`);\n }\n const tried = state.last_attempt_at !== null ? `; last tried ${ago(state.last_attempt_at, now)}` : \"\";\n lines.push(`last delivered ${ago(state.last_success_at, now)}${tried}`);\n const age = oldest !== null ? `, oldest from ${ago(oldest, now)}` : \"\";\n lines.push(`waiting to send ${pending} record(s)${age}`);\n if (state.last_error && state.invalid_since === null) {\n lines.push(`last problem ${state.last_error}`);\n if (pending > 0 && oldest !== null && now - oldest > 60 * 60 * 1000) {\n problems.push(`${pending} record(s) have waited more than an hour to send (${state.last_error}); they stay queued and send once the workspace accepts them`);\n }\n }\n return { lines, problems, fix };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"policy-sync.d.ts","sourceRoot":"","sources":["../src/policy-sync.ts"],"names":[],"mappings":"AAoBA,OAAO,EAC2F,KAAK,aAAa,EACnH,MAAM,cAAc,CAAC;AAEtB,eAAO,MAAM,gBAAgB,QAAgB,CAAC;AAI9C,+GAA+G;AAC/G,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AAGtC,MAAM,MAAM,WAAW,GACnB;IAAE,KAAK,EAAE,eAAe,CAAA;CAAE,GAC1B;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACxC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrF;IAAE,KAAK,EAAE,cAAc,CAAA;CAAE,GACzB;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;IAC3D,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,yGAAyG;IACzG,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CA+FxF;AAED;;;yEAGyE;AACzE,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,WAAW,EAAE,QAAQ,SAAmE,EAAE,GAAG,SAAa,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAyBvM;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEjD"}
1
+ {"version":3,"file":"policy-sync.d.ts","sourceRoot":"","sources":["../src/policy-sync.ts"],"names":[],"mappings":"AAqCA,OAAO,EAC2F,KAAK,aAAa,EACnH,MAAM,cAAc,CAAC;AAEtB,eAAO,MAAM,gBAAgB,QAAgB,CAAC;AAI9C,+GAA+G;AAC/G,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AAGtC,MAAM,MAAM,WAAW,GACnB;IAAE,KAAK,EAAE,eAAe,CAAA;CAAE,GAC1B;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACxC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrF;IAAE,KAAK,EAAE,cAAc,CAAA;CAAE,GACzB;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC;IAC3D,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,yGAAyG;IACzG,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAiGxF;AAED;;;yEAGyE;AACzE,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,WAAW,EAAE,QAAQ,SAAmE,EAAE,GAAG,SAAa,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAyBvM;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEjD"}
@@ -16,7 +16,26 @@
16
16
  import { closeSync, existsSync, openSync, rmSync, statSync, writeSync } from "node:fs";
17
17
  import { join } from "node:path";
18
18
  import { connectionPath, loadConnection } from "./cloud.js";
19
- import { recordRulesCredential } from "./delivery-state.js";
19
+ import { readDeliveryState, recordRulesCredential } from "./delivery-state.js";
20
+ import { queueStatus } from "./delivery-report.js";
21
+ import { refreshIfDue } from "./credential-refresh.js";
22
+ /** What this computer sends the workspace about its own delivery queue with each rules check, so
23
+ * the portal can say "checking in but not delivering" instead of "reporting". Counts and one
24
+ * error line only; never a record. */
25
+ function deliveryHeaders(dir) {
26
+ try {
27
+ const { pending, oldest } = queueStatus(dir);
28
+ const state = readDeliveryState(dir);
29
+ return {
30
+ "x-scopebond-pending": String(pending),
31
+ ...(oldest !== null ? { "x-scopebond-oldest-pending-at": String(oldest) } : {}),
32
+ ...(state.last_error ? { "x-scopebond-last-error": state.last_error.replace(/[^\x20-\x7e]/g, " ").slice(0, 200) } : {}),
33
+ };
34
+ }
35
+ catch {
36
+ return {};
37
+ }
38
+ }
20
39
  import { inspectManaged, installManaged, isManaged, readMeta, restoreLocal, writeMeta, } from "./managed.js";
21
40
  export const SYNC_INTERVAL_MS = 5 * 60 * 1000;
22
41
  const LOCK_FILE = "managed-sync.lock";
@@ -60,7 +79,7 @@ export async function syncPolicy(dir, options) {
60
79
  res = await fetchImpl(`${base}/v1/policy`, {
61
80
  // The hook's version tells the workspace which settings this computer understands (for example exact-target
62
81
  // exclusions), so it is never sent a document an older hook would refuse.
63
- headers: { ...auth, ...(options.hookVersion ? { "x-scopebond-hook-version": options.hookVersion } : {}), ...(isManaged(dir) && meta.etag ? { "if-none-match": meta.etag } : {}) },
82
+ headers: { ...auth, ...(options.hookVersion ? { "x-scopebond-hook-version": options.hookVersion } : {}), ...(isManaged(dir) && meta.etag ? { "if-none-match": meta.etag } : {}), ...deliveryHeaders(dir) },
64
83
  redirect: "error", signal: AbortSignal.timeout(options.timeoutMs ?? REQUEST_TIMEOUT_MS),
65
84
  });
66
85
  }
@@ -69,6 +88,9 @@ export async function syncPolicy(dir, options) {
69
88
  return { state: "unavailable", message: "could not reach the workspace" };
70
89
  }
71
90
  recordRulesCredential(dir, res.status !== 401, now.getTime());
91
+ // A working connection renews its credential in its last 30 days (the workspace answers "not due" before that).
92
+ if (res.status !== 401)
93
+ await refreshIfDue(dir, connection, { fetchImpl, now: now.getTime(), timeoutMs: options.timeoutMs ?? REQUEST_TIMEOUT_MS });
72
94
  if (res.status === 401) {
73
95
  backToOwnRules();
74
96
  save({ revision: null, rules_digest: null, export_id: null, etag: null, last_error: "the workspace connection is no longer valid; this computer uses its own rules" });
@@ -1 +1 @@
1
- {"version":3,"file":"policy-sync.js","sourceRoot":"","sources":["../src/policy-sync.ts"],"names":[],"mappings":"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","sourcesContent":["// Keep a connected computer's rules in step with its Scopebond Cloud workspace.\n//\n// `syncPolicy` asks the workspace for this computer's rules (`GET /v1/policy`), installs a newer version when there is one, and\n// tells the workspace exactly what it loaded or why it could not (`POST /v1/policy/ack`). A tool call checks two small files;\n// at most every five minutes it also runs the check (`syncIfDue`), in parallel with the delivery of its activity record and\n// capped at about one and a half seconds, so a change made in the workspace applies within a few minutes of the agent's next\n// action. It runs in the hook's own process on purpose: a detached helper process inherits the hook's output pipe on Windows,\n// which makes the coding agent wait for the helper. Any failure or timeout keeps the rules already in force; an interrupted\n// write cannot leave a half-written policy, because every write is a temp file and a rename.\n//\n// Answers from the workspace:\n// 204 the workspace does not set rules for this computer: use (or go back to) its own rules\n// 304 nothing changed since the last check\n// 200 a rules document: checked, installed if newer, confirmed\n// 401 the connection is no longer valid (revoked, expired, removed): go back to this computer's own rules\n\nimport { closeSync, existsSync, openSync, rmSync, statSync, writeSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { connectionPath, loadConnection } from \"./cloud.js\";\nimport { recordRulesCredential } from \"./delivery-state.js\";\nimport {\n inspectManaged, installManaged, isManaged, readMeta, restoreLocal, writeMeta, type ManagedMeta, type RefusalReason,\n} from \"./managed.js\";\n\nexport const SYNC_INTERVAL_MS = 5 * 60 * 1000;\nconst LOCK_FILE = \"managed-sync.lock\";\nconst LOCK_STALE_MS = 2 * 60 * 1000;\nconst REQUEST_TIMEOUT_MS = 5_000;\n/** How long a tool call may spend on the five-minute check, at most (`SCOPEBOND_POLICY_SYNC_MS` overrides). */\nexport const INLINE_BUDGET_MS = 1_500;\nconst UNMANAGED_DIGEST = \"0\".repeat(64);\n\nexport type SyncOutcome =\n | { state: \"not_connected\" }\n | { state: \"own_rules\"; changed: boolean }\n | { state: \"unchanged\"; revision: number }\n | { state: \"applied\"; revision: number }\n | { state: \"refused\"; revision: number | null; reason: RefusalReason; message: string }\n | { state: \"disconnected\" }\n | { state: \"unavailable\"; message: string };\n\nexport interface SyncOptions {\n agentKid: string;\n hookVersion: string;\n policyBuilds: (policy: Record<string, unknown>) => boolean;\n fetchImpl?: typeof fetch;\n now?: () => Date;\n /** Recompute a project's trust pin after its policy.json changed (the same step `rules apply` takes). */\n afterPolicyWrite?: (dir: string) => void;\n /** Per-request timeout; defaults to five seconds. */\n timeoutMs?: number;\n}\n\nexport async function syncPolicy(dir: string, options: SyncOptions): Promise<SyncOutcome> {\n const connection = loadConnection(dir);\n if (!connection) return { state: \"not_connected\" };\n const fetchImpl = options.fetchImpl ?? fetch;\n const now = (options.now ?? (() => new Date()))();\n const installationId = (connection as { installation_id?: string }).installation_id ?? connection.gateway_id;\n const base = connection.url.replace(/\\/+$/, \"\");\n const auth = { authorization: `Bearer ${connection.credential}` };\n const meta = readMeta(dir);\n const save = (patch: Partial<ManagedMeta>) => writeMeta(dir, { ...meta, ...patch, checked_at: now.toISOString() });\n\n const ack = async (body: { export_id: string; revision: number; rules_digest: string; result: \"loaded\" | \"rejected\"; reason?: RefusalReason }): Promise<ManagedMeta[\"last_ack\"]> => {\n try {\n const res = await fetchImpl(`${base}/v1/policy/ack`, {\n method: \"POST\", headers: { ...auth, \"content-type\": \"application/json\" }, redirect: \"error\", signal: AbortSignal.timeout(options.timeoutMs ?? REQUEST_TIMEOUT_MS),\n body: JSON.stringify({ ...body, reason: body.reason ?? null, hook_version: options.hookVersion }),\n });\n return res.ok ? { revision: body.revision, result: body.result, at: now.toISOString() } : meta.last_ack;\n } catch { return meta.last_ack; }\n };\n const backToOwnRules = (): boolean => {\n if (!isManaged(dir)) return false;\n restoreLocal(dir, options.agentKid);\n options.afterPolicyWrite?.(dir);\n return true;\n };\n\n let res: Response;\n try {\n res = await fetchImpl(`${base}/v1/policy`, {\n // The hook's version tells the workspace which settings this computer understands (for example exact-target\n // exclusions), so it is never sent a document an older hook would refuse.\n headers: { ...auth, ...(options.hookVersion ? { \"x-scopebond-hook-version\": options.hookVersion } : {}), ...(isManaged(dir) && meta.etag ? { \"if-none-match\": meta.etag } : {}) },\n redirect: \"error\", signal: AbortSignal.timeout(options.timeoutMs ?? REQUEST_TIMEOUT_MS),\n });\n } catch (error) {\n save({ last_error: `could not reach the workspace (${(error as Error).name}); the rules in force stay` });\n return { state: \"unavailable\", message: \"could not reach the workspace\" };\n }\n\n recordRulesCredential(dir, res.status !== 401, now.getTime());\n if (res.status === 401) {\n backToOwnRules();\n save({ revision: null, rules_digest: null, export_id: null, etag: null, last_error: \"the workspace connection is no longer valid; this computer uses its own rules\" });\n return { state: \"disconnected\" };\n }\n if (res.status === 204) {\n const changed = backToOwnRules();\n const last = meta.last_ack;\n const confirmed = changed || !last || last.revision !== 0\n ? await ack({ export_id: `rev-0-${installationId}`, revision: 0, rules_digest: UNMANAGED_DIGEST, result: \"loaded\" }) : last;\n save({ revision: null, rules_digest: null, export_id: null, etag: null, last_ack: confirmed, last_error: null });\n return { state: \"own_rules\", changed };\n }\n if (res.status === 304 && isManaged(dir) && meta.revision !== null) {\n const confirmed = meta.last_ack?.revision === meta.revision && meta.last_ack.result === \"loaded\" ? meta.last_ack\n : await ack({ export_id: meta.export_id!, revision: meta.revision, rules_digest: meta.rules_digest!, result: \"loaded\" });\n save({ last_ack: confirmed, last_error: null });\n return { state: \"unchanged\", revision: meta.revision };\n }\n if (res.status !== 200) {\n save({ last_error: `the workspace answered ${res.status}; the rules in force stay` });\n return { state: \"unavailable\", message: `the workspace answered ${res.status}` };\n }\n\n let raw: unknown;\n try { raw = await res.json(); } catch { raw = null; }\n const r = (raw && typeof raw === \"object\" ? raw : {}) as Record<string, unknown>;\n const etag = res.headers.get(\"etag\");\n // The same version already in force: confirm it again if the last confirmation did not land.\n if (isManaged(dir) && r.revision === meta.revision && r.rules_digest === meta.rules_digest && meta.revision !== null) {\n const confirmed = meta.last_ack?.revision === meta.revision ? meta.last_ack\n : await ack({ export_id: meta.export_id!, revision: meta.revision, rules_digest: meta.rules_digest!, result: \"loaded\" });\n save({ etag, last_ack: confirmed, last_error: null });\n return { state: \"unchanged\", revision: meta.revision };\n }\n const inspected = inspectManaged(raw, { installationId, currentRevision: isManaged(dir) ? meta.revision : null, currentDigest: isManaged(dir) ? meta.rules_digest : null });\n const echo = typeof r.export_id === \"string\" && Number.isInteger(r.revision) && typeof r.rules_digest === \"string\" && /^[0-9a-f]{64}$/.test(r.rules_digest)\n ? { export_id: r.export_id, revision: r.revision as number, rules_digest: r.rules_digest } : null;\n if (!inspected.ok) {\n const confirmed = echo ? await ack({ ...echo, result: \"rejected\", reason: inspected.reason }) : meta.last_ack;\n save({ last_ack: confirmed, last_error: inspected.message });\n return { state: \"refused\", revision: echo?.revision ?? null, reason: inspected.reason, message: inspected.message };\n }\n const doc = inspected.doc;\n const installed = installManaged(dir, doc, options.agentKid, options.policyBuilds);\n if (!installed.ok) {\n const confirmed = await ack({ export_id: doc.export_id, revision: doc.revision, rules_digest: doc.rules_digest, result: \"rejected\", reason: installed.reason });\n save({ last_ack: confirmed, last_error: installed.message });\n return { state: \"refused\", revision: doc.revision, reason: installed.reason, message: installed.message };\n }\n options.afterPolicyWrite?.(dir);\n const confirmed = await ack({ export_id: doc.export_id, revision: doc.revision, rules_digest: doc.rules_digest, result: \"loaded\" });\n save({ revision: doc.revision, rules_digest: doc.rules_digest, export_id: doc.export_id, etag, last_ack: confirmed, last_error: null });\n return { state: \"applied\", revision: doc.revision };\n}\n\n/** Called on every tool call: when this computer is connected and has not checked in five minutes, run the check, capped at\n * `budgetMs`. Resolves `null` when no check was due, another one is running, or the cap was reached first (the check then\n * finishes if the process lives long enough, else runs again on a later call). Never throws. `makeOptions` is called only when a\n * check is due, so an ordinary call pays nothing but two file reads. */\nexport async function syncIfDue(dir: string, makeOptions: () => SyncOptions, budgetMs = Number(process.env.SCOPEBOND_POLICY_SYNC_MS ?? INLINE_BUDGET_MS), now = Date.now()): Promise<SyncOutcome | null> {\n try {\n if (process.env.SCOPEBOND_POLICY_SYNC === \"off\") return null;\n if (!existsSync(connectionPath(dir))) return null;\n const checked = readMeta(dir).checked_at;\n if (checked && now - Date.parse(checked) < SYNC_INTERVAL_MS) return null;\n const lock = join(dir, LOCK_FILE);\n if (existsSync(lock)) {\n if (now - statSync(lock).mtimeMs < LOCK_STALE_MS) return null;\n rmSync(lock, { force: true });\n }\n const fd = openSync(lock, \"wx\");\n writeSync(fd, String(process.pid));\n closeSync(fd);\n const budget = Number.isFinite(budgetMs) && budgetMs > 0 ? budgetMs : INLINE_BUDGET_MS;\n const work = Promise.resolve()\n .then(() => syncPolicy(dir, { ...makeOptions(), timeoutMs: budget }))\n .catch((): SyncOutcome => ({ state: \"unavailable\", message: \"the check failed\" }))\n .finally(() => releaseSyncLock(dir));\n let timer: ReturnType<typeof setTimeout> | undefined;\n const capped = new Promise<null>((resolve) => { timer = setTimeout(() => resolve(null), budget); });\n const outcome = await Promise.race([work, capped]);\n if (timer) clearTimeout(timer);\n return outcome;\n } catch { return null; }\n}\n\nexport function releaseSyncLock(dir: string): void {\n try { rmSync(join(dir, LOCK_FILE), { force: true }); } catch { /* expires on its own */ }\n}\n"]}
1
+ {"version":3,"file":"policy-sync.js","sourceRoot":"","sources":["../src/policy-sync.ts"],"names":[],"mappings":"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","sourcesContent":["// Keep a connected computer's rules in step with its Scopebond Cloud workspace.\n//\n// `syncPolicy` asks the workspace for this computer's rules (`GET /v1/policy`), installs a newer version when there is one, and\n// tells the workspace exactly what it loaded or why it could not (`POST /v1/policy/ack`). A tool call checks two small files;\n// at most every five minutes it also runs the check (`syncIfDue`), in parallel with the delivery of its activity record and\n// capped at about one and a half seconds, so a change made in the workspace applies within a few minutes of the agent's next\n// action. It runs in the hook's own process on purpose: a detached helper process inherits the hook's output pipe on Windows,\n// which makes the coding agent wait for the helper. Any failure or timeout keeps the rules already in force; an interrupted\n// write cannot leave a half-written policy, because every write is a temp file and a rename.\n//\n// Answers from the workspace:\n// 204 the workspace does not set rules for this computer: use (or go back to) its own rules\n// 304 nothing changed since the last check\n// 200 a rules document: checked, installed if newer, confirmed\n// 401 the connection is no longer valid (revoked, expired, removed): go back to this computer's own rules\n\nimport { closeSync, existsSync, openSync, rmSync, statSync, writeSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { connectionPath, loadConnection } from \"./cloud.js\";\nimport { readDeliveryState, recordRulesCredential } from \"./delivery-state.js\";\nimport { queueStatus } from \"./delivery-report.js\";\nimport { refreshIfDue } from \"./credential-refresh.js\";\n\n/** What this computer sends the workspace about its own delivery queue with each rules check, so\n * the portal can say \"checking in but not delivering\" instead of \"reporting\". Counts and one\n * error line only; never a record. */\nfunction deliveryHeaders(dir: string): Record<string, string> {\n try {\n const { pending, oldest } = queueStatus(dir);\n const state = readDeliveryState(dir);\n return {\n \"x-scopebond-pending\": String(pending),\n ...(oldest !== null ? { \"x-scopebond-oldest-pending-at\": String(oldest) } : {}),\n ...(state.last_error ? { \"x-scopebond-last-error\": state.last_error.replace(/[^\\x20-\\x7e]/g, \" \").slice(0, 200) } : {}),\n };\n } catch { return {}; }\n}\nimport {\n inspectManaged, installManaged, isManaged, readMeta, restoreLocal, writeMeta, type ManagedMeta, type RefusalReason,\n} from \"./managed.js\";\n\nexport const SYNC_INTERVAL_MS = 5 * 60 * 1000;\nconst LOCK_FILE = \"managed-sync.lock\";\nconst LOCK_STALE_MS = 2 * 60 * 1000;\nconst REQUEST_TIMEOUT_MS = 5_000;\n/** How long a tool call may spend on the five-minute check, at most (`SCOPEBOND_POLICY_SYNC_MS` overrides). */\nexport const INLINE_BUDGET_MS = 1_500;\nconst UNMANAGED_DIGEST = \"0\".repeat(64);\n\nexport type SyncOutcome =\n | { state: \"not_connected\" }\n | { state: \"own_rules\"; changed: boolean }\n | { state: \"unchanged\"; revision: number }\n | { state: \"applied\"; revision: number }\n | { state: \"refused\"; revision: number | null; reason: RefusalReason; message: string }\n | { state: \"disconnected\" }\n | { state: \"unavailable\"; message: string };\n\nexport interface SyncOptions {\n agentKid: string;\n hookVersion: string;\n policyBuilds: (policy: Record<string, unknown>) => boolean;\n fetchImpl?: typeof fetch;\n now?: () => Date;\n /** Recompute a project's trust pin after its policy.json changed (the same step `rules apply` takes). */\n afterPolicyWrite?: (dir: string) => void;\n /** Per-request timeout; defaults to five seconds. */\n timeoutMs?: number;\n}\n\nexport async function syncPolicy(dir: string, options: SyncOptions): Promise<SyncOutcome> {\n const connection = loadConnection(dir);\n if (!connection) return { state: \"not_connected\" };\n const fetchImpl = options.fetchImpl ?? fetch;\n const now = (options.now ?? (() => new Date()))();\n const installationId = (connection as { installation_id?: string }).installation_id ?? connection.gateway_id;\n const base = connection.url.replace(/\\/+$/, \"\");\n const auth = { authorization: `Bearer ${connection.credential}` };\n const meta = readMeta(dir);\n const save = (patch: Partial<ManagedMeta>) => writeMeta(dir, { ...meta, ...patch, checked_at: now.toISOString() });\n\n const ack = async (body: { export_id: string; revision: number; rules_digest: string; result: \"loaded\" | \"rejected\"; reason?: RefusalReason }): Promise<ManagedMeta[\"last_ack\"]> => {\n try {\n const res = await fetchImpl(`${base}/v1/policy/ack`, {\n method: \"POST\", headers: { ...auth, \"content-type\": \"application/json\" }, redirect: \"error\", signal: AbortSignal.timeout(options.timeoutMs ?? REQUEST_TIMEOUT_MS),\n body: JSON.stringify({ ...body, reason: body.reason ?? null, hook_version: options.hookVersion }),\n });\n return res.ok ? { revision: body.revision, result: body.result, at: now.toISOString() } : meta.last_ack;\n } catch { return meta.last_ack; }\n };\n const backToOwnRules = (): boolean => {\n if (!isManaged(dir)) return false;\n restoreLocal(dir, options.agentKid);\n options.afterPolicyWrite?.(dir);\n return true;\n };\n\n let res: Response;\n try {\n res = await fetchImpl(`${base}/v1/policy`, {\n // The hook's version tells the workspace which settings this computer understands (for example exact-target\n // exclusions), so it is never sent a document an older hook would refuse.\n headers: { ...auth, ...(options.hookVersion ? { \"x-scopebond-hook-version\": options.hookVersion } : {}), ...(isManaged(dir) && meta.etag ? { \"if-none-match\": meta.etag } : {}), ...deliveryHeaders(dir) },\n redirect: \"error\", signal: AbortSignal.timeout(options.timeoutMs ?? REQUEST_TIMEOUT_MS),\n });\n } catch (error) {\n save({ last_error: `could not reach the workspace (${(error as Error).name}); the rules in force stay` });\n return { state: \"unavailable\", message: \"could not reach the workspace\" };\n }\n\n recordRulesCredential(dir, res.status !== 401, now.getTime());\n // A working connection renews its credential in its last 30 days (the workspace answers \"not due\" before that).\n if (res.status !== 401) await refreshIfDue(dir, connection, { fetchImpl, now: now.getTime(), timeoutMs: options.timeoutMs ?? REQUEST_TIMEOUT_MS });\n if (res.status === 401) {\n backToOwnRules();\n save({ revision: null, rules_digest: null, export_id: null, etag: null, last_error: \"the workspace connection is no longer valid; this computer uses its own rules\" });\n return { state: \"disconnected\" };\n }\n if (res.status === 204) {\n const changed = backToOwnRules();\n const last = meta.last_ack;\n const confirmed = changed || !last || last.revision !== 0\n ? await ack({ export_id: `rev-0-${installationId}`, revision: 0, rules_digest: UNMANAGED_DIGEST, result: \"loaded\" }) : last;\n save({ revision: null, rules_digest: null, export_id: null, etag: null, last_ack: confirmed, last_error: null });\n return { state: \"own_rules\", changed };\n }\n if (res.status === 304 && isManaged(dir) && meta.revision !== null) {\n const confirmed = meta.last_ack?.revision === meta.revision && meta.last_ack.result === \"loaded\" ? meta.last_ack\n : await ack({ export_id: meta.export_id!, revision: meta.revision, rules_digest: meta.rules_digest!, result: \"loaded\" });\n save({ last_ack: confirmed, last_error: null });\n return { state: \"unchanged\", revision: meta.revision };\n }\n if (res.status !== 200) {\n save({ last_error: `the workspace answered ${res.status}; the rules in force stay` });\n return { state: \"unavailable\", message: `the workspace answered ${res.status}` };\n }\n\n let raw: unknown;\n try { raw = await res.json(); } catch { raw = null; }\n const r = (raw && typeof raw === \"object\" ? raw : {}) as Record<string, unknown>;\n const etag = res.headers.get(\"etag\");\n // The same version already in force: confirm it again if the last confirmation did not land.\n if (isManaged(dir) && r.revision === meta.revision && r.rules_digest === meta.rules_digest && meta.revision !== null) {\n const confirmed = meta.last_ack?.revision === meta.revision ? meta.last_ack\n : await ack({ export_id: meta.export_id!, revision: meta.revision, rules_digest: meta.rules_digest!, result: \"loaded\" });\n save({ etag, last_ack: confirmed, last_error: null });\n return { state: \"unchanged\", revision: meta.revision };\n }\n const inspected = inspectManaged(raw, { installationId, currentRevision: isManaged(dir) ? meta.revision : null, currentDigest: isManaged(dir) ? meta.rules_digest : null });\n const echo = typeof r.export_id === \"string\" && Number.isInteger(r.revision) && typeof r.rules_digest === \"string\" && /^[0-9a-f]{64}$/.test(r.rules_digest)\n ? { export_id: r.export_id, revision: r.revision as number, rules_digest: r.rules_digest } : null;\n if (!inspected.ok) {\n const confirmed = echo ? await ack({ ...echo, result: \"rejected\", reason: inspected.reason }) : meta.last_ack;\n save({ last_ack: confirmed, last_error: inspected.message });\n return { state: \"refused\", revision: echo?.revision ?? null, reason: inspected.reason, message: inspected.message };\n }\n const doc = inspected.doc;\n const installed = installManaged(dir, doc, options.agentKid, options.policyBuilds);\n if (!installed.ok) {\n const confirmed = await ack({ export_id: doc.export_id, revision: doc.revision, rules_digest: doc.rules_digest, result: \"rejected\", reason: installed.reason });\n save({ last_ack: confirmed, last_error: installed.message });\n return { state: \"refused\", revision: doc.revision, reason: installed.reason, message: installed.message };\n }\n options.afterPolicyWrite?.(dir);\n const confirmed = await ack({ export_id: doc.export_id, revision: doc.revision, rules_digest: doc.rules_digest, result: \"loaded\" });\n save({ revision: doc.revision, rules_digest: doc.rules_digest, export_id: doc.export_id, etag, last_ack: confirmed, last_error: null });\n return { state: \"applied\", revision: doc.revision };\n}\n\n/** Called on every tool call: when this computer is connected and has not checked in five minutes, run the check, capped at\n * `budgetMs`. Resolves `null` when no check was due, another one is running, or the cap was reached first (the check then\n * finishes if the process lives long enough, else runs again on a later call). Never throws. `makeOptions` is called only when a\n * check is due, so an ordinary call pays nothing but two file reads. */\nexport async function syncIfDue(dir: string, makeOptions: () => SyncOptions, budgetMs = Number(process.env.SCOPEBOND_POLICY_SYNC_MS ?? INLINE_BUDGET_MS), now = Date.now()): Promise<SyncOutcome | null> {\n try {\n if (process.env.SCOPEBOND_POLICY_SYNC === \"off\") return null;\n if (!existsSync(connectionPath(dir))) return null;\n const checked = readMeta(dir).checked_at;\n if (checked && now - Date.parse(checked) < SYNC_INTERVAL_MS) return null;\n const lock = join(dir, LOCK_FILE);\n if (existsSync(lock)) {\n if (now - statSync(lock).mtimeMs < LOCK_STALE_MS) return null;\n rmSync(lock, { force: true });\n }\n const fd = openSync(lock, \"wx\");\n writeSync(fd, String(process.pid));\n closeSync(fd);\n const budget = Number.isFinite(budgetMs) && budgetMs > 0 ? budgetMs : INLINE_BUDGET_MS;\n const work = Promise.resolve()\n .then(() => syncPolicy(dir, { ...makeOptions(), timeoutMs: budget }))\n .catch((): SyncOutcome => ({ state: \"unavailable\", message: \"the check failed\" }))\n .finally(() => releaseSyncLock(dir));\n let timer: ReturnType<typeof setTimeout> | undefined;\n const capped = new Promise<null>((resolve) => { timer = setTimeout(() => resolve(null), budget); });\n const outcome = await Promise.race([work, capped]);\n if (timer) clearTimeout(timer);\n return outcome;\n } catch { return null; }\n}\n\nexport function releaseSyncLock(dir: string): void {\n try { rmSync(join(dir, LOCK_FILE), { force: true }); } catch { /* expires on its own */ }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scopebond/hook",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Scopebond hook for Claude Code, Cursor, and OpenAI Codex — checks coding-agent actions against policy before they run and records a signed receipt locally.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Avouro LLC",
@@ -42,9 +42,9 @@
42
42
  "dist"
43
43
  ],
44
44
  "dependencies": {
45
+ "@scopebond/policy-schema": "^0.5.0",
45
46
  "@scopebond/sdk": "^0.1.3",
46
- "@scopebond/gateway": "^0.10.0",
47
- "@scopebond/policy-schema": "^0.5.0"
47
+ "@scopebond/gateway": "^0.11.0"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"