@code-yeongyu/senpi 2026.11.6-3 → 2026.11.9

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 (147) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/bundle/chunks/{chunk-JMQOLOC2.js → chunk-3CRPNPTR.js} +1 -1
  3. package/dist/bundle/chunks/{chunk-5LKB732P.js → chunk-3TZQXZKH.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-X53PD4ZC.js → chunk-45MVD7BU.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-L7ZIS3MC.js → chunk-4JIBEZZE.js} +3 -3
  6. package/dist/bundle/chunks/{chunk-CF5OGBC4.js → chunk-4NYNMAYF.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-6CEDL5S2.js → chunk-4UR6UFBH.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-F5JLDCYD.js → chunk-527Q2UYM.js} +5 -5
  9. package/dist/bundle/chunks/{chunk-7DRWPZ3N.js → chunk-622VYTRI.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-GKAYPJ4E.js → chunk-7PROV6BB.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-4E7GDG4H.js → chunk-7VMEE62L.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-CFEHJ4VU.js → chunk-HXNXISJD.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-4BATXPB7.js → chunk-K3UHGDUQ.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-SB6MXNO5.js → chunk-M7GF73FO.js} +2 -2
  15. package/dist/bundle/chunks/{chunk-V2QAYGOK.js → chunk-OCMGRBJ5.js} +22 -22
  16. package/dist/bundle/chunks/{chunk-EO2VPZ2K.js → chunk-SGAFIMAU.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-BASS47UD.js → chunk-STGOYIXC.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-DSWDJ7HT.js → chunk-WQ4RBPMS.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-MJTCZUXS.js → chunk-WX2HB4DD.js} +1 -1
  20. package/dist/bundle/chunks/chunk-XZDCMDUH.js +2 -0
  21. package/dist/bundle/chunks/{chunk-LUZVGPQ7.js → chunk-Y5L4UFMI.js} +1 -1
  22. package/dist/bundle/chunks/{cli-main-RWHDZKTQ.js → cli-main-E4NDWAUD.js} +1 -1
  23. package/dist/bundle/chunks/{hand-entry-G5LYF62Q.js → hand-entry-5X5Z5Y3D.js} +1 -1
  24. package/dist/bundle/chunks/{headless-host-ES74ZB6M.js → headless-host-YXUGONR3.js} +1 -1
  25. package/dist/bundle/chunks/{host-command-I2JZ5JM7.js → host-command-VHX2ILLM.js} +1 -1
  26. package/dist/bundle/chunks/{host-session-command-Y52NFPP2.js → host-session-command-P3MRW63Z.js} +1 -1
  27. package/dist/bundle/chunks/{interactive-mode-PA7DGVS6.js → interactive-mode-XUSBLLWI.js} +1 -1
  28. package/dist/bundle/chunks/{main-OBL6375C.js → main-3TWVKTJN.js} +1 -1
  29. package/dist/bundle/chunks/{multi-session-host-NM6I5BWZ.js → multi-session-host-HRGM66PY.js} +1 -1
  30. package/dist/bundle/chunks/{package-manager-cli-GNRQ5MVR.js → package-manager-cli-WSH7U4YE.js} +1 -1
  31. package/dist/bundle/chunks/{role-F56BS2ZQ.js → role-4BUU46VW.js} +3 -3
  32. package/dist/bundle/chunks/{rotation-stream-BN2YK2MC.js → rotation-stream-TODBIHXJ.js} +1 -1
  33. package/dist/bundle/chunks/{rpc-mode-G4KIP7XS.js → rpc-mode-OAPIV46G.js} +1 -1
  34. package/dist/bundle/chunks/{run-attached-tui-XTGPAGYM.js → run-attached-tui-MXCC5IZR.js} +1 -1
  35. package/dist/bundle/chunks/{schedule-command-L4M3TSQ3.js → schedule-command-MX56D6UE.js} +1 -1
  36. package/dist/bundle/chunks/{session-control-endpoint-V46WMCDF.js → session-control-endpoint-IL6ERD4U.js} +1 -1
  37. package/dist/bundle/chunks/{session-picker-AG6KSR5A.js → session-picker-6VRC25MJ.js} +1 -1
  38. package/dist/bundle/chunks/session-worker.js +44 -44
  39. package/dist/bundle/cli.js +2 -2
  40. package/dist/bundle/index.js +1 -1
  41. package/dist/bundle/rpc-entry.js +1 -1
  42. package/dist/bundle/runtime-manifest.json +1 -1
  43. package/dist/core/agent-session-services.d.ts +2 -0
  44. package/dist/core/agent-session-services.js +1 -0
  45. package/dist/core/agent-session.d.ts +8 -0
  46. package/dist/core/agent-session.js +51 -4
  47. package/dist/core/egress/classify.d.ts +8 -3
  48. package/dist/core/egress/classify.js +177 -158
  49. package/dist/core/egress/console-redactor.d.ts +7 -0
  50. package/dist/core/egress/console-redactor.js +74 -0
  51. package/dist/core/egress/credentials.d.ts +73 -0
  52. package/dist/core/egress/credentials.js +20 -0
  53. package/dist/core/egress/device-custody.d.ts +1 -1
  54. package/dist/core/egress/device-custody.js +18 -15
  55. package/dist/core/egress/device-proxy-call.d.ts +59 -0
  56. package/dist/core/egress/device-proxy-call.js +148 -0
  57. package/dist/core/egress/device-proxy-lost-calls.d.ts +27 -0
  58. package/dist/core/egress/device-proxy-lost-calls.js +28 -0
  59. package/dist/core/egress/device-proxy-ports.d.ts +49 -0
  60. package/dist/core/egress/device-proxy-ports.js +2 -0
  61. package/dist/core/egress/device-proxy-request.d.ts +34 -0
  62. package/dist/core/egress/device-proxy-request.js +69 -0
  63. package/dist/core/egress/device-proxy-waits.d.ts +40 -0
  64. package/dist/core/egress/device-proxy-waits.js +67 -0
  65. package/dist/core/egress/device-proxy.d.ts +3 -0
  66. package/dist/core/egress/device-proxy.js +214 -0
  67. package/dist/core/egress/endpoint.js +2 -1
  68. package/dist/core/egress/entry-verify.d.ts +13 -0
  69. package/dist/core/egress/entry-verify.js +33 -0
  70. package/dist/core/egress/hpke.js +13 -9
  71. package/dist/core/egress/key-policy-template.d.ts +7 -1
  72. package/dist/core/egress/ledger-schema.d.ts +80 -0
  73. package/dist/core/egress/ledger-schema.js +117 -0
  74. package/dist/core/egress/ledger.d.ts +55 -0
  75. package/dist/core/egress/ledger.js +159 -0
  76. package/dist/core/egress/pinned-infra.d.ts +23 -2
  77. package/dist/core/egress/pinned-infra.js +9 -2
  78. package/dist/core/egress/redact.d.ts +10 -0
  79. package/dist/core/egress/redact.js +134 -0
  80. package/dist/core/egress/sealed-source.d.ts +35 -0
  81. package/dist/core/egress/sealed-source.js +140 -0
  82. package/dist/core/egress/verified-keys.js +1 -1
  83. package/dist/core/egress/wire.d.ts +5 -3
  84. package/dist/core/egress/wire.js +5 -3
  85. package/dist/core/extensions/builtin/monitor-state-event.d.ts +26 -0
  86. package/dist/core/extensions/builtin/monitor-state-event.js +2 -0
  87. package/dist/core/extensions/builtin/terminal/background-age.d.ts +25 -0
  88. package/dist/core/extensions/builtin/terminal/background-age.js +91 -0
  89. package/dist/core/extensions/builtin/terminal/extension-state.d.ts +1 -1
  90. package/dist/core/extensions/builtin/terminal/extension-state.js +21 -3
  91. package/dist/core/extensions/builtin/terminal/notify.d.ts +6 -0
  92. package/dist/core/extensions/builtin/terminal/notify.js +39 -5
  93. package/dist/core/extensions/builtin/terminal/session-bundle.d.ts +9 -2
  94. package/dist/core/extensions/builtin/terminal/session-bundle.js +36 -1
  95. package/dist/core/extensions/builtin/terminal/settings.d.ts +2 -0
  96. package/dist/core/extensions/builtin/terminal/settings.js +8 -0
  97. package/dist/core/extensions/builtin/terminal/tools/background-auto-reap.d.ts +15 -0
  98. package/dist/core/extensions/builtin/terminal/tools/background-auto-reap.js +87 -0
  99. package/dist/core/extensions/builtin/terminal/tools/bash.d.ts +2 -0
  100. package/dist/core/extensions/builtin/terminal/tools/bash.js +6 -6
  101. package/dist/core/extensions/builtin/terminal/tools/child-placement.d.ts +8 -0
  102. package/dist/core/extensions/builtin/terminal/tools/child-placement.js +43 -6
  103. package/dist/core/extensions/builtin/terminal/tools/context.d.ts +11 -2
  104. package/dist/core/extensions/session-control-types.d.ts +15 -0
  105. package/dist/core/extensions/session-control-types.js +11 -0
  106. package/dist/core/external-admission.d.ts +16 -2
  107. package/dist/core/external-admission.js +22 -4
  108. package/dist/core/retry-fallback/controller.d.ts +1 -0
  109. package/dist/core/retry-fallback/controller.js +3 -0
  110. package/dist/core/retry-fallback/unusable.d.ts +2 -0
  111. package/dist/core/retry-fallback/unusable.js +7 -1
  112. package/dist/core/retry-fallback/usage-limit.d.ts +6 -1
  113. package/dist/core/retry-fallback/usage-limit.js +9 -4
  114. package/dist/core/sdk.d.ts +6 -1
  115. package/dist/core/sdk.js +1 -0
  116. package/dist/core/terminal-settings.d.ts +1 -0
  117. package/dist/core/turn-origin.d.ts +4 -0
  118. package/dist/core/turn-origin.js +10 -0
  119. package/dist/modes/interactive/components/remote-delivery-message.d.ts +2 -1
  120. package/dist/modes/interactive/components/remote-delivery-message.js +13 -5
  121. package/dist/modes/rpc/admit-external-message-command.d.ts +37 -0
  122. package/dist/modes/rpc/admit-external-message-command.js +61 -0
  123. package/dist/modes/rpc/admit-external-message-wire.d.ts +42 -0
  124. package/dist/modes/rpc/admit-external-message-wire.js +55 -0
  125. package/dist/modes/rpc/connection-handler.js +14 -0
  126. package/dist/modes/rpc/effect-commands.d.ts +1 -1
  127. package/dist/modes/rpc/host-idle-handover.js +1 -0
  128. package/dist/modes/rpc/method-classes-schema-delivery.d.ts +32 -0
  129. package/dist/modes/rpc/method-classes-schema-delivery.js +22 -0
  130. package/dist/modes/rpc/method-classes-schema-session.d.ts +27 -0
  131. package/dist/modes/rpc/method-classes-schema-session.js +2 -0
  132. package/dist/modes/rpc/method-classes-schema.d.ts +27 -0
  133. package/dist/modes/rpc/method-classes.d.ts +1 -0
  134. package/dist/modes/rpc/method-classes.js +2 -0
  135. package/dist/modes/rpc/rpc-input-validation.js +4 -3
  136. package/dist/modes/rpc/rpc-response-types.d.ts +2 -1
  137. package/dist/modes/rpc/rpc-session-command-types.d.ts +2 -1
  138. package/dist/modes/rpc/tool-child/terminal-jobs.d.ts +1 -0
  139. package/dist/modes/rpc/tool-child/terminal-jobs.js +17 -1
  140. package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
  141. package/docs/engine-socket/fixtures/rpc-inbound-samples.json +19 -2
  142. package/docs/rpc.md +57 -0
  143. package/docs/terminal-tools.md +25 -1
  144. package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
  145. package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
  146. package/package.json +6 -6
  147. package/dist/bundle/chunks/chunk-UODSYQ2D.js +0 -2
