@skrr-ai/cli 0.1.49 → 0.1.51

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 (180) hide show
  1. package/README.md +5 -4
  2. package/dist/base-command.js +75 -6
  3. package/dist/commands/agents/chat.d.ts +14 -0
  4. package/dist/commands/agents/chat.js +37 -1
  5. package/dist/commands/agents/create.js +14 -7
  6. package/dist/commands/agents/show.d.ts +9 -0
  7. package/dist/commands/agents/show.js +53 -0
  8. package/dist/commands/agents/update.d.ts +9 -0
  9. package/dist/commands/agents/update.js +38 -5
  10. package/dist/commands/balance/show.d.ts +34 -0
  11. package/dist/commands/balance/show.js +92 -2
  12. package/dist/commands/code/index.d.ts +1 -0
  13. package/dist/commands/code/index.js +49 -5
  14. package/dist/commands/code/run.js +5 -1
  15. package/dist/commands/commitments/cycles.d.ts +19 -0
  16. package/dist/commands/commitments/cycles.js +46 -0
  17. package/dist/commands/commitments/doctor.d.ts +1 -0
  18. package/dist/commands/commitments/doctor.js +19 -4
  19. package/dist/commands/commitments/effective-policy.js +40 -1
  20. package/dist/commands/commitments/explain.d.ts +17 -0
  21. package/dist/commands/commitments/explain.js +40 -0
  22. package/dist/commands/commitments/next-tick.js +3 -1
  23. package/dist/commands/commitments/preflight.js +10 -0
  24. package/dist/commands/commitments/show.js +11 -1
  25. package/dist/commands/daemon/config.d.ts +15 -0
  26. package/dist/commands/daemon/config.js +38 -0
  27. package/dist/commands/daemon/index.js +1 -0
  28. package/dist/commands/daemon/status.js +15 -1
  29. package/dist/commands/goals/create.d.ts +1 -0
  30. package/dist/commands/goals/create.js +22 -0
  31. package/dist/commands/harnesses/models.d.ts +54 -0
  32. package/dist/commands/harnesses/models.js +100 -7
  33. package/dist/commands/harnesses/show.d.ts +11 -0
  34. package/dist/commands/harnesses/show.js +35 -0
  35. package/dist/commands/harnesses/update.d.ts +28 -1
  36. package/dist/commands/harnesses/update.js +73 -10
  37. package/dist/commands/logout.js +12 -1
  38. package/dist/commands/machines/dedicated/index.js +1 -1
  39. package/dist/commands/machines/dedicated/sign-in.js +4 -1
  40. package/dist/commands/machines/dedicated/terminal/kill.d.ts +21 -0
  41. package/dist/commands/machines/dedicated/terminal/kill.js +55 -0
  42. package/dist/commands/machines/dedicated/terminal/ls.d.ts +22 -0
  43. package/dist/commands/machines/dedicated/terminal/ls.js +70 -0
  44. package/dist/commands/machines/dedicated/terminal/rename.d.ts +23 -0
  45. package/dist/commands/machines/dedicated/terminal/rename.js +64 -0
  46. package/dist/commands/machines/dedicated/terminal.d.ts +9 -0
  47. package/dist/commands/machines/dedicated/terminal.js +27 -2
  48. package/dist/commands/machines/hosted/connect.js +2 -2
  49. package/dist/commands/machines/hosted/destroy.js +1 -1
  50. package/dist/commands/machines/hosted/exec.js +1 -1
  51. package/dist/commands/machines/hosted/list.js +1 -1
  52. package/dist/commands/machines/hosted/pause.js +1 -1
  53. package/dist/commands/machines/hosted/pull.js +1 -1
  54. package/dist/commands/machines/hosted/resume.js +1 -1
  55. package/dist/commands/machines/hosted/start.js +2 -2
  56. package/dist/commands/machines/hosted/status.js +2 -1
  57. package/dist/commands/outbox/list.d.ts +12 -1
  58. package/dist/commands/outbox/list.js +31 -2
  59. package/dist/commands/revoke-daemon.js +9 -0
  60. package/dist/commands/spaces/create.d.ts +1 -0
  61. package/dist/commands/spaces/create.js +17 -0
  62. package/dist/commands/spaces/show.js +5 -0
  63. package/dist/commands/spaces/update.d.ts +1 -0
  64. package/dist/commands/spaces/update.js +16 -0
  65. package/dist/commands/tasks/create.d.ts +1 -0
  66. package/dist/commands/tasks/create.js +6 -0
  67. package/dist/commands/tasks/list.js +1 -0
  68. package/dist/commands/tasks/show.js +45 -0
  69. package/dist/commands/tasks/update.d.ts +1 -0
  70. package/dist/commands/tasks/update.js +22 -0
  71. package/dist/commands/tasks/upsert.d.ts +1 -0
  72. package/dist/commands/tasks/upsert.js +16 -0
  73. package/dist/lib/agent-config.d.ts +2 -0
  74. package/dist/lib/agent-config.js +3 -1
  75. package/dist/lib/agentic-stream.d.ts +68 -1
  76. package/dist/lib/agentic-stream.js +294 -7
  77. package/dist/lib/api-fetch.js +19 -0
  78. package/dist/lib/auth-core-init.d.ts +2 -10
  79. package/dist/lib/auth-core-init.js +20 -67
  80. package/dist/lib/auth-failure.d.ts +33 -0
  81. package/dist/lib/auth-failure.js +65 -0
  82. package/dist/lib/auth-storage.d.ts +14 -0
  83. package/dist/lib/auth-storage.js +14 -0
  84. package/dist/lib/commitments.d.ts +37 -2
  85. package/dist/lib/commitments.js +148 -0
  86. package/dist/lib/daemonBroker.d.ts +134 -31
  87. package/dist/lib/daemonBroker.js +376 -31
  88. package/dist/lib/dedicated-lease-command.d.ts +14 -0
  89. package/dist/lib/dedicated-lease-command.js +30 -1
  90. package/dist/lib/dedicated-machines.js +8 -25
  91. package/dist/lib/dedicated-service-command.d.ts +11 -3
  92. package/dist/lib/dedicated-service-command.js +20 -3
  93. package/dist/lib/dedicated-service.d.ts +46 -0
  94. package/dist/lib/dedicated-service.js +85 -7
  95. package/dist/lib/dedicated-terminal.d.ts +21 -0
  96. package/dist/lib/dedicated-terminal.js +104 -9
  97. package/dist/lib/delegated-cli.js +4 -1
  98. package/dist/lib/exec-runtime-binary.d.ts +3 -1
  99. package/dist/lib/exec-runtime-binary.js +4 -2
  100. package/dist/lib/first-party-harness-broker.d.ts +10 -0
  101. package/dist/lib/first-party-harness-broker.js +13 -1
  102. package/dist/lib/first-party-harness-doctor.js +41 -1
  103. package/dist/lib/first-party-harness-managed.d.ts +4 -3
  104. package/dist/lib/first-party-harness-project-trust.d.ts +125 -0
  105. package/dist/lib/first-party-harness-project-trust.js +364 -0
  106. package/dist/lib/first-party-harness.d.ts +9 -2
  107. package/dist/lib/first-party-harness.js +6 -5
  108. package/dist/lib/harnesses.d.ts +10 -0
  109. package/dist/lib/hosted-machines.d.ts +10 -1
  110. package/dist/lib/hosted-machines.js +36 -2
  111. package/dist/lib/live-runtime-binary.d.ts +40 -0
  112. package/dist/lib/live-runtime-binary.js +193 -0
  113. package/dist/lib/login.d.ts +23 -0
  114. package/dist/lib/login.js +60 -10
  115. package/dist/lib/machine-spend-cap.d.ts +23 -0
  116. package/dist/lib/machine-spend-cap.js +68 -0
  117. package/dist/lib/message-intent.js +3 -1
  118. package/dist/lib/node-adapter.js +15 -3
  119. package/dist/lib/tasks.d.ts +20 -0
  120. package/dist/lib/tasks.js +54 -0
  121. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +90 -0
  122. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +113 -0
  123. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
  124. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
  125. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.d.ts +47 -0
  126. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +69 -0
  127. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +133 -0
  128. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +189 -0
  129. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
  130. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  131. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
  132. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
  133. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
  134. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
  135. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
  136. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
  137. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
  138. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +8 -3
  139. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +9 -4
  140. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
  141. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +16 -10
  142. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.d.ts +6 -0
  143. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.js +32 -9
  144. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.d.ts +81 -0
  145. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.js +87 -0
  146. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
  147. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
  148. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +6 -1
  149. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +90 -0
  150. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +105 -0
  151. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
  152. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
  153. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.d.ts +47 -0
  154. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +66 -0
  155. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +133 -0
  156. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +182 -0
  157. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
  158. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  159. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
  160. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
  161. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
  162. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
  163. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
  164. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
  165. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
  166. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +8 -3
  167. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +9 -4
  168. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
  169. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +16 -10
  170. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.d.ts +6 -0
  171. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.js +31 -9
  172. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.d.ts +81 -0
  173. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.js +80 -0
  174. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
  175. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
  176. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +6 -1
  177. package/dist/node_modules/@skrr-ai/auth-core/package.json +61 -1
  178. package/dist/node_modules/@skrr-ai/data-provider/index.js +3418 -3344
  179. package/oclif.manifest.json +24813 -24242
  180. package/package.json +4 -1
