@scopebond/hook 0.10.0 → 0.11.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 +13 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +57 -6
- package/dist/cli.js.map +1 -1
- package/dist/cloud.d.ts +24 -2
- package/dist/cloud.d.ts.map +1 -1
- package/dist/cloud.js +71 -8
- package/dist/cloud.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/managed.d.ts +3 -1
- package/dist/managed.d.ts.map +1 -1
- package/dist/managed.js +50 -4
- package/dist/managed.js.map +1 -1
- package/dist/obs-emitter.d.ts.map +1 -1
- package/dist/obs-emitter.js +2 -2
- package/dist/obs-emitter.js.map +1 -1
- package/dist/policy-sync.d.ts.map +1 -1
- package/dist/policy-sync.js +4 -2
- package/dist/policy-sync.js.map +1 -1
- package/dist/proof.d.ts +1 -1
- package/dist/proof.d.ts.map +1 -1
- package/dist/proof.js +2 -1
- package/dist/proof.js.map +1 -1
- package/dist/recover.d.ts +33 -0
- package/dist/recover.d.ts.map +1 -0
- package/dist/recover.js +201 -0
- package/dist/recover.js.map +1 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +2 -0
- package/dist/runtime.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -216,6 +216,19 @@ locally and retried if the workspace is unreachable. `npx @scopebond/hook flush`
|
|
|
216
216
|
delivers anything still queued — run it on a session-end hook (and set
|
|
217
217
|
`SCOPEBOND_HOOK_FLUSH_MS=0`) if you want zero per-call latency.
|
|
218
218
|
|
|
219
|
+
**Reconnecting.** Run `login` again from any folder: unless that folder has its own project
|
|
220
|
+
setup, it repairs the connection the hook actually uses (usually the user-level one) instead
|
|
221
|
+
of creating a second one; pass `--project` to set up the folder you are in. If the workspace
|
|
222
|
+
no longer accepts this computer's countersigning key (the computer was replaced or
|
|
223
|
+
disconnected there), `login` replaces the key and keeps the old one in
|
|
224
|
+
`.scopebond/retired-keys/`.
|
|
225
|
+
|
|
226
|
+
**Records signed by an earlier key.** The new connection cannot deliver records the earlier
|
|
227
|
+
key signed, so they leave the delivery queue but stay in the local log.
|
|
228
|
+
`npx @scopebond/hook recover` asks the workspace to accept them, waits while an owner or
|
|
229
|
+
admin approves it there, then sends them; the workspace checks each signature against the
|
|
230
|
+
key it kept and labels the records as recovered.
|
|
231
|
+
|
|
219
232
|
### Rules set by your workspace
|
|
220
233
|
|
|
221
234
|
A connected computer keeps its own rules (`.scopebond/rules.json`) until someone who manages
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAiqBA,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,GAAG,IAAI,CAS7F"}
|
package/dist/cli.js
CHANGED
|
@@ -40,7 +40,8 @@ import { openObservations, describeObservations, observationStatus, stopReasonFr
|
|
|
40
40
|
import { OBSERVATION_DB, ObservationStore } from "./obs-store.js";
|
|
41
41
|
import { loadOrCreateBindingKey } from "./observation.js";
|
|
42
42
|
import { uploadPending } from "./obs-upload.js";
|
|
43
|
-
import { connectCloud, loadConnection } from "./cloud.js";
|
|
43
|
+
import { connectCloud, ingestUrl, loadConnection } from "./cloud.js";
|
|
44
|
+
import { recoverEarlierReceipts } from "./recover.js";
|
|
44
45
|
import { loadPolicyExport, policyBuilds } from "./policy-load.js";
|
|
45
46
|
import { isManaged, readMeta, MANAGED_DOC_FILE } from "./managed.js";
|
|
46
47
|
import { syncIfDue, syncPolicy } from "./policy-sync.js";
|
|
@@ -871,11 +872,11 @@ async function runConnect(args) {
|
|
|
871
872
|
const bundleArg = positional[1];
|
|
872
873
|
const harness = selectedHarness(args);
|
|
873
874
|
if (!url) {
|
|
874
|
-
console.error(`usage: ${cliCommand("connect <workspace-url> <enrollment> [--claude|--cursor|--codex] [--no-install]")}`);
|
|
875
|
+
console.error(`usage: ${cliCommand("connect <workspace-url> <enrollment> [--claude|--cursor|--codex] [--no-install] [--project]")}`);
|
|
875
876
|
console.error(enrollmentHelp);
|
|
876
877
|
process.exit(1);
|
|
877
878
|
}
|
|
878
|
-
const dir =
|
|
879
|
+
const dir = connectDir(args);
|
|
879
880
|
// One command sets everything up: scaffold the key, attester and starter policy if
|
|
880
881
|
// they do not exist, then enroll and persist the scoped machine credential. The
|
|
881
882
|
// enrollment can be a file, an inline base64 blob (what the portal hands out) or
|
|
@@ -897,6 +898,10 @@ async function finishConnect(dir, url, bundle, harness, args) {
|
|
|
897
898
|
try {
|
|
898
899
|
const c = await connectCloud(dir, url, bundle);
|
|
899
900
|
console.log(`✓ Connected to ${c.url}`);
|
|
901
|
+
if (c.rotatedFrom)
|
|
902
|
+
console.log(`✓ This computer's earlier key (${c.rotatedFrom}) was no longer accepted, so it was replaced; the old key is kept in ${join(dir, "retired-keys")}`);
|
|
903
|
+
if (c.setAside)
|
|
904
|
+
console.log(` ${c.setAside.toLocaleString()} queued record(s) signed by an earlier key cannot go through this connection. To deliver them, run: ${cliCommand("recover")}`);
|
|
900
905
|
// With a user-level install present, the hook ignores a project policy until it is
|
|
901
906
|
// trusted, and would fall back to the user home, which holds no cloud.json: the agent
|
|
902
907
|
// stays governed, but nothing reaches the workspace. Connecting this project is the
|
|
@@ -959,6 +964,43 @@ const enrollmentHelp = [
|
|
|
959
964
|
"Each enrollment is single-use and expires soon after it is created; if this one was",
|
|
960
965
|
"used or has expired, create a new one there.",
|
|
961
966
|
].join("\n");
|
|
967
|
+
/** Where `connect` and `login` write. A project someone set up here with `init` (it has a
|
|
968
|
+
* policy) is connected, as before. Otherwise the configuration the hook itself uses from
|
|
969
|
+
* here — usually the user-level install — so reconnecting from any folder repairs the
|
|
970
|
+
* connection that is actually failing instead of creating a second, project-level one
|
|
971
|
+
* beside it. `--project` asks for a new per-project setup explicitly. */
|
|
972
|
+
function connectDir(args) {
|
|
973
|
+
const project = configDir();
|
|
974
|
+
return args.includes("--project") || existsSync(join(project, "policy.json")) ? project : resolveConfigDir(process.cwd());
|
|
975
|
+
}
|
|
976
|
+
/** `recover [--no-wait]`: deliver receipts that an earlier, since-revoked key of this computer
|
|
977
|
+
* signed and the workspace never received, after an owner or admin approves it there. */
|
|
978
|
+
async function runRecover(args) {
|
|
979
|
+
const dir = resolveConfigDir(process.cwd());
|
|
980
|
+
const connection = loadConnection(dir);
|
|
981
|
+
if (!connection) {
|
|
982
|
+
console.error(`not connected to a workspace; run \`${cliCommand("login <workspace-url>")}\` first`);
|
|
983
|
+
process.exit(1);
|
|
984
|
+
}
|
|
985
|
+
console.log(`Recovering from ${dir}`);
|
|
986
|
+
const result = await recoverEarlierReceipts(dir, connection, {
|
|
987
|
+
log: (line) => console.log(line),
|
|
988
|
+
fetch,
|
|
989
|
+
now: Date.now,
|
|
990
|
+
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
991
|
+
}, { wait: !args.includes("--no-wait"), waitMs: 15 * 60_000, pollMs: 5_000 });
|
|
992
|
+
if (result.groups) {
|
|
993
|
+
const parts = [
|
|
994
|
+
`recovered ${result.accepted.toLocaleString()}`,
|
|
995
|
+
`already present ${result.duplicates.toLocaleString()}`,
|
|
996
|
+
`refused ${result.rejected.toLocaleString()}`,
|
|
997
|
+
...(result.pending ? [`waiting for approval ${result.pending.toLocaleString()}`] : []),
|
|
998
|
+
...(result.skipped ? [`skipped ${result.skipped.toLocaleString()}`] : []),
|
|
999
|
+
];
|
|
1000
|
+
console.log(`\nDone: ${parts.join(", ")}.`);
|
|
1001
|
+
}
|
|
1002
|
+
process.exitCode = result.failed || result.pending ? 1 : 0;
|
|
1003
|
+
}
|
|
962
1004
|
async function runFlush() {
|
|
963
1005
|
const dir = resolveConfigDir(process.cwd());
|
|
964
1006
|
if (!loadConnection(dir)) {
|
|
@@ -1180,7 +1222,7 @@ async function runObservations(args) {
|
|
|
1180
1222
|
const emitter = opened.emitter;
|
|
1181
1223
|
try {
|
|
1182
1224
|
if (sub === "flush") {
|
|
1183
|
-
const outcome = await uploadPending(emitter.store, { url: emitter.connection
|
|
1225
|
+
const outcome = await uploadPending(emitter.store, { url: ingestUrl(emitter.connection), credential: emitter.connection.credential, timeoutMs: 10_000 });
|
|
1184
1226
|
const left = emitter.store.pendingSummary().count;
|
|
1185
1227
|
console.log(`${outcome.result}: ${outcome.acknowledged} acknowledged, ${outcome.deferred} deferred, ${outcome.rejected} refused; ${left} still pending${outcome.detail ? ` (${outcome.detail})` : ""}`);
|
|
1186
1228
|
process.exitCode = left > 0 ? 1 : 0;
|
|
@@ -1577,7 +1619,7 @@ async function runLogin(args) {
|
|
|
1577
1619
|
origin = parsed.origin;
|
|
1578
1620
|
}
|
|
1579
1621
|
catch {
|
|
1580
|
-
console.error(`usage: ${cliCommand("login <workspace-url> [--claude|--cursor|--codex] [--no-install]")}`);
|
|
1622
|
+
console.error(`usage: ${cliCommand("login <workspace-url> [--claude|--cursor|--codex] [--no-install] [--project]")}`);
|
|
1581
1623
|
console.error("The workspace URL is the address of your Scopebond workspace, for example https://cloud.scopebond.com.");
|
|
1582
1624
|
process.exit(1);
|
|
1583
1625
|
}
|
|
@@ -1607,7 +1649,7 @@ async function runLogin(args) {
|
|
|
1607
1649
|
const deadline = Date.now() + Math.max(60, Number(start.json.expires_in ?? 600)) * 1000;
|
|
1608
1650
|
console.log(`To connect this computer, open:\n\n ${verify}\n\nand check that it shows the code ${userCode}\n`);
|
|
1609
1651
|
console.log("Waiting for approval (the code expires in 10 minutes; Ctrl+C to stop)…");
|
|
1610
|
-
const dir =
|
|
1652
|
+
const dir = connectDir(args);
|
|
1611
1653
|
scaffold(dir, {});
|
|
1612
1654
|
while (Date.now() < deadline) {
|
|
1613
1655
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
@@ -1750,6 +1792,12 @@ const COMMANDS = [
|
|
|
1750
1792
|
{ name: "connect", args: "<workspace-url> <enrollment> [--claude|--cursor|--codex]",
|
|
1751
1793
|
summary: "send receipts to a Scopebond Cloud workspace as well as keeping them locally" },
|
|
1752
1794
|
{ name: "flush", summary: "deliver any receipts still queued for the workspace now" },
|
|
1795
|
+
{ name: "recover", args: "[--no-wait]", summary: "deliver records an earlier, revoked key signed, once the workspace approves",
|
|
1796
|
+
detail: [
|
|
1797
|
+
"When this computer was replaced or disconnected while records were still queued, the",
|
|
1798
|
+
"workspace refuses them from the new connection. This asks the workspace to accept them,",
|
|
1799
|
+
"waits while an owner or admin approves it on Activity, then sends them, labelled Recovered.",
|
|
1800
|
+
] },
|
|
1753
1801
|
{ name: "trust", args: "[--yes]", summary: "let this project's .scopebond policy govern here (pinned by hash)" },
|
|
1754
1802
|
{ name: "uninstall", args: "[--purge] [--yes]", summary: "remove the hook from your agent config; --purge also deletes the home" },
|
|
1755
1803
|
{ name: "claude", summary: "(internal) decide one Claude Code PreToolUse call, JSON on stdin" },
|
|
@@ -1840,6 +1888,9 @@ else if (cmd === "test") {
|
|
|
1840
1888
|
else if (cmd === "flush") {
|
|
1841
1889
|
await runFlush();
|
|
1842
1890
|
}
|
|
1891
|
+
else if (cmd === "recover") {
|
|
1892
|
+
await runRecover(rest);
|
|
1893
|
+
}
|
|
1843
1894
|
else if (cmd === "status") {
|
|
1844
1895
|
runStatus();
|
|
1845
1896
|
}
|