@@ -0,0 +1,214 @@
1
+ import { createAssistantMessageEventStream, } from "@earendil-works/pi-ai";
2
+ import { followCall } from "./device-proxy-call.js";
3
+ import { lostCallPacer } from "./device-proxy-lost-calls.js";
4
+ import { ABORT_TEXT, admittedRequestJson, failureMessage, parseStoredRequest, planDispatch, } from "./device-proxy-request.js";
5
+ import { createAbortion, nextKeyBackoffMs, stepWaits } from "./device-proxy-waits.js";
6
+ /** The `done` reason of a finished, successful stop; `undefined` (never replayed) for error, aborted or pending. */
7
+ const doneReason = (stopReason) => stopReason === "error" || stopReason === "aborted" || stopReason === "pending" ? undefined : stopReason;
8
+ export function createDeviceProxyEgress(deps) {
9
+ const { ledger, route, host } = deps;
10
+ const abortion = createAbortion();
11
+ const waitsOf = (run) => stepWaits(run.out, host, run.step, run.signal, abortion);
12
+ const callContext = (run, deviceId) => ({
13
+ out: run.out,
14
+ model: run.model,
15
+ ledger,
16
+ channel: deps.hands.channel(deviceId),
17
+ offlineFor: deps.hands.offlineFor,
18
+ waits: waitsOf(run),
19
+ conditions: deps.conditions,
20
+ clock: deps.clock,
21
+ bounds: deps.bounds,
22
+ signal: run.signal,
23
+ abortion,
24
+ claim: deps.claims.current,
25
+ live: run.live,
26
+ });
27
+ const servedDecision = async (run) => {
28
+ for (;;) {
29
+ const decision = route.decide(run.model.provider, await ledger.variants(run.model.provider));
30
+ const { deviceId } = decision;
31
+ if (decision.servedNow && decision.route === "device" && deviceId !== undefined)
32
+ return { ...decision, deviceId };
33
+ if (decision.servedNow)
34
+ throw new Error(`the device route cannot serve a ${decision.route} decision`);
35
+ const waitFor = decision.waitFor ?? [];
36
+ const wait = { reason: decision.reason, steer: decision.steer ?? "", waitFor };
37
+ await waitsOf(run).hold(wait, deps.conditions.until(waitFor, run.signal));
38
+ }
39
+ };
40
+ /** 4.6: renew an expired or superseded claim, back off on an unknown registry key, and restart the same call. */
41
+ const startWithClaim = async (run, dispatched, decision, plan) => {
42
+ let row = dispatched;
43
+ const deviceId = row.deviceId ?? "";
44
+ const expectedSourceDigest = decision.serveOwnDefinition ? undefined : route.headDigest(row.providerId);
45
+ const maxOutputTokens = run.options?.maxTokens;
46
+ let backoff;
47
+ for (;;) {
48
+ const start = {
49
+ t: "provider.call.start",
50
+ callId: row.callId,
51
+ sessionId: deps.sessionId,
52
+ call: row.call,
53
+ claim: await deps.claims.current(),
54
+ providerId: row.providerId,
55
+ modelId: row.modelId,
56
+ ...(expectedSourceDigest === undefined ? {} : { expectedSourceDigest }),
57
+ hooks: plan.hooks,
58
+ ...(deps.cwd === undefined ? {} : { cwd: deps.cwd }),
59
+ request: parseStoredRequest(plan.requestJson),
60
+ budget: {
61
+ deadlineMs: deps.bounds.deadlineMs,
62
+ ...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
63
+ },
64
+ };
65
+ const outcome = await followCall(callContext(run, deviceId), row, { start });
66
+ if (outcome.kind !== "refused")
67
+ return outcome;
68
+ await deps.hands.channel(deviceId).ack({ t: "provider.call.ack", callId: row.callId });
69
+ const waitFor = [{ kind: "claim-renewed" }];
70
+ const code = outcome.code;
71
+ if (code === "registry_key_unknown") {
72
+ backoff = nextKeyBackoffMs(backoff);
73
+ const pause = deps.clock.sleep(backoff, run.signal);
74
+ await waitsOf(run).hold({ reason: "claim-expired", steer: outcome.message, waitFor }, pause);
75
+ }
76
+ else {
77
+ const steer = "renewing this session's placement claim";
78
+ await waitsOf(run).hold({ reason: "claim-expired", steer, waitFor }, deps.claims.renew(code));
79
+ }
80
+ row = await ledger.replaceRefused(outcome.row, crypto.randomUUID());
81
+ }
82
+ };
83
+ /** A lost (`unknown`) or mismatched call is superseded; a mismatch records the device variant in the same write. */
84
+ const retire = async (run, outcome) => {
85
+ const { row } = outcome;
86
+ const deviceId = row.deviceId ?? "";
87
+ if (outcome.kind === "unknown")
88
+ await deps.hands
89
+ .channel(deviceId)
90
+ .cancel({ t: "provider.call.cancel", callId: row.callId, reason: "superseded" });
91
+ const headDigest = route.headDigest(row.providerId);
92
+ const variant = outcome.kind === "mismatch" && headDigest !== undefined
93
+ ? {
94
+ providerId: row.providerId,
95
+ deviceId,
96
+ stepId: run.step.stepId,
97
+ headDigest,
98
+ liveAtMismatch: route.liveDigest(deviceId, row.providerId),
99
+ }
100
+ : undefined;
101
+ const superseded = await ledger.supersede(row, variant);
102
+ run.live.row = superseded;
103
+ deps.notify({ t: "step.reset", sessionId: deps.sessionId, stepId: run.step.stepId, cause: "superseded" });
104
+ return superseded;
105
+ };
106
+ const serve = async (run, pacer, firstRequest, firstPrevious) => {
107
+ let stored = firstRequest;
108
+ let previous = firstPrevious;
109
+ let attempt = previous && previous.call.kind === "step" ? previous.call.attempt + 1 : run.step.attempt;
110
+ const providerId = run.model.provider;
111
+ for (;;) {
112
+ if (previous !== undefined && route.capability(providerId) === "callback-stream") {
113
+ const waitFor = [{ kind: "user-choice", choiceId: `${run.step.stepId}:${attempt}` }];
114
+ const steer = "re-run this step (tools may run again) or select another device";
115
+ await waitsOf(run).hold({ reason: "capability:callback-stream", steer, waitFor }, deps.conditions.until(waitFor, run.signal));
116
+ }
117
+ const decision = await servedDecision(run);
118
+ const plan = await planDispatch(decision, stored, deps.requestHooks);
119
+ const next = {
120
+ callId: crypto.randomUUID(),
121
+ sessionId: deps.sessionId,
122
+ call: { kind: "step", stepId: run.step.stepId, attempt },
123
+ route: "device",
124
+ deviceId: decision.deviceId,
125
+ providerId,
126
+ modelId: run.model.id,
127
+ epoch: run.step.fence.epoch,
128
+ capability: route.capability(providerId),
129
+ hooksApplied: plan.hooksApplied,
130
+ };
131
+ const row = await ledger.dispatch(next, plan.requestJson, previous);
132
+ const outcome = await startWithClaim(run, row, decision, plan);
133
+ if (outcome.kind === "ended")
134
+ return;
135
+ previous = await retire(run, outcome);
136
+ stored = { requestJson: await ledger.readRequest(previous), hooksApplied: previous.hooksApplied };
137
+ attempt += 1;
138
+ if (outcome.kind === "unknown")
139
+ await pacer.beforeReissue(decision.deviceId, attempt);
140
+ }
141
+ };
142
+ /** The 4.5 replay rules for a step that already has a row, applied before any route decision or request hook. */
143
+ const runStep = async (run, context) => {
144
+ const { stepId } = run.step;
145
+ if (await host.historyHasStep(stepId))
146
+ throw new Error(`step ${stepId} is already committed; resume from history`);
147
+ if (await ledger.isCancelled(stepId)) {
148
+ run.out.push({ type: "error", reason: "aborted", error: failureMessage(run.model, "aborted", ABORT_TEXT) });
149
+ return;
150
+ }
151
+ const latest = (await ledger.rowsForStep(stepId)).at(-1);
152
+ const reason = latest?.result?.kind === "message" ? doneReason(latest.result.stopReason) : undefined;
153
+ if (latest?.state === "ended" && reason !== undefined) {
154
+ const message = await ledger.readMessage(latest);
155
+ run.out.push({ type: "start", partial: { ...message } });
156
+ run.out.push({ type: "done", reason, message });
157
+ return;
158
+ }
159
+ const askUser = route.capability(run.model.provider) !== "callback-stream";
160
+ const pacer = lostCallPacer({ ...deps, stepId, waits: waitsOf(run), signal: run.signal, askUser });
161
+ const resumable = latest?.state === "dispatched" || latest?.state === "ended" || latest?.state === "superseded";
162
+ if (!latest || !resumable) {
163
+ const admitted = { requestJson: admittedRequestJson(context, run.options), hooksApplied: false };
164
+ return serve(run, pacer, admitted, undefined);
165
+ }
166
+ let previous = latest;
167
+ // The resumed row is live until the next W4 retires it: an abort in the waits before that must delete its chunks.
168
+ run.live.row = latest;
169
+ if (latest.state === "dispatched") {
170
+ const deviceId = latest.deviceId ?? "";
171
+ if (deps.hands.offlineFor(deviceId) >= deps.bounds.resumeWindowMs) {
172
+ const waitFor = [{ kind: "device-online", deviceId }];
173
+ const pinned = deps.conditions.until(waitFor, run.signal);
174
+ await waitsOf(run).hold({ reason: "device-offline", steer: `open omo on ${deviceId}`, waitFor }, pinned);
175
+ }
176
+ const outcome = await followCall(callContext(run, deviceId), latest, { resume: true });
177
+ if (outcome.kind === "ended")
178
+ return;
179
+ if (outcome.kind === "refused")
180
+ throw new Error(`device ${deviceId} refused the placement claim: ${outcome.message}`);
181
+ previous = await retire(run, outcome);
182
+ if (outcome.kind === "unknown" && latest.call.kind === "step")
183
+ await pacer.beforeReissue(deviceId, latest.call.attempt + 1);
184
+ }
185
+ const stored = { requestJson: await ledger.readRequest(previous), hooksApplied: previous.hooksApplied };
186
+ return serve(run, pacer, stored, previous);
187
+ };
188
+ const abortFailure = async (run) => {
189
+ const row = run.live.row;
190
+ let cause;
191
+ await ledger.cancelStep(run.step.stepId, row).catch((error) => {
192
+ cause = error;
193
+ });
194
+ if (row?.deviceId !== undefined && row.state === "dispatched")
195
+ await deps.hands
196
+ .channel(row.deviceId)
197
+ .cancel({ t: "provider.call.cancel", callId: row.callId, reason: "user" });
198
+ return new Error(ABORT_TEXT, cause === undefined ? undefined : { cause });
199
+ };
200
+ return {
201
+ stream: (model, context, options) => {
202
+ const out = createAssistantMessageEventStream();
203
+ const signal = options?.signal ?? new AbortController().signal;
204
+ const run = { out, model, options, step: host.currentStep(), signal, live: { row: undefined } };
205
+ out.trackLocalWork(runStep(run, context)).catch(async (error) => {
206
+ if (!signal.aborted)
207
+ return out.fail(error);
208
+ out.fail(await abortFailure(run).catch((cause) => new Error(ABORT_TEXT, { cause })));
209
+ });
210
+ return out;
211
+ },
212
+ };
213
+ }
214
+ //# sourceMappingURL=device-proxy.js.map
@@ -36,7 +36,8 @@ function isTemplatedUrlPart(raw) {
36
36
  return isCommandConfigValue(raw) || getConfigValueEnvVarNames(raw).length > 0;
37
37
  }