@@ -0,0 +1,66 @@
1
+ /**
2
+ * credentialSession.ts — the shared credential-lifecycle kernel (policy half).
3
+ *
4
+ * A credential lifecycle is policy × mechanism. The policy — serve a cached
5
+ * token until its expiry-skew window, collapse concurrent acquires onto one
6
+ * in-flight operation, invalidate on a resource-side 401 — is identical for
7
+ * every surface and was being re-implemented per consumer (task token-broker,
8
+ * delegated-cli redemption, the human CLI refresh path). The mechanism —
9
+ * WHERE the durable secret lives and HOW a token is acquired — legitimately
10
+ * differs (in-memory daemon family vs file+envelope+lock vs loopback redeem)
11
+ * and stays behind the `acquire` seam.
12
+ *
13
+ * Deliberately thin: no persistence, no retry policy, no logging. Callers
14
+ * own their store and their error surfaces; this owns the cache/single-
15
+ * flight/invalidate invariants so they cannot drift between surfaces.
16
+ */
17
+ const DEFAULT_SKEW_MS = 30_000;
18
+ export function createCredentialSession(opts) {
19
+ const skewMs = opts.skewMs ?? DEFAULT_SKEW_MS;
20
+ const now = opts.now ?? Date.now;
21
+ let cached = null;
22
+ let inFlight = null;
23
+ let inFlightForced = false;
24
+ // Monotonic generation so a superseded in-flight acquire (a forceRefresh
25
+ // launched while a plain acquire was still running) cannot write its stale
26
+ // result back over the newer one.
27
+ let generation = 0;
28
+ const fresh = (t) => t.expiresAtMs == null || now() < t.expiresAtMs - skewMs;
29
+ const acquireShared = (force) => {
30
+ // A forced acquire must not join a non-forced in-flight one — that would
31
+ // serve the very credential a 401 just invalidated. A forced in-flight
32
+ // DOES satisfy non-forced waiters: it is already renewing.
33
+ if (inFlight && (inFlightForced || !force))
34
+ return inFlight;
35
+ const gen = ++generation;
36
+ const p = opts.acquire(force).then((t) => {
37
+ if (gen === generation)
38
+ cached = t;
39
+ return t;
40
+ });
41
+ inFlight = p;
42
+ inFlightForced = force;
43
+ // `then(cleanup, cleanup)` not `.finally(cleanup)`: finally propagates the
44
+ // rejection into a derived promise nobody awaits — an unhandledRejection.
45
+ const cleanup = () => {
46
+ if (inFlight === p) {
47
+ inFlight = null;
48
+ inFlightForced = false;
49
+ }
50
+ };
51
+ p.then(cleanup, cleanup);
52
+ return p;
53
+ };
54
+ return {
55
+ async getToken(forceRefresh = false) {
56
+ if (forceRefresh)
57
+ cached = null;
58
+ if (cached && fresh(cached))
59
+ return cached.accessToken;
60
+ return (await acquireShared(forceRefresh)).accessToken;
61
+ },
62
+ invalidate() {
63
+ cached = null;
64
+ },
65
+ };
66
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Server-attested tool approval (OSK-11975 follow-up to the `manual` mode
3
+ * refusal on `daemon:tool:request`).
4
+ *
5
+ * A daemon category in `manual` mode means "a PERSON approves each call". On
6
+ * the harness-session path the daemon asks through the permission bridge; on
7
+ * the daemon-tool path a cloud agent's call used to be refused outright,
8
+ * because the request carried no approval and there was no channel back to a
9
+ * person. The attested-approval protocol closes that gap without weakening
10
+ * the guarantee:
11
+ *
12
+ * 1. The daemon announces `daemon_tool_attested_approval_v1` and its
13
+ * resolved per-category posture (`toolPermissionModes`) at register time.
14
+ * 2. Before proxying a call, the server reads the announced posture for the
15
+ * tool's category. `manual` means it must ask first — through the same
16
+ * permission surface (`canUseTool` → `permission:request`) an in-process
17
+ * SDK turn would have used — and only then dispatch, carrying an
18
+ * `attestedApproval` object on the request.
19
+ * 3. The daemon verifies the attestation's SHAPE and BINDING — it is bound
20
+ * to this exact (toolUseId, toolName, toolInput digest) and expires
21
+ * quickly — and allows the call. A missing or malformed attestation gets
22
+ * the same named refusal as before.
23
+ *
24
+ * What the daemon verifies is the binding, not the server's honesty: the
25
+ * request already arrives over the daemon's authenticated server channel, so
26
+ * the field IS the attestation — it is unsigned, and `decidedBy` is a literal.
27
+ * What the binding buys is that the model — which authors `toolInput` —
28
+ * cannot mint an approval, and an approval for one call cannot be replayed
29
+ * onto another. The verifier also bounds the validity window itself (never
30
+ * trusting the issuer's `expiresAt` beyond `ATTESTED_TOOL_APPROVAL_TTL_MS`),
31
+ * and the daemon spends each approval once: a second delivery of the same
32
+ * attestation inside its window is refused as `replayed`. That ledger lives
33
+ * in daemon memory, so a daemon restart inside the (≤60s) window forgets it.
34
+ */
35
+ /** Announced by a daemon that honours `attestedApproval` on daemon:tool:request. */
36
+ export declare const DAEMON_TOOL_ATTESTED_APPROVAL_CAPABILITY = "daemon_tool_attested_approval_v1";
37
+ /**
38
+ * Announced by a daemon that honours `toolPosture: 'read_only'` on
39
+ * daemon:tool:request — the daemon-tool-path expression of the wire `plan`
40
+ * session mode: reads run, every mutating category is refused with a named
41
+ * reason. Carries INTENT like `browserPolicy: 'read_only'` — the daemon owns
42
+ * what read-only means, so the server cannot widen the fence by shipping a
43
+ * wider value.
44
+ */
45
+ export declare const DAEMON_TOOL_READ_ONLY_CAPABILITY = "daemon_tool_read_only_v1";
46
+ /**
47
+ * Tool categories the daemon recognizes for permission purposes. This is the
48
+ * wire vocabulary the daemon's announced `toolPermissionModes` is keyed on —
49
+ * shared so the server's pre-dispatch check and the daemon's own enforcement
50
+ * classify a tool name identically (a third map was exactly the drift this
51
+ * package exists to prevent).
52
+ */
53
+ export type DaemonToolCategory = 'read' | 'write' | 'bash' | 'browser' | 'claude_session' | 'native_ui';
54
+ export declare const DAEMON_TOOL_CATEGORIES: readonly DaemonToolCategory[];
55
+ /** Map a tool name to its category. Unknown tools take the restrictive answer. */
56
+ export declare function categoryForDaemonTool(toolName: string): DaemonToolCategory;
57
+ /**
58
+ * The posture a daemon announces per category for the daemon-tool path:
59
+ * - `auto` — the call runs once guardrails pass; no attestation needed
60
+ * - `manual` — the call needs a person; the server must attach a fresh
61
+ * `attestedApproval` or the daemon refuses it
62
+ * - `delegated` — the category's own policy decides per action (browser,
63
+ * native UI); asking up front would ask the wrong question
64
+ */
65
+ export type DaemonToolPathPosture = 'auto' | 'manual' | 'delegated';
66
+ export type DaemonToolPermissionModes = Readonly<Partial<Record<DaemonToolCategory, DaemonToolPathPosture>>>;
67
+ /** The wire field values `toolPosture` accepts. Unknown values must be ignored. */
68
+ export type DaemonToolPosture = 'read_only';
69
+ /**
70
+ * How long an attestation stays valid. Short: it answers ONE person, about
71
+ * THIS call, moments ago — it is not a standing grant. The server asks and
72
+ * dispatches back-to-back, so the window only has to outlive publish latency.
73
+ */
74
+ export declare const ATTESTED_TOOL_APPROVAL_TTL_MS: number;
75
+ export interface AttestedToolApproval {
76
+ v: 1;
77
+ /** Binds the approval to this exact call — a different call gets nothing. */
78
+ toolUseId: string;
79
+ toolName: string;
80
+ /** SHA-256 of the canonical tool input (see `canonicalToolInputDigest`). */
81
+ inputSha256: string;
82
+ /** Who answered on the server's permission surface. */
83
+ decidedBy: 'user';
84
+ issuedAt: number;
85
+ expiresAt: number;
86
+ }
87
+ /** The digest both sides compute over `toolInput`. */
88
+ export declare function canonicalToolInputDigest(toolInput: unknown): string;
89
+ /** Server side: mint the attestation after a person approved THIS call. */
90
+ export declare function mintAttestedToolApproval(args: {
91
+ toolUseId: string;
92
+ toolName: string;
93
+ toolInput: unknown;
94
+ now?: number;
95
+ ttlMs?: number;
96
+ }): AttestedToolApproval;
97
+ /**
98
+ * Why an attestation did not approve a call — logged by the daemon, never
99
+ * shown to the caller beyond "absent" vs "did not verify".
100
+ * - `absent` — no attestation arrived (the server did not ask anyone)
101
+ * - `malformed` — not the v1 shape
102
+ * - `expired` — past `expiresAt`, or issued implausibly far in the future
103
+ * - `window` — the issuer claimed a validity window longer than
104
+ * `ATTESTED_TOOL_APPROVAL_TTL_MS` (or an empty/negative one)
105
+ * - `binding` — minted for a different call (toolUseId, tool, or input)
106
+ * - `replayed` — this exact approval was already spent on this daemon
107
+ * (the ledger is the verifier's caller's, see the daemon's
108
+ * `permissions/attestation-ledger.ts`)
109
+ */
110
+ export type AttestedToolApprovalRefusalReason = 'absent' | 'malformed' | 'binding' | 'expired' | 'window' | 'replayed';
111
+ export type AttestedToolApprovalVerdict = {
112
+ ok: true;
113
+ } | {
114
+ ok: false;
115
+ reason: AttestedToolApprovalRefusalReason;
116
+ };
117
+ /**
118
+ * Clock skew tolerated on `issuedAt`: a server clock slightly ahead of the
119
+ * daemon's must not refuse a fresh approval.
120
+ */
121
+ export declare const ATTESTED_TOOL_APPROVAL_CLOCK_SKEW_MS: number;
122
+ /**
123
+ * Daemon side: verify an attestation against the call it claims to approve.
124
+ * Untrusted wire input — every field is checked, and any mismatch is a plain
125
+ * "no approval", never a partial credit. `reason` is for daemon logs; the
126
+ * refusal text stays generic so a probing caller learns nothing new.
127
+ */
128
+ export declare function verifyAttestedToolApproval(approval: unknown, call: {
129
+ toolUseId: string;
130
+ toolName: string;
131
+ toolInput: unknown;
132
+ now?: number;
133
+ }): AttestedToolApprovalVerdict;
@@ -0,0 +1,182 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * Server-attested tool approval (OSK-11975 follow-up to the `manual` mode
4
+ * refusal on `daemon:tool:request`).
5
+ *
6
+ * A daemon category in `manual` mode means "a PERSON approves each call". On
7
+ * the harness-session path the daemon asks through the permission bridge; on
8
+ * the daemon-tool path a cloud agent's call used to be refused outright,
9
+ * because the request carried no approval and there was no channel back to a
10
+ * person. The attested-approval protocol closes that gap without weakening
11
+ * the guarantee:
12
+ *
13
+ * 1. The daemon announces `daemon_tool_attested_approval_v1` and its
14
+ * resolved per-category posture (`toolPermissionModes`) at register time.
15
+ * 2. Before proxying a call, the server reads the announced posture for the
16
+ * tool's category. `manual` means it must ask first — through the same
17
+ * permission surface (`canUseTool` → `permission:request`) an in-process
18
+ * SDK turn would have used — and only then dispatch, carrying an
19
+ * `attestedApproval` object on the request.
20
+ * 3. The daemon verifies the attestation's SHAPE and BINDING — it is bound
21
+ * to this exact (toolUseId, toolName, toolInput digest) and expires
22
+ * quickly — and allows the call. A missing or malformed attestation gets
23
+ * the same named refusal as before.
24
+ *
25
+ * What the daemon verifies is the binding, not the server's honesty: the
26
+ * request already arrives over the daemon's authenticated server channel, so
27
+ * the field IS the attestation — it is unsigned, and `decidedBy` is a literal.
28
+ * What the binding buys is that the model — which authors `toolInput` —
29
+ * cannot mint an approval, and an approval for one call cannot be replayed
30
+ * onto another. The verifier also bounds the validity window itself (never
31
+ * trusting the issuer's `expiresAt` beyond `ATTESTED_TOOL_APPROVAL_TTL_MS`),
32
+ * and the daemon spends each approval once: a second delivery of the same
33
+ * attestation inside its window is refused as `replayed`. That ledger lives
34
+ * in daemon memory, so a daemon restart inside the (≤60s) window forgets it.
35
+ */
36
+ /** Announced by a daemon that honours `attestedApproval` on daemon:tool:request. */
37
+ export const DAEMON_TOOL_ATTESTED_APPROVAL_CAPABILITY = 'daemon_tool_attested_approval_v1';
38
+ /**
39
+ * Announced by a daemon that honours `toolPosture: 'read_only'` on
40
+ * daemon:tool:request — the daemon-tool-path expression of the wire `plan`
41
+ * session mode: reads run, every mutating category is refused with a named
42
+ * reason. Carries INTENT like `browserPolicy: 'read_only'` — the daemon owns
43
+ * what read-only means, so the server cannot widen the fence by shipping a
44
+ * wider value.
45
+ */
46
+ export const DAEMON_TOOL_READ_ONLY_CAPABILITY = 'daemon_tool_read_only_v1';
47
+ export const DAEMON_TOOL_CATEGORIES = [
48
+ 'read',
49
+ 'write',
50
+ 'bash',
51
+ 'browser',
52
+ 'claude_session',
53
+ 'native_ui',
54
+ ];
55
+ /** Map a tool name to its category. Unknown tools take the restrictive answer. */
56
+ export function categoryForDaemonTool(toolName) {
57
+ switch (String(toolName || '').toLowerCase()) {
58
+ case 'read':
59
+ case 'glob':
60
+ case 'grep':
61
+ case 'filetransferread':
62
+ return 'read';
63
+ case 'write':
64
+ case 'edit':
65
+ case 'multiedit':
66
+ case 'apply_patch':
67
+ case 'filetransferwrite':
68
+ return 'write';
69
+ // Reading or stopping a background command is part of running it.
70
+ case 'bash':
71
+ case 'taskoutput':
72
+ case 'taskstop':
73
+ return 'bash';
74
+ case 'browser':
75
+ return 'browser';
76
+ case 'native':
77
+ return 'native_ui';
78
+ case 'claude_session':
79
+ case 'claude-session':
80
+ return 'claude_session';
81
+ default:
82
+ return 'write';
83
+ }
84
+ }
85
+ // ---------------------------------------------------------------------------
86
+ // Attested approval
87
+ // ---------------------------------------------------------------------------
88
+ /**
89
+ * How long an attestation stays valid. Short: it answers ONE person, about
90
+ * THIS call, moments ago — it is not a standing grant. The server asks and
91
+ * dispatches back-to-back, so the window only has to outlive publish latency.
92
+ */
93
+ export const ATTESTED_TOOL_APPROVAL_TTL_MS = 60 * 1000;
94
+ /**
95
+ * Deterministic serialization for the input digest: objects sort keys
96
+ * recursively, arrays keep order, non-JSON values normalize the way
97
+ * `JSON.stringify` would emit them (undefined/functions in objects drop, in
98
+ * arrays become null). Both runtimes must digest the SAME bytes — the server
99
+ * mints and the daemon verifies — so this lives here, once.
100
+ */
101
+ function canonicalJson(value) {
102
+ if (value === null || typeof value !== 'object') {
103
+ if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
104
+ return 'null';
105
+ }
106
+ return JSON.stringify(value);
107
+ }
108
+ if (Array.isArray(value)) {
109
+ return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`;
110
+ }
111
+ const record = value;
112
+ const keys = Object.keys(record).filter((key) => record[key] !== undefined &&
113
+ typeof record[key] !== 'function' &&
114
+ typeof record[key] !== 'symbol');
115
+ keys.sort();
116
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
117
+ }
118
+ /** The digest both sides compute over `toolInput`. */
119
+ export function canonicalToolInputDigest(toolInput) {
120
+ return createHash('sha256')
121
+ .update(canonicalJson(toolInput ?? {}))
122
+ .digest('hex');
123
+ }
124
+ /** Server side: mint the attestation after a person approved THIS call. */
125
+ export function mintAttestedToolApproval(args) {
126
+ const now = args.now ?? Date.now();
127
+ return {
128
+ v: 1,
129
+ toolUseId: args.toolUseId,
130
+ toolName: args.toolName,
131
+ inputSha256: canonicalToolInputDigest(args.toolInput),
132
+ decidedBy: 'user',
133
+ issuedAt: now,
134
+ expiresAt: now + (args.ttlMs ?? ATTESTED_TOOL_APPROVAL_TTL_MS),
135
+ };
136
+ }
137
+ /**
138
+ * Clock skew tolerated on `issuedAt`: a server clock slightly ahead of the
139
+ * daemon's must not refuse a fresh approval.
140
+ */
141
+ export const ATTESTED_TOOL_APPROVAL_CLOCK_SKEW_MS = 30 * 1000;
142
+ /**
143
+ * Daemon side: verify an attestation against the call it claims to approve.
144
+ * Untrusted wire input — every field is checked, and any mismatch is a plain
145
+ * "no approval", never a partial credit. `reason` is for daemon logs; the
146
+ * refusal text stays generic so a probing caller learns nothing new.
147
+ */
148
+ export function verifyAttestedToolApproval(approval, call) {
149
+ if (approval === undefined || approval === null)
150
+ return { ok: false, reason: 'absent' };
151
+ if (typeof approval !== 'object' || Array.isArray(approval)) {
152
+ return { ok: false, reason: 'malformed' };
153
+ }
154
+ const a = approval;
155
+ if (a.v !== 1 ||
156
+ typeof a.toolUseId !== 'string' ||
157
+ typeof a.toolName !== 'string' ||
158
+ typeof a.inputSha256 !== 'string' ||
159
+ a.decidedBy !== 'user' ||
160
+ typeof a.issuedAt !== 'number' ||
161
+ typeof a.expiresAt !== 'number') {
162
+ return { ok: false, reason: 'malformed' };
163
+ }
164
+ const now = call.now ?? Date.now();
165
+ if (a.expiresAt <= now || a.issuedAt > now + ATTESTED_TOOL_APPROVAL_CLOCK_SKEW_MS) {
166
+ return { ok: false, reason: 'expired' };
167
+ }
168
+ // The TTL is the VERIFIER's bound, not the issuer's claim (OSK-12108): an
169
+ // attestation whose own window is longer than one approval may stay valid
170
+ // is refused whatever `expiresAt` says, so "answers one person about this
171
+ // call, moments ago" holds even against an issuer that set a year.
172
+ const window = a.expiresAt - a.issuedAt;
173
+ if (!(window > 0) || window > ATTESTED_TOOL_APPROVAL_TTL_MS) {
174
+ return { ok: false, reason: 'window' };
175
+ }
176
+ if (a.toolUseId !== call.toolUseId ||
177
+ a.toolName.toLowerCase() !== String(call.toolName).toLowerCase() ||
178
+ a.inputSha256 !== canonicalToolInputDigest(call.toolInput)) {
179
+ return { ok: false, reason: 'binding' };
180
+ }
181
+ return { ok: true };
182
+ }
@@ -585,11 +585,49 @@ export async function enrollWithServer(opts) {
585
585
  // Enrollment is the CLI's (JWT) job; the daemon holding an opaque
586
586
  // token should not enroll. Guarded at the daemon call site, this is
587
587
  // defense-in-depth for any other opaque caller.
588
- // • 409 already-enrolled — a concurrent binary won the enrollment race
589
- // for the shared keypair; idempotent, nothing to do.
588
+ // • 409 ALREADY_ENROLLED with OUR thumbprint — a concurrent binary won
589
+ // the enrollment race for this keypair; idempotent, nothing to do.
590
+ //
591
+ // A 409 is NOT benign when the row is pinned to a DIFFERENT key
592
+ // (OSK-12063). The server keeps one pin per (user, machine) row while
593
+ // every local profile has its own key, so a 409 can mean "every refresh
594
+ // this key signs will be refused with DEVICE_PROOF_THUMBPRINT_MISMATCH".
595
+ // The server names the pinned thumbprint (`enrolledThumbprint`) so the
596
+ // two cases can be told apart. An older server that omits it keeps the
597
+ // old reading, because there is nothing to compare. A 409
598
+ // ENROLLMENT_DUPLICATE_KEY — this key is pinned on another active row —
599
+ // is never benign either.
600
+ let conflictCode = null;
601
+ let enrolledThumbprint = null;
602
+ if (response.status === 409) {
603
+ try {
604
+ const parsed = JSON.parse(responseText);
605
+ conflictCode = typeof parsed.code === 'string' ? parsed.code : null;
606
+ enrolledThumbprint =
607
+ typeof parsed.enrolledThumbprint === 'string' ? parsed.enrolledThumbprint : null;
608
+ }
609
+ catch {
610
+ /* non-JSON 409 — treat as the legacy benign race */
611
+ }
612
+ }
613
+ const pinnedToOtherKey = response.status === 409 &&
614
+ enrolledThumbprint !== null &&
615
+ enrolledThumbprint !== keypair.thumbprint;
616
+ const keyPinnedElsewhere = response.status === 409 && conflictCode === 'ENROLLMENT_DUPLICATE_KEY';
590
617
  const isBenignRejection = (response.status === 404 && responseText.includes('DAEMON_NOT_FOUND')) ||
591
618
  (response.status === 403 && responseText.includes('OPAQUE_AUTH_REFUSED')) ||
592
- response.status === 409;
619
+ (response.status === 409 && !pinnedToOtherKey && !keyPinnedElsewhere);
620
+ if (pinnedToOtherKey) {
621
+ emit('enrollment_key_mismatch', false, {
622
+ profile,
623
+ thumbprint: keypair.thumbprint,
624
+ enrolledThumbprint,
625
+ serverUrl,
626
+ message: 'this machine is registered with a different device key than this profile signs ' +
627
+ 'with; its refreshes will be refused. An interactive daemon login (skrrd login) ' +
628
+ 're-registers this key.',
629
+ }, { logLevel: 'warn' });
630
+ }
593
631
  if (response.status === 404 && responseText.includes('DAEMON_NOT_FOUND')) {
594
632
  try {
595
633
  atomicWrite0600(backoffPath, JSON.stringify({
@@ -29,11 +29,13 @@ export { __setKekStrategyForTest, getKekStrategy, InMemoryKek, UnavailableKek, K
29
29
  export { buildProof as buildDeviceProof, generateDeviceKeyPair, jwkThumbprint, normalizeHtu, publicJwkFromX, tokenHash, verifyProof as verifyDeviceProof, type DeviceKeyPair, type DeviceProofClaims, type DeviceProofHeader, type Ed25519PrivateJwk, type Ed25519PublicJwk, type VerifyFailureReason as DeviceProofVerifyFailureReason, type VerifyFailure as DeviceProofVerifyFailure, type VerifyOptions as DeviceProofVerifyOptions, type VerifySuccess as DeviceProofVerifySuccess, } from './deviceKey.js';
30
30
  export { initCredEnvelope, resetCredEnvelope, shutdownCredEnvelope, isCredEnvelopeActive, hasCredEnvelopePathConflict, describeCredEnvelopeState, maybeEncryptForWrite, maybeDecryptOnRead, __wrappedDekFilePathForTest, __setStateForTest as __setCredEnvelopeStateForTest, __getStateForTest as __getCredEnvelopeStateForTest, __resetShutdownGuardForTest, type ReadTransformResult, } from './credentialEnvelopeBridge.js';
31
31
  export { initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState, __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, type EnrollmentResult, } from './deviceIdentityBridge.js';
32
+ export { getMachineUuid, hostedRuntimeIdOverride, HOSTED_RUNTIME_MACHINE_SOURCES, __resetMachineUuidCacheForTest, } from './machineUuid.js';
32
33
  export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
33
34
  export { PAIR_AAD_PREFIX, PAIR_AAD_MAX_LEN, PAIR_BUNDLE_MAX_BASE64_LEN, PAIR_NONCE_LEN, PAIR_PUBKEY_LEN, PAIR_SECRET_LEN, PairBundleError, assemblePairBundleBase64, base64ToUint8Array as pairBase64ToUint8Array, base64UrlToUint8Array as pairBase64UrlToUint8Array, buildPairAad, buildPairUrl, generatePairKeyPair, openPairBundleBase64, parsePairBundle, parsePairPlaintext, parsePairUrl, uint8ArrayToBase64 as pairUint8ArrayToBase64, uint8ArrayToBase64Url as pairUint8ArrayToBase64Url, type AssembleBundleOptions as PairAssembleBundleOptions, type OpenBundleOptions as PairOpenBundleOptions, type PairBundle, type PairBundleErrorCode, type PairKeyPair, type PairPlaintextV1, type PairUrlFields, } from './daemonPair.js';
34
35
  export { HARNESS_TIERS, getHarnessTier, credentialPolicyForTier, tierPermitsConfiguredAutoMode, type HarnessTrustTier, type CredentialDeliveryPolicy, type CredentialMode, } from './harnessTrust.js';
35
36
  export { HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT, claudeSkillsLayoutEvidence, harnessLoadsClaudeSkillsLayout, } from './harnessSkillLayout.js';
36
- export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, } from './spawnEnv.js';
37
+ export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, LOOPBACK_NO_PROXY_HOSTS, envHasProxy, withLoopbackNoProxy, } from './spawnEnv.js';
38
+ export { loopbackFetch, assertLoopbackHttpUrl, isLoopbackHostname, isLoopbackAddress, LoopbackTargetRefusedError, LOOPBACK_DEFAULT_MAX_BODY_BYTES, type LoopbackFetch, type LoopbackFetchInit, } from './loopbackHttp.js';
37
39
  export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATIVE_ID_PREFIXES, DEFAULT_BINARY_NAME, } from './localIdentity.js';
38
40
  export { findLegacyLocalState, describeLegacyState, type LegacyStateFinding, type LegacyStateReport, } from './legacyStatePreflight.js';
39
41
  export * from './firstPartyHarness.js';
@@ -118,7 +118,12 @@ export {
118
118
  initDeviceIdentity, resetDeviceIdentity, signServerRequest, enrollWithServer, clearEnrollmentMarker, isDeviceIdentityActive, getDevicePublicKey, describeDeviceIdentityState,
119
119
  // Test seams — file path resolvers + state setters for spec files.
120
120
  __publicKeyPathForTest, __privateKeyPathForTest, __enrollmentMarkerPathForTest, __enrollmentBackoffPathForTest, __setStateForTest as __setDeviceIdentityStateForTest, __getStateForTest as __getDeviceIdentityStateForTest, } from './deviceIdentityBridge.js';
121
+ // Hardware machine identity sent as `machineUuid` (IOPlatformUUID /
122
+ // machine-id / MachineGuid). ONE implementation for daemon and CLI —
123
+ // OSK-12063. Also exposed dependency-free as `@skrr-ai/auth-core/machine-uuid`.
124
+ export { getMachineUuid, hostedRuntimeIdOverride, HOSTED_RUNTIME_MACHINE_SOURCES, __resetMachineUuidCacheForTest, } from './machineUuid.js';
121
125
  // Stable per-machine UUIDv7 identity persisted at <configDir>/daemon.id.
126
+ // NOT a `machineUuid` — see machineUuid.ts.
122
127
  export { ensureMachineDaemonId, legacyDaemonUuids, __resetCachedDaemonIdForTest, } from './machineId.js';
123
128
  // Tier 1 #2 — daemon QR pairing primitives. Pure crypto + AAD framing,
124
129
  // shared between the daemon CLI orchestrator (`daemon/src/pair.ts`) and
@@ -140,7 +145,11 @@ export { HARNESSES_LOADING_CLAUDE_SKILLS_LAYOUT, claudeSkillsLayoutEvidence, har
140
145
  // spawns the SAME engine and did not). `daemon/src/env-sanitize.ts` re-exports
141
146
  // these so its own importers are unchanged, and keeps `filterCustomEnv` — that
142
147
  // governs SERVER-PROVIDED env, which no other process is handed.
143
- export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, } from './spawnEnv.js';
148
+ export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_NAME_TOKENS, SENSITIVE_ENV_PREFIX_EXCEPTIONS, LOOPBACK_NO_PROXY_HOSTS, envHasProxy, withLoopbackNoProxy, } from './spawnEnv.js';
149
+ // OSK-12134 — the one proxy-free client for loopback HTTP. Bun's fetch (and
150
+ // Node's under NODE_USE_ENV_PROXY) sends 127.0.0.1 requests to HTTP_PROXY,
151
+ // credentials and all; every loopback caller goes through this instead.
152
+ export { loopbackFetch, assertLoopbackHttpUrl, isLoopbackHostname, isLoopbackAddress, LoopbackTargetRefusedError, LOOPBACK_DEFAULT_MAX_BODY_BYTES, } from './loopbackHttp.js';
144
153
  /* Local-machine identity — the names this product leaves on a disk. Exported so
145
154
  the CLI, daemon and Desktop share one definition instead of 47 literals. */
146
155
  export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATIVE_ID_PREFIXES, DEFAULT_BINARY_NAME, } from './localIdentity.js';
@@ -0,0 +1,32 @@
1
+ /** Upper bound on a buffered response body. Loopback control-plane bodies are small. */
2
+ export declare const LOOPBACK_DEFAULT_MAX_BODY_BYTES: number;
3
+ /** Thrown (synchronously surfaced as a rejection) for a URL that is not loopback HTTP. */
4
+ export declare class LoopbackTargetRefusedError extends Error {
5
+ readonly code = "LOOPBACK_TARGET_REFUSED";
6
+ constructor(message: string);
7
+ }
8
+ /** True for an IP literal in 127.0.0.0/8, `::1`, or an IPv4-mapped 127/8 address. */
9
+ export declare function isLoopbackAddress(address: string): boolean;
10
+ /** True for a host name or address this client accepts as "this machine". */
11
+ export declare function isLoopbackHostname(hostname: string): boolean;
12
+ /**
13
+ * Parse and validate a loopback HTTP URL. Throws {@link LoopbackTargetRefusedError}
14
+ * for anything that is not `http:` to a loopback host.
15
+ */
16
+ export declare function assertLoopbackHttpUrl(input: string | URL): URL;
17
+ /**
18
+ * `RequestInit`, so a `fetch` call site can switch to {@link loopbackFetch}
19
+ * unchanged. Only `method`, `headers`, `signal` and a string/bytes `body` are
20
+ * honoured; a stream, Blob or FormData body is rejected rather than guessed at.
21
+ */
22
+ export interface LoopbackFetchInit extends RequestInit {
23
+ /** Cap on the buffered response body; exceeding it rejects. */
24
+ maxBodyBytes?: number;
25
+ }
26
+ /** A `fetch`-shaped signature, for callers that keep a `fetchImpl` seam. */
27
+ export type LoopbackFetch = (input: string | URL, init?: LoopbackFetchInit) => Promise<Response>;
28
+ /**
29
+ * `fetch` for a loopback URL, over `node:http`, never through a proxy.
30
+ * Refuses (rejects with {@link LoopbackTargetRefusedError}) any other target.
31
+ */
32
+ export declare const loopbackFetch: LoopbackFetch;