@code-yeongyu/senpi 2026.11.6-2 → 2026.11.9-2
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/CHANGELOG.md +64 -0
- package/dist/bundle/chunks/{bun-extension-importer-RCGPQTI2.js → bun-extension-importer-GQ6IKEXD.js} +2 -2
- package/dist/bundle/chunks/{chunk-RQ6CHXQ2.js → chunk-3TZQXZKH.js} +1 -1
- package/dist/bundle/chunks/{chunk-QN2BHP3M.js → chunk-44BJPS5I.js} +1 -1
- package/dist/bundle/chunks/{chunk-WGRKM3N6.js → chunk-45MVD7BU.js} +1 -1
- package/dist/bundle/chunks/chunk-4JIBEZZE.js +106 -0
- package/dist/bundle/chunks/{chunk-CF5OGBC4.js → chunk-4NYNMAYF.js} +1 -1
- package/dist/bundle/chunks/{chunk-X5ZVYNOL.js → chunk-4UR6UFBH.js} +1 -1
- package/dist/bundle/chunks/{chunk-LNPAT22L.js → chunk-527Q2UYM.js} +9 -8
- package/dist/bundle/chunks/{chunk-HLM46XEB.js → chunk-622VYTRI.js} +1 -1
- package/dist/bundle/chunks/{chunk-D5MRJWUK.js → chunk-7CFORZPE.js} +221 -229
- package/dist/bundle/chunks/{chunk-GKAYPJ4E.js → chunk-7PROV6BB.js} +1 -1
- package/dist/bundle/chunks/{chunk-4E7GDG4H.js → chunk-7VMEE62L.js} +1 -1
- package/dist/bundle/chunks/{chunk-C7OKUC7L.js → chunk-K3UHGDUQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-7III44PV.js → chunk-LHSWAJJL.js} +1 -1
- package/dist/bundle/chunks/{chunk-47PPZKPO.js → chunk-LM6DOWQH.js} +4 -4
- package/dist/bundle/chunks/chunk-LPYBKMPT.js +2 -0
- package/dist/bundle/chunks/{chunk-2FDW2LQL.js → chunk-M7GF73FO.js} +2 -2
- package/dist/bundle/chunks/{chunk-ECW7VBMB.js → chunk-NDSWTAYJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-GSKCWXXG.js → chunk-Q6NR6644.js} +1 -1
- package/dist/bundle/chunks/{chunk-TIPKAMSS.js → chunk-SGAFIMAU.js} +1 -1
- package/dist/bundle/chunks/{chunk-7PSBD5KS.js → chunk-STGOYIXC.js} +1 -1
- package/dist/bundle/chunks/{chunk-DSWDJ7HT.js → chunk-WQ4RBPMS.js} +1 -1
- package/dist/bundle/chunks/chunk-XZDCMDUH.js +2 -0
- package/dist/bundle/chunks/{chunk-GNSMNSL7.js → chunk-Y5L4UFMI.js} +2 -2
- package/dist/bundle/chunks/{cli-main-T74YJGIF.js → cli-main-XBEFQW5A.js} +1 -1
- package/dist/bundle/chunks/{dispatch-BAK2YZKD.js → dispatch-VDIYSXQN.js} +1 -1
- package/dist/bundle/chunks/{hand-entry-AYRUGE2A.js → hand-entry-QFEY3JMQ.js} +1 -1
- package/dist/bundle/chunks/{headless-host-ES74ZB6M.js → headless-host-YXUGONR3.js} +1 -1
- package/dist/bundle/chunks/{host-command-YRXZE4TV.js → host-command-VHX2ILLM.js} +1 -1
- package/dist/bundle/chunks/{host-session-command-N7IN7ZDV.js → host-session-command-P3MRW63Z.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-YGL3QHU6.js +2 -0
- package/dist/bundle/chunks/main-5SD4DHMC.js +2 -0
- package/dist/bundle/chunks/{multi-session-host-XSUJDHFZ.js → multi-session-host-PGRHY3JP.js} +20 -17
- package/dist/bundle/chunks/{package-manager-cli-GLUNPQJU.js → package-manager-cli-SADLU7QX.js} +1 -1
- package/dist/bundle/chunks/{role-RRFFHZF5.js → role-4BUU46VW.js} +3 -3
- package/dist/bundle/chunks/{rotation-stream-BN2YK2MC.js → rotation-stream-TODBIHXJ.js} +1 -1
- package/dist/bundle/chunks/rpc-mode-27TC7W6K.js +2 -0
- package/dist/bundle/chunks/{run-attached-tui-W236SEOR.js → run-attached-tui-MXCC5IZR.js} +1 -1
- package/dist/bundle/chunks/{runtime-session-OBXYK4PI.js → runtime-session-VO3X4OE3.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-B2EJTBOE.js → schedule-command-MX56D6UE.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-YIMLHGLG.js → session-control-endpoint-IL6ERD4U.js} +1 -1
- package/dist/bundle/chunks/{session-picker-E6ZONUJB.js → session-picker-6VRC25MJ.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +114 -110
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session-runtime.d.ts +2 -0
- package/dist/core/agent-session-runtime.js +9 -2
- package/dist/core/agent-session-services.d.ts +2 -0
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session.d.ts +32 -3
- package/dist/core/agent-session.js +142 -29
- package/dist/core/credential-pool/rotation-slots.d.ts +3 -3
- package/dist/core/credential-pool/state-store.d.ts +1 -1
- package/dist/core/egress/classify.d.ts +8 -3
- package/dist/core/egress/classify.js +177 -158
- package/dist/core/egress/console-redactor.d.ts +7 -0
- package/dist/core/egress/console-redactor.js +74 -0
- package/dist/core/egress/credentials.d.ts +73 -0
- package/dist/core/egress/credentials.js +20 -0
- package/dist/core/egress/device-custody.d.ts +1 -1
- package/dist/core/egress/device-custody.js +18 -15
- package/dist/core/egress/device-proxy-call.d.ts +59 -0
- package/dist/core/egress/device-proxy-call.js +148 -0
- package/dist/core/egress/device-proxy-lost-calls.d.ts +27 -0
- package/dist/core/egress/device-proxy-lost-calls.js +28 -0
- package/dist/core/egress/device-proxy-ports.d.ts +49 -0
- package/dist/core/egress/device-proxy-ports.js +2 -0
- package/dist/core/egress/device-proxy-request.d.ts +34 -0
- package/dist/core/egress/device-proxy-request.js +69 -0
- package/dist/core/egress/device-proxy-waits.d.ts +40 -0
- package/dist/core/egress/device-proxy-waits.js +67 -0
- package/dist/core/egress/device-proxy.d.ts +3 -0
- package/dist/core/egress/device-proxy.js +214 -0
- package/dist/core/egress/endpoint.js +2 -1
- package/dist/core/egress/entry-verify.d.ts +13 -0
- package/dist/core/egress/entry-verify.js +33 -0
- package/dist/core/egress/hpke.d.ts +63 -0
- package/dist/core/egress/hpke.js +192 -0
- package/dist/core/egress/key-policy-template.d.ts +7 -1
- package/dist/core/egress/ledger-schema.d.ts +80 -0
- package/dist/core/egress/ledger-schema.js +117 -0
- package/dist/core/egress/ledger.d.ts +55 -0
- package/dist/core/egress/ledger.js +159 -0
- package/dist/core/egress/pinned-infra.d.ts +23 -2
- package/dist/core/egress/pinned-infra.js +9 -2
- package/dist/core/egress/placement-claim.d.ts +69 -0
- package/dist/core/egress/placement-claim.js +115 -0
- package/dist/core/egress/pool-state.d.ts +18 -0
- package/dist/core/egress/pool-state.js +5 -0
- package/dist/core/egress/redact.d.ts +10 -0
- package/dist/core/egress/redact.js +134 -0
- package/dist/core/egress/sealed-format.d.ts +101 -0
- package/dist/core/egress/sealed-format.js +174 -0
- package/dist/core/egress/sealed-source.d.ts +35 -0
- package/dist/core/egress/sealed-source.js +140 -0
- package/dist/core/egress/verified-keys.js +1 -1
- package/dist/core/egress/wire.d.ts +228 -0
- package/dist/core/egress/wire.js +49 -0
- package/dist/core/extensions/builtin/ask-user/escape-residue.d.ts +50 -0
- package/dist/core/extensions/builtin/ask-user/escape-residue.js +113 -0
- package/dist/core/extensions/builtin/ask-user/extension.js +7 -1
- package/dist/core/extensions/builtin/ask-user/resume.js +2 -1
- package/dist/core/extensions/builtin/ask-user/tool.d.ts +3 -0
- package/dist/core/extensions/builtin/ask-user/tool.js +8 -0
- package/dist/core/extensions/builtin/goal/index.js +46 -0
- package/dist/core/extensions/builtin/goal/stop-pause.d.ts +1 -0
- package/dist/core/extensions/builtin/goal/stop-pause.js +4 -2
- package/dist/core/extensions/builtin/monitor-state-event.d.ts +26 -0
- package/dist/core/extensions/builtin/monitor-state-event.js +2 -0
- package/dist/core/extensions/builtin/permission-system/index.js +10 -4
- package/dist/core/extensions/builtin/permission-system/preset-registry.d.ts +2 -0
- package/dist/core/extensions/builtin/permission-system/preset-registry.js +11 -0
- package/dist/core/extensions/builtin/permission-system/settings.d.ts +2 -0
- package/dist/core/extensions/builtin/permission-system/settings.js +10 -1
- package/dist/core/extensions/builtin/terminal/background-age.d.ts +25 -0
- package/dist/core/extensions/builtin/terminal/background-age.js +91 -0
- package/dist/core/extensions/builtin/terminal/extension-state.d.ts +1 -1
- package/dist/core/extensions/builtin/terminal/extension-state.js +21 -3
- package/dist/core/extensions/builtin/terminal/notify.d.ts +6 -0
- package/dist/core/extensions/builtin/terminal/notify.js +39 -5
- package/dist/core/extensions/builtin/terminal/session-bundle.d.ts +9 -2
- package/dist/core/extensions/builtin/terminal/session-bundle.js +36 -1
- package/dist/core/extensions/builtin/terminal/settings.d.ts +2 -0
- package/dist/core/extensions/builtin/terminal/settings.js +8 -0
- package/dist/core/extensions/builtin/terminal/tools/background-auto-reap.d.ts +15 -0
- package/dist/core/extensions/builtin/terminal/tools/background-auto-reap.js +87 -0
- package/dist/core/extensions/builtin/terminal/tools/bash.d.ts +3 -1
- package/dist/core/extensions/builtin/terminal/tools/bash.js +44 -15
- package/dist/core/extensions/builtin/terminal/tools/child-placement.d.ts +8 -0
- package/dist/core/extensions/builtin/terminal/tools/child-placement.js +43 -6
- package/dist/core/extensions/builtin/terminal/tools/context.d.ts +14 -2
- package/dist/core/extensions/bun-extension-importer.js +4 -3
- package/dist/core/extensions/bun-extension-load-turns.d.ts +4 -0
- package/dist/core/extensions/bun-extension-load-turns.js +45 -0
- package/dist/core/extensions/bun-extension-registry.d.ts +1 -0
- package/dist/core/extensions/bun-extension-registry.js +19 -1
- package/dist/core/extensions/loader.js +5 -0
- package/dist/core/extensions/session-control-types.d.ts +15 -0
- package/dist/core/extensions/session-control-types.js +11 -0
- package/dist/core/extensions/session-dialog-api.d.ts +5 -0
- package/dist/core/extensions/session-dialog-api.js +13 -0
- package/dist/core/extensions/types.d.ts +14 -1
- package/dist/core/external-admission.d.ts +16 -2
- package/dist/core/external-admission.js +22 -4
- package/dist/core/permission-decision.d.ts +22 -0
- package/dist/core/permission-decision.js +53 -0
- package/dist/core/permission-mode.d.ts +56 -0
- package/dist/core/permission-mode.js +133 -0
- package/dist/core/retry-fallback/controller-types.d.ts +6 -0
- package/dist/core/retry-fallback/controller.d.ts +9 -0
- package/dist/core/retry-fallback/controller.js +34 -3
- package/dist/core/retry-fallback/unusable.d.ts +25 -1
- package/dist/core/retry-fallback/unusable.js +87 -2
- package/dist/core/retry-fallback/usage-limit.d.ts +6 -1
- package/dist/core/retry-fallback/usage-limit.js +12 -4
- package/dist/core/sdk.d.ts +6 -1
- package/dist/core/sdk.js +3 -0
- package/dist/core/session-log.js +1 -1
- package/dist/core/stall-warning.d.ts +41 -0
- package/dist/core/stall-warning.js +72 -0
- package/dist/core/terminal-settings.d.ts +1 -0
- package/dist/core/turn-origin.d.ts +31 -0
- package/dist/core/turn-origin.js +64 -0
- package/dist/modes/interactive/components/remote-delivery-message.d.ts +2 -1
- package/dist/modes/interactive/components/remote-delivery-message.js +13 -5
- package/dist/modes/rpc/admit-external-message-command.d.ts +37 -0
- package/dist/modes/rpc/admit-external-message-command.js +61 -0
- package/dist/modes/rpc/admit-external-message-wire.d.ts +42 -0
- package/dist/modes/rpc/admit-external-message-wire.js +55 -0
- package/dist/modes/rpc/client-input-handler.d.ts +2 -0
- package/dist/modes/rpc/client-input-handler.js +2 -0
- package/dist/modes/rpc/connection-handler.js +30 -5
- package/dist/modes/rpc/connection-roles.d.ts +6 -0
- package/dist/modes/rpc/connection-roles.js +13 -0
- package/dist/modes/rpc/delegated-drive-origin.d.ts +9 -0
- package/dist/modes/rpc/delegated-drive-origin.js +16 -0
- package/dist/modes/rpc/effect-commands.d.ts +7 -1
- package/dist/modes/rpc/host-bare-endpoint-registration.d.ts +7 -0
- package/dist/modes/rpc/host-bare-endpoint-registration.js +35 -0
- package/dist/modes/rpc/host-capabilities.d.ts +2 -0
- package/dist/modes/rpc/host-capabilities.js +3 -0
- package/dist/modes/rpc/host-handover-parked.d.ts +11 -0
- package/dist/modes/rpc/host-handover-parked.js +17 -0
- package/dist/modes/rpc/host-handover-reopen.js +18 -6
- package/dist/modes/rpc/host-handover-resident-writer.d.ts +16 -0
- package/dist/modes/rpc/host-handover-resident-writer.js +57 -0
- package/dist/modes/rpc/host-idle-handover.js +1 -0
- package/dist/modes/rpc/host-warm.d.ts +16 -1
- package/dist/modes/rpc/host-warm.js +33 -6
- package/dist/modes/rpc/method-classes-schema-delivery.d.ts +32 -0
- package/dist/modes/rpc/method-classes-schema-delivery.js +22 -0
- package/dist/modes/rpc/method-classes-schema-session.d.ts +36 -0
- package/dist/modes/rpc/method-classes-schema-session.js +3 -0
- package/dist/modes/rpc/method-classes-schema.d.ts +36 -0
- package/dist/modes/rpc/method-classes.d.ts +2 -0
- package/dist/modes/rpc/method-classes.js +4 -0
- package/dist/modes/rpc/multi-session-host.js +6 -1
- package/dist/modes/rpc/permission-preset-command.d.ts +6 -0
- package/dist/modes/rpc/permission-preset-command.js +27 -0
- package/dist/modes/rpc/rpc-input-validation.js +4 -3
- package/dist/modes/rpc/rpc-mutation-response-types.d.ts +7 -0
- package/dist/modes/rpc/rpc-response-types.d.ts +2 -1
- package/dist/modes/rpc/rpc-session-command-types.d.ts +6 -1
- package/dist/modes/rpc/rpc-session-event-types.d.ts +5 -2
- package/dist/modes/rpc/rpc-session-state-types.d.ts +2 -1
- package/dist/modes/rpc/rpc-session-state.js +2 -0
- package/dist/modes/rpc/session-attachment-commands.js +3 -3
- package/dist/modes/rpc/session-event-writer-connections.js +1 -0
- package/dist/modes/rpc/session-event-writer.d.ts +1 -1
- package/dist/modes/rpc/session-registry-attach.d.ts +0 -9
- package/dist/modes/rpc/session-registry-attach.js +16 -3
- package/dist/modes/rpc/session-registry-types.d.ts +2 -0
- package/dist/modes/rpc/session-registry.d.ts +2 -0
- package/dist/modes/rpc/session-registry.js +16 -3
- package/dist/modes/rpc/session-router-close.js +43 -6
- package/dist/modes/rpc/session-router-context.js +2 -0
- package/dist/modes/rpc/session-router-occupancy.js +2 -11
- package/dist/modes/rpc/session-router-open-profile.js +4 -0
- package/dist/modes/rpc/session-router-open.d.ts +1 -1
- package/dist/modes/rpc/session-router-open.js +6 -17
- package/dist/modes/rpc/session-router-residency.d.ts +24 -0
- package/dist/modes/rpc/session-router-residency.js +90 -0
- package/dist/modes/rpc/session-router-routing.js +7 -1
- package/dist/modes/rpc/session-status.js +4 -1
- package/dist/modes/rpc/session-worker.js +4 -0
- package/dist/modes/rpc/tool-child/terminal-jobs.d.ts +1 -0
- package/dist/modes/rpc/tool-child/terminal-jobs.js +17 -1
- package/dist/modes/rpc/worker-session-registry.js +5 -2
- package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
- package/docs/engine-socket/fixtures/rpc-inbound-samples.json +29 -2
- package/docs/extensions.md +12 -0
- package/docs/rpc.md +129 -12
- package/docs/terminal-tools.md +25 -1
- package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
- package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-C7HEEBFH.js +0 -2
- package/dist/bundle/chunks/chunk-XCMII2VQ.js +0 -98
- package/dist/bundle/chunks/interactive-mode-B5SQDLH7.js +0 -2
- package/dist/bundle/chunks/main-UQFVFCNY.js +0 -2
- package/dist/bundle/chunks/rpc-mode-JYN3E7P7.js +0 -2
|
@@ -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
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { callKey, cancelledKey, decodeCallRow, decodeVariant, encodeCallRow, encodeVariant, isStoredMessage, requestChunkKey, resultChunkKey, splitUtf8, stepIndexKey, stepIndexPrefix, variantKey, variantPrefix, } from "./ledger-schema.js";
|
|
2
|
+
const putRow = (row) => ({ op: "put", key: callKey(row.callId), data: encodeCallRow(row) });
|
|
3
|
+
const deletes = (count, key) => Array.from({ length: count }, (_, index) => ({ op: "delete", key: key(index) }));
|
|
4
|
+
/** Every stored chunk of `row`: its request and, once `ended` with a message, the assembled message. */
|
|
5
|
+
const chunkDeletes = (row) => [
|
|
6
|
+
...deletes(row.requestChunks, (index) => requestChunkKey(row.callId, index)),
|
|
7
|
+
...deletes(row.result?.kind === "message" ? row.result.chunks : 0, (index) => resultChunkKey(row.callId, index)),
|
|
8
|
+
];
|
|
9
|
+
/** A cancelled row keeps its identity and an error result, never pointers to chunks W14 deleted. */
|
|
10
|
+
function cancelledRow(row) {
|
|
11
|
+
const { result, ...fields } = row;
|
|
12
|
+
return { ...fields, state: "cancelled", requestChunks: 0, ...(result?.kind === "error" ? { result } : {}) };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* W14 on the step's live row. A dispatched or ended row becomes `cancelled`; a superseded row (the abort came while its
|
|
16
|
+
* reissue waited: the pacer backoff or the user's choice) stays superseded. Either way its chunks are deleted, because a
|
|
17
|
+
* cancelled step is never dialed again, so no later W4 retires them.
|
|
18
|
+
*/
|
|
19
|
+
function cancelWrites(row) {
|
|
20
|
+
if (row.state === "closed" || row.state === "cancelled")
|
|
21
|
+
return [];
|
|
22
|
+
const kept = row.state === "superseded" ? { ...row, requestChunks: 0 } : cancelledRow(row);
|
|
23
|
+
return [putRow(kept), ...chunkDeletes(row)];
|
|
24
|
+
}
|
|
25
|
+
/** The W9 part the ledger owns: the step's row becomes `closed` and its request chunks are deleted (4.5 retention). */
|
|
26
|
+
export function closeCallWrites(row) {
|
|
27
|
+
return [putRow({ ...row, state: "closed" }), ...chunkDeletes(row)];
|
|
28
|
+
}
|
|
29
|
+
export function createEgressLedger(options) {
|
|
30
|
+
const { store, mutate } = options;
|
|
31
|
+
const withRequest = (row, requestJson) => {
|
|
32
|
+
const chunks = splitUtf8(requestJson);
|
|
33
|
+
const next = { ...row, requestChunks: chunks.length };
|
|
34
|
+
const writes = chunks.map((data, index) => ({
|
|
35
|
+
op: "put",
|
|
36
|
+
key: requestChunkKey(row.callId, index),
|
|
37
|
+
data,
|
|
38
|
+
}));
|
|
39
|
+
return { row: next, writes: [...writes, putRow(next)] };
|
|
40
|
+
};
|
|
41
|
+
const variantWrite = async (variant) => {
|
|
42
|
+
const key = variantKey(variant.providerId, variant.deviceId);
|
|
43
|
+
const prior = decodeVariant(await store.getDocument(key));
|
|
44
|
+
const sameRun = prior !== undefined && prior.headDigest === variant.headDigest && prior.stepId === variant.stepId;
|
|
45
|
+
const mismatches = sameRun ? prior.mismatches + 1 : 1;
|
|
46
|
+
const { headDigest, liveAtMismatch, stepId } = variant;
|
|
47
|
+
return { op: "put", key, data: encodeVariant({ headDigest, liveAtMismatch, mismatches, stepId }) };
|
|
48
|
+
};
|
|
49
|
+
const stepOf = (row) => {
|
|
50
|
+
if (row.call.kind !== "step")
|
|
51
|
+
throw new Error("the device-route ledger indexes loop steps only");
|
|
52
|
+
return row.call;
|
|
53
|
+
};
|
|
54
|
+
const readRow = async (callId) => decodeCallRow(await store.getDocument(callKey(callId)));
|
|
55
|
+
const readChunks = async (row, what, count, key) => {
|
|
56
|
+
const chunks = await Promise.all(Array.from({ length: count }, (_, index) => store.getDocument(key(index))));
|
|
57
|
+
if (!chunks.every((chunk) => typeof chunk === "string"))
|
|
58
|
+
throw new Error(`egress_call ${row.callId}: stored ${what} is incomplete`);
|
|
59
|
+
return chunks.join("");
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
async rowsForStep(stepId) {
|
|
63
|
+
const index = await store.scanDocuments(stepIndexPrefix(stepId));
|
|
64
|
+
const rows = await Promise.all(index.map((entry) => (typeof entry.data === "string" ? readRow(entry.data) : undefined)));
|
|
65
|
+
return rows.filter((row) => row !== undefined);
|
|
66
|
+
},
|
|
67
|
+
readRequest: (row) => readChunks(row, "request", row.requestChunks, (index) => requestChunkKey(row.callId, index)),
|
|
68
|
+
async readMessage(row) {
|
|
69
|
+
if (row.result?.kind !== "message")
|
|
70
|
+
throw new Error(`egress_call ${row.callId} has no stored message`);
|
|
71
|
+
const key = (index) => resultChunkKey(row.callId, index);
|
|
72
|
+
const parsed = JSON.parse(await readChunks(row, "message", row.result.chunks, key));
|
|
73
|
+
if (!isStoredMessage(parsed))
|
|
74
|
+
throw new Error(`egress_call ${row.callId}: stored message is malformed`);
|
|
75
|
+
return parsed;
|
|
76
|
+
},
|
|
77
|
+
async dispatch(next, requestJson, previous) {
|
|
78
|
+
const { stepId, attempt } = stepOf(next);
|
|
79
|
+
const created = withRequest({ ...next, state: "dispatched", requestChunks: 0 }, requestJson);
|
|
80
|
+
const retire = previous
|
|
81
|
+
? [
|
|
82
|
+
putRow({ ...previous, state: previous.state === "ended" ? "closed" : previous.state }),
|
|
83
|
+
...chunkDeletes(previous),
|
|
84
|
+
]
|
|
85
|
+
: [];
|
|
86
|
+
const index = { op: "put", key: stepIndexKey(stepId, attempt), data: next.callId };
|
|
87
|
+
await mutate(async () => [...retire, ...created.writes, index]);
|
|
88
|
+
return created.row;
|
|
89
|
+
},
|
|
90
|
+
async recordHooked(row, requestJson) {
|
|
91
|
+
const updated = withRequest({ ...row, hooksApplied: true }, requestJson);
|
|
92
|
+
await mutate(async () => [...chunkDeletes(row), ...updated.writes]);
|
|
93
|
+
return updated.row;
|
|
94
|
+
},
|
|
95
|
+
async end(row, result) {
|
|
96
|
+
if (result.kind === "error") {
|
|
97
|
+
const failed = { ...row, state: "ended", result };
|
|
98
|
+
await mutate(async () => [putRow(failed)]);
|
|
99
|
+
return failed;
|
|
100
|
+
}
|
|
101
|
+
const chunks = splitUtf8(JSON.stringify(result.message));
|
|
102
|
+
const { stopReason, usage } = result.message;
|
|
103
|
+
const ended = {
|
|
104
|
+
...row,
|
|
105
|
+
state: "ended",
|
|
106
|
+
result: { kind: "message", stopReason, usage, chunks: chunks.length },
|
|
107
|
+
};
|
|
108
|
+
const writes = chunks.map((data, index) => ({
|
|
109
|
+
op: "put",
|
|
110
|
+
key: resultChunkKey(row.callId, index),
|
|
111
|
+
data,
|
|
112
|
+
}));
|
|
113
|
+
await mutate(async () => [...writes, putRow(ended)]);
|
|
114
|
+
return ended;
|
|
115
|
+
},
|
|
116
|
+
async replaceRefused(row, callId) {
|
|
117
|
+
const { stepId, attempt } = stepOf(row);
|
|
118
|
+
const requestJson = await readChunks(row, "request", row.requestChunks, (i) => requestChunkKey(row.callId, i));
|
|
119
|
+
const created = withRequest({ ...row, callId, state: "dispatched", requestChunks: 0 }, requestJson);
|
|
120
|
+
const index = { op: "put", key: stepIndexKey(stepId, attempt), data: callId };
|
|
121
|
+
const dropped = { op: "delete", key: callKey(row.callId) };
|
|
122
|
+
await mutate(async () => [dropped, ...chunkDeletes(row), ...created.writes, index]);
|
|
123
|
+
return created.row;
|
|
124
|
+
},
|
|
125
|
+
async supersede(row, variant) {
|
|
126
|
+
const superseded = { ...row, state: "superseded" };
|
|
127
|
+
await mutate(async () => [putRow(superseded), ...(variant ? [await variantWrite(variant)] : [])]);
|
|
128
|
+
return superseded;
|
|
129
|
+
},
|
|
130
|
+
async cancelStep(stepId, row) {
|
|
131
|
+
const live = row ? cancelWrites(row) : [];
|
|
132
|
+
await mutate(async () => [{ op: "put", key: cancelledKey(stepId), data: stepId }, ...live]);
|
|
133
|
+
},
|
|
134
|
+
async isCancelled(stepId) {
|
|
135
|
+
return (await store.getDocument(cancelledKey(stepId))) !== undefined;
|
|
136
|
+
},
|
|
137
|
+
async recordVariant(variant) {
|
|
138
|
+
await mutate(async () => [await variantWrite(variant)]);
|
|
139
|
+
},
|
|
140
|
+
async variants(providerId) {
|
|
141
|
+
const prefix = variantPrefix(providerId);
|
|
142
|
+
const entries = await store.scanDocuments(prefix);
|
|
143
|
+
const variants = new Map();
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
const stored = decodeVariant(entry.data);
|
|
146
|
+
if (!stored)
|
|
147
|
+
continue;
|
|
148
|
+
const { headDigest, liveAtMismatch, mismatches } = stored;
|
|
149
|
+
variants.set(decodeURIComponent(entry.key.slice(prefix.length)), {
|
|
150
|
+
headDigest,
|
|
151
|
+
liveAtMismatch,
|
|
152
|
+
mismatches,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return variants;
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=ledger.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type KeyFactsAnswer } from "./key-facts-client.ts";
|
|
2
|
-
import { type CustodyRoleArns, type KeyPolicyTemplate } from "./key-policy-template.ts";
|
|
2
|
+
import { type CustodyRoleArns, type KeyPolicyKind, type KeyPolicyTemplate } from "./key-policy-template.ts";
|
|
3
3
|
import type { ServiceKeyEntry } from "./roster-entries.ts";
|
|
4
4
|
import type { VerifierKeyPin } from "./signed-statement.ts";
|
|
5
5
|
export type CustodyRole = keyof CustodyRoleArns;
|
|
@@ -14,7 +14,28 @@ export interface PinnedCustodyInfra {
|
|
|
14
14
|
/** The verifier signing keys this release trusts (three consecutive generations once minted, 15.15 M-2). */
|
|
15
15
|
readonly keyFactsVerifierKeys: readonly VerifierKeyPin[];
|
|
16
16
|
}
|
|
17
|
-
export declare const PINNED_CUSTODY_INFRA:
|
|
17
|
+
export declare const PINNED_CUSTODY_INFRA: {
|
|
18
|
+
readonly custodyAccountIds: readonly [];
|
|
19
|
+
readonly arnNamespace: "arn:aws:kms:us-east-1:";
|
|
20
|
+
readonly roleNames: {
|
|
21
|
+
readonly runtime: readonly ["custody-runtime"];
|
|
22
|
+
readonly registrySigner: readonly ["registry-signer"];
|
|
23
|
+
readonly provisioning: readonly ["custody-provisioning"];
|
|
24
|
+
readonly reader: readonly ["key-facts-reader"];
|
|
25
|
+
readonly breakglass: readonly ["custody-breakglass"];
|
|
26
|
+
};
|
|
27
|
+
readonly templates: {
|
|
28
|
+
readonly 1: {
|
|
29
|
+
readonly render: ({ keyKind, roles }: {
|
|
30
|
+
readonly accountId: string;
|
|
31
|
+
readonly keyKind: KeyPolicyKind;
|
|
32
|
+
readonly roles: CustodyRoleArns;
|
|
33
|
+
}) => string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
readonly keyFactsEndpoint: undefined;
|
|
37
|
+
readonly keyFactsVerifierKeys: readonly [];
|
|
38
|
+
};
|
|
18
39
|
export type ServiceKeyCheckReason = "ok" | "template-unknown" | "facts-unknown" | "facts-stale" | "shape" | "arn" | "arn-namespace" | "account-not-pinned" | "key-usage" | "key-property" | "spki" | "principal-id" | "role-outside-pins" | "policy";
|
|
19
40
|
export interface ServiceKeyCheck {
|
|
20
41
|
readonly result: "ok" | "mismatch" | "unknown";
|
|
@@ -29,7 +29,13 @@ const SERVICE_KEY_NOTICES = {
|
|
|
29
29
|
"principal-id": "the custody key's access policy no longer matches; cloud custody for this key is unavailable",
|
|
30
30
|
};
|
|
31
31
|
const KMS_KEY_ARN = /^(arn:[a-z0-9-]+:kms:[a-z0-9-]+:)(\d{12}):key\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
32
|
-
const ROLE_ORDER = [
|
|
32
|
+
const ROLE_ORDER = [
|
|
33
|
+
"runtime",
|
|
34
|
+
"registrySigner",
|
|
35
|
+
"provisioning",
|
|
36
|
+
"reader",
|
|
37
|
+
"breakglass",
|
|
38
|
+
];
|
|
33
39
|
export function roleArn(accountId, roleName) {
|
|
34
40
|
return `arn:aws:iam::${accountId}:role/${roleName}`;
|
|
35
41
|
}
|
|
@@ -117,7 +123,8 @@ function isFresh(facts, now) {
|
|
|
117
123
|
return now - facts.readAt <= MAX_FACTS_AGE_MS && facts.readAt - now <= MAX_FACTS_FUTURE_SKEW_MS;
|
|
118
124
|
}
|
|
119
125
|
function outcome(result, reason) {
|
|
120
|
-
const
|
|
126
|
+
const notices = SERVICE_KEY_NOTICES;
|
|
127
|
+
const notice = notices[reason];
|
|
121
128
|
return notice === undefined ? { result, reason } : { result, reason, notice };
|
|
122
129
|
}
|
|
123
130
|
/** `facts` maps each of `entry.kmsKeyArns` to the verifier's answer for exactly that ARN. */
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type RosterView, type ServiceSig } from "./roster-entries.ts";
|
|
2
|
+
import { type LocalKeyVerification } from "./verified-keys.ts";
|
|
3
|
+
import type { VerifierRevocation } from "./verifier-revocations.ts";
|
|
4
|
+
export interface PlacementClaim {
|
|
5
|
+
readonly accountId: string;
|
|
6
|
+
readonly placementId: string;
|
|
7
|
+
readonly epoch: number;
|
|
8
|
+
/** Generation of the placement-registry key that signed this claim (4.6). */
|
|
9
|
+
readonly registryGeneration: number;
|
|
10
|
+
readonly issuedAt: number;
|
|
11
|
+
readonly expiresAt: number;
|
|
12
|
+
}
|
|
13
|
+
export interface SignedPlacementClaim {
|
|
14
|
+
readonly claim: PlacementClaim;
|
|
15
|
+
readonly sig: ServiceSig;
|
|
16
|
+
}
|
|
17
|
+
/** A claim may live at most 24 h; the brain renews it at half-life under the same epoch (4.6). */
|
|
18
|
+
export declare const CLAIM_MAX_LIFETIME_MS = 86400000;
|
|
19
|
+
/** Device-side tolerance for a claim's `issuedAt` in the hand's future (15.15 M-1); `MOVE_SKEW_MS` stays server-side. */
|
|
20
|
+
export declare const CLAIM_ISSUE_SKEW_MS = 300000;
|
|
21
|
+
export type ClaimVerdict = "valid" | "invalid" | "malformed" | "expired" | "superseded" | "unknown";
|
|
22
|
+
/** The `provider.call.error` each refusal answers with (4.6, 15.14 M-3, C.P1). */
|
|
23
|
+
export declare const CLAIM_VERDICT_ERRORS: {
|
|
24
|
+
readonly invalid: {
|
|
25
|
+
readonly code: "stale_epoch";
|
|
26
|
+
readonly retryable: false;
|
|
27
|
+
};
|
|
28
|
+
readonly malformed: {
|
|
29
|
+
readonly code: "claim_invalid";
|
|
30
|
+
readonly retryable: false;
|
|
31
|
+
};
|
|
32
|
+
readonly expired: {
|
|
33
|
+
readonly code: "claim_expired";
|
|
34
|
+
readonly retryable: true;
|
|
35
|
+
};
|
|
36
|
+
readonly superseded: {
|
|
37
|
+
readonly code: "registry_key_revoked";
|
|
38
|
+
readonly retryable: true;
|
|
39
|
+
};
|
|
40
|
+
readonly unknown: {
|
|
41
|
+
readonly code: "registry_key_unknown";
|
|
42
|
+
readonly retryable: true;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/** The highest epoch this hand accepted for one (account, registry generation), and the placement that held it. */
|
|
46
|
+
export interface EpochFloor {
|
|
47
|
+
readonly epoch: number;
|
|
48
|
+
readonly placementId: string;
|
|
49
|
+
}
|
|
50
|
+
/** What this hand trusts when it checks a claim; supplied by the hand's executor (CK8), read fresh per check. */
|
|
51
|
+
export interface ClaimTrust {
|
|
52
|
+
readonly accountId: string;
|
|
53
|
+
readonly records: readonly LocalKeyVerification[];
|
|
54
|
+
readonly roster: RosterView;
|
|
55
|
+
readonly revocations: readonly VerifierRevocation[];
|
|
56
|
+
epochFloor(accountId: string, registryGeneration: number): EpochFloor | undefined;
|
|
57
|
+
}
|
|
58
|
+
export interface ClaimClock {
|
|
59
|
+
readonly now: number;
|
|
60
|
+
readonly nowMono: number;
|
|
61
|
+
}
|
|
62
|
+
export interface ClaimVerifier {
|
|
63
|
+
verify(claim: unknown, clock: ClaimClock): Promise<ClaimVerdict>;
|
|
64
|
+
}
|
|
65
|
+
/** `ASCII(domain) || 0x00 || UTF8(canonicalJson(claim))`: the exact bytes the registry key signs (C.P1). */
|
|
66
|
+
export declare function placementClaimSigningInput(claim: PlacementClaim): Uint8Array<ArrayBuffer>;
|
|
67
|
+
/** The claim's verdict for this hand (custody design 4.6, 15.15, 15.16 and the total order of C.P1). */
|
|
68
|
+
export declare function verifyPlacementClaim(value: unknown, trust: ClaimTrust, clock: ClaimClock): Promise<ClaimVerdict>;
|
|
69
|
+
//# sourceMappingURL=placement-claim.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placement claims for hosted model-key custody (row CK7; custody design 2.2, 4.6, 15.15, 15.16, Appendix C.P1).
|
|
3
|
+
*
|
|
4
|
+
* The placement registry signs `{ claim, sig }` for the placement that currently owns a session. A hand executes a
|
|
5
|
+
* brain's request (provider.call.start/result, ToolHost dispatch, hook.model_request, custody.refresh.request) only
|
|
6
|
+
* when `verifyPlacementClaim` answers `valid`. The verdict is total and ordered (C.P1): closed envelope, roster
|
|
7
|
+
* freshness, key selection and local trust, signature/account/epoch/placement, lifetime bounds, expiry. Every input
|
|
8
|
+
* is untrusted; nothing here throws on bad input, and the hand records a new epoch floor only after `valid`.
|
|
9
|
+
*
|
|
10
|
+
* C.P1 checks local supersession before the signature, so a forged claim under a superseded generation also answers
|
|
11
|
+
* `superseded`. The hand executor (CK8) must audit-log every `superseded` verdict with `sig.keyId` and
|
|
12
|
+
* `claim.placementId`, as it does for `malformed`, so such a probe stays visible.
|
|
13
|
+
*/
|
|
14
|
+
import { canonicalJson } from "@earendil-works/pi-agent-core";
|
|
15
|
+
import { verifyEcdsaP256Sha256 } from "./ecdsa-p256.js";
|
|
16
|
+
import { rosterViewAt } from "./roster-entries.js";
|
|
17
|
+
import { hasExactKeys, isRecord } from "./signed-statement.js";
|
|
18
|
+
import { serviceEntryValidity } from "./verified-keys.js";
|
|
19
|
+
/** A claim may live at most 24 h; the brain renews it at half-life under the same epoch (4.6). */
|
|
20
|
+
export const CLAIM_MAX_LIFETIME_MS = 86_400_000;
|
|
21
|
+
/** Device-side tolerance for a claim's `issuedAt` in the hand's future (15.15 M-1); `MOVE_SKEW_MS` stays server-side. */
|
|
22
|
+
export const CLAIM_ISSUE_SKEW_MS = 300_000;
|
|
23
|
+
const PLACEMENT_CLAIM_DOMAIN = "omo-custody/placement-claim/v1";
|
|
24
|
+
/** The `provider.call.error` each refusal answers with (4.6, 15.14 M-3, C.P1). */
|
|
25
|
+
export const CLAIM_VERDICT_ERRORS = {
|
|
26
|
+
invalid: { code: "stale_epoch", retryable: false },
|
|
27
|
+
malformed: { code: "claim_invalid", retryable: false },
|
|
28
|
+
expired: { code: "claim_expired", retryable: true },
|
|
29
|
+
superseded: { code: "registry_key_revoked", retryable: true },
|
|
30
|
+
unknown: { code: "registry_key_unknown", retryable: true },
|
|
31
|
+
};
|
|
32
|
+
/** `ASCII(domain) || 0x00 || UTF8(canonicalJson(claim))`: the exact bytes the registry key signs (C.P1). */
|
|
33
|
+
export function placementClaimSigningInput(claim) {
|
|
34
|
+
const { accountId, placementId, epoch, registryGeneration, issuedAt, expiresAt } = claim;
|
|
35
|
+
const payload = canonicalJson({ accountId, placementId, epoch, registryGeneration, issuedAt, expiresAt });
|
|
36
|
+
const encoder = new TextEncoder();
|
|
37
|
+
return new Uint8Array([...encoder.encode(PLACEMENT_CLAIM_DOMAIN), 0, ...encoder.encode(payload)]);
|
|
38
|
+
}
|
|
39
|
+
const CLAIM_KEYS = ["accountId", "placementId", "epoch", "registryGeneration", "issuedAt", "expiresAt"];
|
|
40
|
+
function closedRecord(value, keys) {
|
|
41
|
+
return isRecord(value) && hasExactKeys(value, keys) ? value : undefined;
|
|
42
|
+
}
|
|
43
|
+
const isText = (value) => typeof value === "string" && value.length > 0;
|
|
44
|
+
/** A nonnegative safe integer, never -0 (C.P1 boundary rules: no fractions, non-finite values or -0 enter `C`). */
|
|
45
|
+
const isCount = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0);
|
|
46
|
+
/** The closed `{ claim, sig }` envelope, or `undefined` for any other shape (C.P1 canonical input). */
|
|
47
|
+
function parseSignedPlacementClaim(value) {
|
|
48
|
+
const envelope = closedRecord(value, ["claim", "sig"]);
|
|
49
|
+
const claim = closedRecord(envelope?.claim, CLAIM_KEYS);
|
|
50
|
+
const sig = closedRecord(envelope?.sig, ["alg", "keyId", "sig"]);
|
|
51
|
+
if (!claim || !sig || sig.alg !== "ECDSA-P256-SHA256" || !isText(sig.keyId) || !isText(sig.sig))
|
|
52
|
+
return undefined;
|
|
53
|
+
const { accountId, placementId, epoch, registryGeneration, issuedAt, expiresAt } = claim;
|
|
54
|
+
if (!isText(accountId) || !isText(placementId))
|
|
55
|
+
return undefined;
|
|
56
|
+
if (!isCount(epoch) || !isCount(registryGeneration) || !isCount(issuedAt) || !isCount(expiresAt))
|
|
57
|
+
return undefined;
|
|
58
|
+
return {
|
|
59
|
+
claim: { accountId, placementId, epoch, registryGeneration, issuedAt, expiresAt },
|
|
60
|
+
sig: { alg: "ECDSA-P256-SHA256", keyId: sig.keyId, sig: sig.sig },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The one registry entry of the claim's generation this hand may verify under. A roster entry of that generation that
|
|
65
|
+
* this device's records supersede is `superseded`; a missing, unchecked or ambiguous key is `unknown` (also when a
|
|
66
|
+
* superseded generation's entry has left the roster); only bad entries left is `invalid`.
|
|
67
|
+
*/
|
|
68
|
+
async function selectRegistryKey(claim, trust, view, clock) {
|
|
69
|
+
const candidates = view.serviceEntries.filter((entry) => entry.kind === "placement-registry" &&
|
|
70
|
+
entry.accountId === trust.accountId &&
|
|
71
|
+
entry.generation === claim.registryGeneration);
|
|
72
|
+
const context = { revocations: trust.revocations, now: clock.now, nowMono: clock.nowMono };
|
|
73
|
+
const validities = await Promise.all(candidates.map((entry) => serviceEntryValidity(entry, trust.accountId, trust.records, view, context)));
|
|
74
|
+
if (validities.includes("superseded"))
|
|
75
|
+
return { verdict: "superseded" };
|
|
76
|
+
const valid = candidates.filter((_, index) => validities[index] === "valid");
|
|
77
|
+
const [first] = valid;
|
|
78
|
+
if (first && valid.every((entry) => entry.sigKeyId === first.sigKeyId && entry.sigPk === first.sigPk))
|
|
79
|
+
return { entry: first };
|
|
80
|
+
const onlyBad = candidates.length > 0 && validities.every((validity) => validity === "invalid");
|
|
81
|
+
return { verdict: onlyBad ? "invalid" : "unknown" };
|
|
82
|
+
}
|
|
83
|
+
function belowFloor(claim, floor) {
|
|
84
|
+
if (!floor)
|
|
85
|
+
return false;
|
|
86
|
+
return claim.epoch < floor.epoch || (claim.epoch === floor.epoch && claim.placementId !== floor.placementId);
|
|
87
|
+
}
|
|
88
|
+
/** The claim's verdict for this hand (custody design 4.6, 15.15, 15.16 and the total order of C.P1). */
|
|
89
|
+
export async function verifyPlacementClaim(value, trust, clock) {
|
|
90
|
+
const signed = parseSignedPlacementClaim(value);
|
|
91
|
+
if (!signed)
|
|
92
|
+
return "malformed";
|
|
93
|
+
const view = rosterViewAt(trust.roster, clock.now, clock.nowMono);
|
|
94
|
+
if (view.state === "unknown")
|
|
95
|
+
return "unknown";
|
|
96
|
+
const { claim, sig } = signed;
|
|
97
|
+
if (claim.accountId !== trust.accountId)
|
|
98
|
+
return "invalid";
|
|
99
|
+
const selection = await selectRegistryKey(claim, trust, view, clock);
|
|
100
|
+
if ("verdict" in selection)
|
|
101
|
+
return selection.verdict;
|
|
102
|
+
if (sig.keyId !== selection.entry.sigKeyId)
|
|
103
|
+
return "invalid";
|
|
104
|
+
if (!(await verifyEcdsaP256Sha256(selection.entry.sigPk, placementClaimSigningInput(claim), sig.sig)))
|
|
105
|
+
return "invalid";
|
|
106
|
+
if (belowFloor(claim, trust.epochFloor(claim.accountId, claim.registryGeneration)))
|
|
107
|
+
return "invalid";
|
|
108
|
+
const effectiveNow = Math.max(clock.now, view.rosterIssuedAt);
|
|
109
|
+
if (claim.expiresAt - claim.issuedAt > CLAIM_MAX_LIFETIME_MS ||
|
|
110
|
+
claim.issuedAt > effectiveNow + CLAIM_ISSUE_SKEW_MS ||
|
|
111
|
+
claim.issuedAt > view.rosterIssuedAt + CLAIM_MAX_LIFETIME_MS)
|
|
112
|
+
return "malformed";
|
|
113
|
+
return claim.expiresAt > effectiveNow ? "valid" : "expired";
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=placement-claim.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CredentialSlotState, StoredSlotRevisions } from "../credential-pool/state-store.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The credential-pool health that rotation reads and writes (senpi#5835): cooldowns, half-open probe leases and
|
|
4
|
+
* the revisions they are keyed by. On one machine it is the file-backed `CredentialSlotRepository`; a hosted brain
|
|
5
|
+
* supplies its own durable state behind the same calls.
|
|
6
|
+
*/
|
|
7
|
+
export interface PoolState {
|
|
8
|
+
listSlots(providerId: string, laneId: string): Promise<Record<string, CredentialSlotState>>;
|
|
9
|
+
/** Atomic read-modify-write of one slot; `stateVersion` increments on every write, undefined removes the slot. */
|
|
10
|
+
mutateSlotState(providerId: string, laneId: string, slotId: string, fn: (current: CredentialSlotState | undefined) => Omit<CredentialSlotState, "stateVersion"> | undefined): Promise<CredentialSlotState | undefined>;
|
|
11
|
+
storedSlotRevisions(providerId: string, slotName: string, material: {
|
|
12
|
+
key?: string;
|
|
13
|
+
access?: string;
|
|
14
|
+
refresh?: string;
|
|
15
|
+
}): Promise<StoredSlotRevisions>;
|
|
16
|
+
envCredentialRevision(envVarName: string, envValue: string): Promise<string>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=pool-state.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const REDACTED = "<redacted>";
|
|
2
|
+
export interface RedactorOptions {
|
|
3
|
+
readonly secrets: readonly string[];
|
|
4
|
+
readonly publicPrefixes: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
export interface Redactor {
|
|
7
|
+
redact(text: string): string;
|
|
8
|
+
}
|
|
9
|
+
export declare function createRedactor(options: RedactorOptions): Redactor;
|
|
10
|
+
//# sourceMappingURL=redact.d.ts.map
|