38
38
  function isKeyishQueryName(name) {
39
- return KEYISH_QUERY_NAMES.includes(name.toLowerCase());
39
+ const lower = name.toLowerCase();
40
+ return KEYISH_QUERY_NAMES.some((keyish) => keyish === lower);
40
41
  }
41
42
  export function splitEndpoint(baseUrl, secretUrlParts = []) {
42
43
  const match = URL_SHAPE.exec(baseUrl);
@@ -0,0 +1,13 @@
1
+ export declare const ENTRY_SIG_DOMAIN = "omo-custody/provider-entry/v1";
2
+ /** r11.1 C.P5: entrySig signs ASCII(domain) || 0x00 || ASCII(entryDigest), entryDigest in lowercase hex. */
3
+ export declare function entrySigPayload(entryDigest: string): Uint8Array;
4
+ export interface EntrySigInput {
5
+ readonly entryDigest: string;
6
+ /** Canonical base64url without padding. */
7
+ readonly entrySig: string;
8
+ /** The owner's enrolled Ed25519 public key (32 raw bytes). */
9
+ readonly ownerPublicKey: Uint8Array;
10
+ }
11
+ /** The brain-side check before dialing (custody design 3.2); a false result is a wait, never a dial. */
12
+ export declare function verifyEntrySig(input: EntrySigInput): Promise<boolean>;
13
+ //# sourceMappingURL=entry-verify.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { strictEd25519Verify } from "../../modes/rpc/ed25519-strict.js";
2
+ export const ENTRY_SIG_DOMAIN = "omo-custody/provider-entry/v1";
3
+ const LOWER_HEX = /^[0-9a-f]+$/u;
4
+ const BASE64URL_NO_PAD = /^[A-Za-z0-9_-]+$/u;
5
+ const ED25519_PUBLIC_KEY_BYTES = 32;
6
+ const ED25519_SIGNATURE_BYTES = 64;
7
+ /** r11.1 C.P5: entrySig signs ASCII(domain) || 0x00 || ASCII(entryDigest), entryDigest in lowercase hex. */
8
+ export function entrySigPayload(entryDigest) {
9
+ if (!LOWER_HEX.test(entryDigest))
10
+ throw new TypeError("entryDigest must be lowercase hexadecimal");
11
+ const domain = Buffer.from(ENTRY_SIG_DOMAIN, "ascii");
12
+ return new Uint8Array(Buffer.concat([domain, Buffer.from([0]), Buffer.from(entryDigest, "ascii")]));
13
+ }
14
+ function decodeSignature(entrySig) {
15
+ if (!BASE64URL_NO_PAD.test(entrySig))
16
+ return undefined;
17
+ const bytes = Buffer.from(entrySig, "base64url");
18
+ if (bytes.length !== ED25519_SIGNATURE_BYTES || bytes.toString("base64url") !== entrySig)
19
+ return undefined;
20
+ return new Uint8Array(bytes);
21
+ }
22
+ /** The brain-side check before dialing (custody design 3.2); a false result is a wait, never a dial. */
23
+ export async function verifyEntrySig(input) {
24
+ if (input.ownerPublicKey.length !== ED25519_PUBLIC_KEY_BYTES)
25
+ return false;
26
+ if (!LOWER_HEX.test(input.entryDigest))
27
+ return false;
28
+ const signature = decodeSignature(input.entrySig);
29
+ if (signature === undefined)
30
+ return false;
31
+ return strictEd25519Verify(input.ownerPublicKey, entrySigPayload(input.entryDigest), signature);
32
+ }
33
+ //# sourceMappingURL=entry-verify.js.map
@@ -46,8 +46,12 @@ function concat(...parts) {
46
46
  }
47
47
  return out;
48
48
  }
49
- const KEM_SUITE_ID = concat(encoder.encode("KEM"), i2osp(KEM_ID, 2));
50
- const HPKE_SUITE_ID = concat(encoder.encode("HPKE"), i2osp(KEM_ID, 2), i2osp(KDF_ID, 2), i2osp(AEAD_ID, 2));
49
+ function kemSuiteId() {
50
+ return concat(encoder.encode("KEM"), i2osp(KEM_ID, 2));
51
+ }
52
+ function hpkeSuiteId() {
53
+ return concat(encoder.encode("HPKE"), i2osp(KEM_ID, 2), i2osp(KDF_ID, 2), i2osp(AEAD_ID, 2));
54
+ }
51
55
  async function hmacSha256(key, data) {
52
56
  const hmacKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
53
57
  return new Uint8Array(await crypto.subtle.sign("HMAC", hmacKey, data));
@@ -73,8 +77,8 @@ function labeledExpand(suiteId, prk, label, info, length) {
73
77
  return hkdfExpand(prk, concat(i2osp(length, 2), encoder.encode(HPKE_V1), suiteId, encoder.encode(label), info), length);
74
78
  }
75
79
  async function extractAndExpand(dh, kemContext) {
76
- const eaePrk = await labeledExtract(KEM_SUITE_ID, new Uint8Array(), "eae_prk", dh);
77
- return labeledExpand(KEM_SUITE_ID, eaePrk, "shared_secret", kemContext, N_SECRET);
80
+ const eaePrk = await labeledExtract(kemSuiteId(), new Uint8Array(), "eae_prk", dh);
81
+ return labeledExpand(kemSuiteId(), eaePrk, "shared_secret", kemContext, N_SECRET);
78
82
  }
79
83
  async function importPublicKey(raw) {
80
84
  if (raw.length !== N_PK || raw[0] !== 0x04)
@@ -140,13 +144,13 @@ async function encapWith(recipientPublicKey, ephemeral, enc) {
140
144
  /** RFC 9180 KeySchedule for mode_base (no PSK). The exporter secret is not derived: the format never exports. */
141
145
  export async function hpkeKeySchedule(sharedSecret, info) {
142
146
  const empty = new Uint8Array();
143
- const pskIdHash = await labeledExtract(HPKE_SUITE_ID, empty, "psk_id_hash", empty);
144
- const infoHash = await labeledExtract(HPKE_SUITE_ID, empty, "info_hash", info);
147
+ const pskIdHash = await labeledExtract(hpkeSuiteId(), empty, "psk_id_hash", empty);
148
+ const infoHash = await labeledExtract(hpkeSuiteId(), empty, "info_hash", info);
145
149
  const context = concat(Uint8Array.of(MODE_BASE), pskIdHash, infoHash);
146
- const secret = await labeledExtract(HPKE_SUITE_ID, sharedSecret, "secret", empty);
150
+ const secret = await labeledExtract(hpkeSuiteId(), sharedSecret, "secret", empty);
147
151
  return {
148
- key: await labeledExpand(HPKE_SUITE_ID, secret, "key", context, N_K),
149
- baseNonce: await labeledExpand(HPKE_SUITE_ID, secret, "base_nonce", context, N_N),
152
+ key: await labeledExpand(hpkeSuiteId(), secret, "key", context, N_K),
153
+ baseNonce: await labeledExpand(hpkeSuiteId(), secret, "base_nonce", context, N_N),
150
154
  };
151
155
  }
152
156
  async function aesGcmKey(key, usage) {
@@ -13,5 +13,11 @@ export interface KeyPolicyTemplate {
13
13
  readonly roles: CustodyRoleArns;
14
14
  }): string;
15
15
  }
16
- export declare const KEY_POLICY_TEMPLATE_V1: KeyPolicyTemplate;
16
+ export declare const KEY_POLICY_TEMPLATE_V1: {
17
+ readonly render: ({ keyKind, roles }: {
18
+ readonly accountId: string;
19
+ readonly keyKind: KeyPolicyKind;
20
+ readonly roles: CustodyRoleArns;
21
+ }) => string;
22
+ };
17
23
  //# sourceMappingURL=key-policy-template.d.ts.map
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Schema of the brain's `egress_call` ledger (row CK9; custody design 4.5, 2.3, 4.2, 15.7). One row per dialed model
3
+ * call with its admitted request and, once `ended`, its assembled message stored beside it in 96 KiB chunks (never the
4
+ * event log: every streamed event carries the whole message so far), the step-cancelled marker (W14), and the
5
+ * per-session device variants (`session/variants`, read by the route). Every value here is written by `ledger.ts`
6
+ * through the host's admitted mutations; this module only names keys and converts rows to and from store values.
7
+ */
8
+ import type { DeviceId, ProviderId } from "@earendil-works/pi-agent-core";
9
+ import type { StoreValue } from "@earendil-works/pi-agent-core/conformance";
10
+ import type { AssistantMessage, StopReason, Usage } from "@earendil-works/pi-ai";
11
+ import { type ProviderCallErrorCode, type ProviderCallKind } from "./wire.ts";
12
+ /** `dispatched` -> `ended` -> `closed` (by the step commit, W9); `superseded` and `cancelled` are final. */
13
+ export type EgressCallState = "dispatched" | "ended" | "superseded" | "cancelled" | "closed";
14
+ /** `callback-stream` providers run tools inside the stream, so every reissue of their step needs a user choice. */
15
+ export type EgressCallCapability = "callback-stream" | "stream";
16
+ /** `message`: the assembled message lives in `chunks` result chunks; the row keeps only its stop reason and usage. */
17
+ export type EgressCallResult = {
18
+ readonly kind: "message";
19
+ readonly stopReason: StopReason;
20
+ readonly usage: Usage;
21
+ readonly chunks: number;
22
+ } | {
23
+ readonly kind: "error";
24
+ readonly code: ProviderCallErrorCode;
25
+ readonly message: string;
26
+ readonly retryable: boolean;
27
+ readonly usage?: Usage;
28
+ };
29
+ /** What `ended` records: the assembled message of a finished call, or the hand's refusal or failure. */
30
+ export type EgressCallEnd = {
31
+ readonly kind: "message";
32
+ readonly message: AssistantMessage;
33
+ } | Extract<EgressCallResult, {
34
+ readonly kind: "error";
35
+ }>;
36
+ export interface EgressCallRow {
37
+ readonly callId: string;
38
+ readonly sessionId: string;
39
+ readonly call: ProviderCallKind;
40
+ readonly route: "sealed" | "device";
41
+ readonly deviceId?: DeviceId;
42
+ readonly providerId: ProviderId;
43
+ readonly modelId: string;
44
+ readonly epoch: number;
45
+ readonly capability: EgressCallCapability;
46
+ /** The stored request already carries this session's request hooks (4.5). */
47
+ readonly hooksApplied: boolean;
48
+ readonly state: EgressCallState;
49
+ readonly requestChunks: number;
50
+ readonly result?: EgressCallResult;
51
+ }
52
+ /** Durable `session/variants[P][deviceId]` (4.2); `mismatches` counts per (device, step) and restarts on a new head. */
53
+ export interface VariantRecord {
54
+ readonly headDigest: string;
55
+ readonly liveAtMismatch: string | undefined;
56
+ readonly mismatches: number;
57
+ }
58
+ export declare const REQUEST_CHUNK_BYTES: number;
59
+ export declare const callKey: (callId: string) => string;
60
+ export declare const requestPrefix: (callId: string) => string;
61
+ export declare const requestChunkKey: (callId: string, index: number) => string;
62
+ export declare const resultPrefix: (callId: string) => string;
63
+ export declare const resultChunkKey: (callId: string, index: number) => string;
64
+ export declare const stepIndexPrefix: (stepId: string) => string;
65
+ export declare const stepIndexKey: (stepId: string, attempt: number) => string;
66
+ export declare const cancelledKey: (stepId: string) => string;
67
+ export declare const variantPrefix: (providerId: ProviderId) => string;
68
+ export declare const variantKey: (providerId: ProviderId, deviceId: DeviceId) => string;
69
+ /** Splits UTF-8 text into chunks of at most `REQUEST_CHUNK_BYTES` bytes, never inside a code point. */
70
+ export declare function splitUtf8(text: string): string[];
71
+ /** The stored message text is the ledger's own JSON; the shape check guards against a truncated or foreign chunk set. */
72
+ export declare function isStoredMessage(value: unknown): value is AssistantMessage;
73
+ export declare function encodeCallRow(row: EgressCallRow): StoreValue;
74
+ export declare function decodeCallRow(value: StoreValue | undefined): EgressCallRow | undefined;
75
+ export interface StoredVariant extends VariantRecord {
76
+ readonly stepId: string;
77
+ }
78
+ export declare function decodeVariant(value: StoreValue | undefined): StoredVariant | undefined;
79
+ export declare const encodeVariant: (variant: StoredVariant) => StoreValue;
80
+ //# sourceMappingURL=ledger-schema.d.ts.map
@@ -0,0 +1,117 @@
1
+ import { PROVIDER_CALL_ERROR_CODES } from "./wire.js";
2
+ export const REQUEST_CHUNK_BYTES = 96 * 1024;
3
+ const pad = (index) => String(index).padStart(6, "0");
4
+ export const callKey = (callId) => `egress_call/${encodeURIComponent(callId)}`;
5
+ export const requestPrefix = (callId) => `egress_call_request/${encodeURIComponent(callId)}/`;
6
+ export const requestChunkKey = (callId, index) => `${requestPrefix(callId)}${pad(index)}`;
7
+ export const resultPrefix = (callId) => `egress_call_result/${encodeURIComponent(callId)}/`;
8
+ export const resultChunkKey = (callId, index) => `${resultPrefix(callId)}${pad(index)}`;
9
+ export const stepIndexPrefix = (stepId) => `egress_step/${encodeURIComponent(stepId)}/`;
10
+ export const stepIndexKey = (stepId, attempt) => `${stepIndexPrefix(stepId)}${pad(attempt)}`;
11
+ export const cancelledKey = (stepId) => `session/step.cancelled/${encodeURIComponent(stepId)}`;
12
+ export const variantPrefix = (providerId) => `session/variants/${encodeURIComponent(providerId)}/`;
13
+ export const variantKey = (providerId, deviceId) => `${variantPrefix(providerId)}${encodeURIComponent(deviceId)}`;
14
+ /** Splits UTF-8 text into chunks of at most `REQUEST_CHUNK_BYTES` bytes, never inside a code point. */
15
+ export function splitUtf8(text) {
16
+ const bytes = new TextEncoder().encode(text);
17
+ const decoder = new TextDecoder();
18
+ const chunks = [];
19
+ for (let start = 0; start < bytes.length;) {
20
+ let end = Math.min(start + REQUEST_CHUNK_BYTES, bytes.length);
21
+ while (end < bytes.length && ((bytes[end] ?? 0) & 0xc0) === 0x80)
22
+ end--;
23
+ chunks.push(decoder.decode(bytes.subarray(start, end)));
24
+ start = end;
25
+ }
26
+ return chunks.length > 0 ? chunks : [""];
27
+ }
28
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
29
+ const isErrorCode = (value) => PROVIDER_CALL_ERROR_CODES.some((code) => code === value);
30
+ const isState = (value) => value === "dispatched" || value === "ended" || value === "superseded" || value === "cancelled" || value === "closed";
31
+ function decodeCall(value) {
32
+ if (!isObject(value))
33
+ return undefined;
34
+ if (value.kind === "step" && typeof value.stepId === "string" && typeof value.attempt === "number")
35
+ return { kind: "step", stepId: value.stepId, attempt: value.attempt };
36
+ const { auxId, streamKind, auxKind } = value;
37
+ if (value.kind !== "aux" || typeof auxId !== "string" || typeof streamKind !== "string")
38
+ return undefined;
39
+ if (auxKind !== "stream" && auxKind !== "warm" && auxKind !== "compact-endpoint")
40
+ return undefined;
41
+ return { kind: "aux", auxId, streamKind, auxKind };
42
+ }
43
+ /** The ledger's own result JSON; the shape check guards against a truncated or foreign document. */
44
+ function isCallResult(value) {
45
+ if (typeof value !== "object" || value === null || !("kind" in value))
46
+ return false;
47
+ if (value.kind === "message")
48
+ return "chunks" in value && typeof value.chunks === "number" && "stopReason" in value && "usage" in value;
49
+ return value.kind === "error" && "code" in value && isErrorCode(value.code) && "message" in value;
50
+ }
51
+ /** The stored message text is the ledger's own JSON; the shape check guards against a truncated or foreign chunk set. */
52
+ export function isStoredMessage(value) {
53
+ if (typeof value !== "object" || value === null || !("role" in value) || value.role !== "assistant")
54
+ return false;
55
+ return "content" in value && Array.isArray(value.content) && "stopReason" in value && "usage" in value;
56
+ }
57
+ export function encodeCallRow(row) {
58
+ const { deviceId, result, ...fields } = row;
59
+ return {
60
+ ...fields,
61
+ ...(deviceId === undefined ? {} : { deviceId }),
62
+ ...(result === undefined ? {} : { result: JSON.stringify(result) }),
63
+ };
64
+ }
65
+ export function decodeCallRow(value) {
66
+ if (!isObject(value))
67
+ return undefined;
68
+ const { callId, sessionId, route, deviceId, providerId, modelId, epoch, capability, hooksApplied, state } = value;
69
+ const { requestChunks, result } = value;
70
+ const call = decodeCall(value.call);
71
+ if (typeof callId !== "string" || typeof sessionId !== "string" || !call)
72
+ return undefined;
73
+ if ((route !== "sealed" && route !== "device") || typeof providerId !== "string" || typeof modelId !== "string")
74
+ return undefined;
75
+ if (typeof epoch !== "number" || typeof hooksApplied !== "boolean" || typeof requestChunks !== "number")
76
+ return undefined;
77
+ if ((capability !== "callback-stream" && capability !== "stream") || !isState(state))
78
+ return undefined;
79
+ const parsed = typeof result === "string" ? JSON.parse(result) : undefined;
80
+ if (result !== undefined && !isCallResult(parsed))
81
+ return undefined;
82
+ return {
83
+ callId,
84
+ sessionId,
85
+ call,
86
+ route,
87
+ providerId,
88
+ modelId,
89
+ epoch,
90
+ capability,
91
+ hooksApplied,
92
+ state,
93
+ requestChunks,
94
+ ...(typeof deviceId === "string" ? { deviceId } : {}),
95
+ ...(isCallResult(parsed) ? { result: parsed } : {}),
96
+ };
97
+ }
98
+ export function decodeVariant(value) {
99
+ if (!isObject(value))
100
+ return undefined;
101
+ const { headDigest, liveAtMismatch, mismatches, stepId } = value;
102
+ if (typeof headDigest !== "string" || typeof mismatches !== "number" || typeof stepId !== "string")
103
+ return undefined;
104
+ return {
105
+ headDigest,
106
+ liveAtMismatch: typeof liveAtMismatch === "string" ? liveAtMismatch : undefined,
107
+ mismatches,
108
+ stepId,
109
+ };
110
+ }
111
+ export const encodeVariant = (variant) => ({
112
+ headDigest: variant.headDigest,
113
+ liveAtMismatch: variant.liveAtMismatch ?? null,
114
+ mismatches: variant.mismatches,
115
+ stepId: variant.stepId,
116
+ });
117
+ //# sourceMappingURL=ledger-schema.js.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The brain's `egress_call` ledger (row CK9; custody design 4.5). Every write is one admitted mutation supplied by the
3
+ * host wrapper, which fences it (epoch, admission seq, instance generation) and commits it atomically; the ledger only
4
+ * builds the writes. W4 (row + request chunks) precedes any dial, W4' (`hooked`) replaces the request and sets
5
+ * `hooksApplied` in one mutation, `ended` (row + the assembled message in result chunks) is durable before the terminal
6
+ * event, and the step commit (W9) closes the row and deletes its chunks with `closeCallWrites` inside its own mutation.
7
+ * A cancelled step (W14) is never dialed again, so W14 deletes the live row's chunks too: nothing else would.
8
+ */
9
+ import type { DeviceId, ProviderId } from "@earendil-works/pi-agent-core";
10
+ import type { DurableStore, StoreWrite } from "@earendil-works/pi-agent-core/conformance";
11
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
12
+ import { type EgressCallEnd, type EgressCallRow, type VariantRecord } from "./ledger-schema.ts";
13
+ /** Runs `build` inside the host's admission slot and commits the writes it returns as one fenced, atomic mutation. */
14
+ export type AdmittedMutation = (build: () => Promise<readonly StoreWrite[]>) => Promise<void>;
15
+ export type NewCallRow = Omit<EgressCallRow, "state" | "requestChunks" | "result">;
16
+ export interface VariantMismatch {
17
+ readonly providerId: ProviderId;
18
+ readonly deviceId: DeviceId;
19
+ readonly stepId: string;
20
+ readonly headDigest: string;
21
+ readonly liveAtMismatch: string | undefined;
22
+ }
23
+ export interface EgressLedger {
24
+ /** Every row of the step, oldest attempt first. */
25
+ rowsForStep(stepId: string): Promise<EgressCallRow[]>;
26
+ /** The stored request text, byte-identical to what was admitted (or what `hooked` replaced it with). */
27
+ readRequest(row: EgressCallRow): Promise<string>;
28
+ /** The assembled message of an `ended` row whose result is a message. */
29
+ readMessage(row: EgressCallRow): Promise<AssistantMessage>;
30
+ /** W4. With `previous`, the previous row is retired (`ended` -> `closed`) and its request chunks deleted, atomically. */
31
+ dispatch(next: NewCallRow, requestJson: string, previous?: EgressCallRow): Promise<EgressCallRow>;
32
+ /** W4': the post-hook request replaces the stored one and `hooksApplied` becomes true, in one mutation. */
33
+ recordHooked(row: EgressCallRow, requestJson: string): Promise<EgressCallRow>;
34
+ end(row: EgressCallRow, result: EgressCallEnd): Promise<EgressCallRow>;
35
+ /**
36
+ * The hand refused `row`'s start, so nothing ran: the row and its chunks are replaced by the same call under a new
37
+ * `callId` (same attempt, same stored request) in one mutation. A duplicate `start{callId}` is a result query (4.3),
38
+ * so the refused callId can never be started again.
39
+ */
40
+ replaceRefused(row: EgressCallRow, callId: string): Promise<EgressCallRow>;
41
+ /** The row is superseded; a definition mismatch records the device variant in the same mutation. */
42
+ supersede(row: EgressCallRow, variant?: VariantMismatch): Promise<EgressCallRow>;
43
+ /** W14: the step is cancelled (and its live row with it); a cancelled step is never dialed again. */
44
+ cancelStep(stepId: string, row: EgressCallRow | undefined): Promise<void>;
45
+ isCancelled(stepId: string): Promise<boolean>;
46
+ recordVariant(variant: VariantMismatch): Promise<void>;
47
+ variants(providerId: ProviderId): Promise<ReadonlyMap<DeviceId, VariantRecord>>;
48
+ }
49
+ /** The W9 part the ledger owns: the step's row becomes `closed` and its request chunks are deleted (4.5 retention). */
50
+ export declare function closeCallWrites(row: EgressCallRow): StoreWrite[];
51
+ export declare function createEgressLedger(options: {
52
+ readonly store: Pick<DurableStore, "getDocument" | "scanDocuments">;
53
+ readonly mutate: AdmittedMutation;
54
+ }): EgressLedger;
55
+ //# sourceMappingURL=ledger.d.ts.map