@corca-org/ceal-host-win32-x64 1.0.18 → 1.0.19
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/bin/ceal-host.exe +0 -0
- package/libexec/ceal-host/claude-hook.mjs +1924 -522
- package/libexec/ceal-host/claude-invocation-authentication.mjs +1806 -330
- package/libexec/ceal-host/claude-producer.mjs +1773 -412
- package/libexec/ceal-host/codex-hook.mjs +1427 -1013
- package/libexec/ceal-host/codex-invocation-authentication.mjs +3071 -515
- package/libexec/ceal-host/codex-producer.mjs +815 -439
- package/libexec/ceal-host/control.mjs +2266 -1095
- package/libexec/ceal-host/relay.mjs +468 -103
- package/package.json +1 -1
- package/share/ceal-host/bundle-manifest.json +12 -12
- package/share/ceal-host/bundle-manifest.sigstore.json +1 -1
|
@@ -109,6 +109,9 @@ var client_wire_contract_default = {
|
|
|
109
109
|
"document"
|
|
110
110
|
],
|
|
111
111
|
optional_fields: [
|
|
112
|
+
"operation_id",
|
|
113
|
+
"visible_output_max_bytes",
|
|
114
|
+
"local_delivery",
|
|
112
115
|
"verified_artifact"
|
|
113
116
|
],
|
|
114
117
|
bare_document_fields: [
|
|
@@ -195,6 +198,7 @@ var CEAL_COMMAND_RESPONSE_FOCUSED_PROMPT_CONTINUATION_DOCUMENT_FIELDS = Object.f
|
|
|
195
198
|
var CEAL_COMMAND_RESPONSE_DISCOVERY_COMMAND = response.discovery_command;
|
|
196
199
|
var CEAL_COMMAND_RESPONSE_DISCOVERY_MANIFEST_SCHEMA_VERSION = response.discovery_manifest_schema_version;
|
|
197
200
|
var CEAL_COMMAND_RESPONSE_DISCOVERY_MANIFEST_DOCUMENT_FIELDS = Object.freeze([...response.discovery_manifest_document_fields]);
|
|
201
|
+
var CEAL_COMMAND_RESPONSE_MAX_BYTES = CEAL_ENCODED_JSON_DOCUMENT_MAX_BYTES;
|
|
198
202
|
|
|
199
203
|
// packages/ceal-client-protocol/src/vocabulary.ts
|
|
200
204
|
function vocabulary(words) {
|
|
@@ -225,6 +229,7 @@ var CEAL_WRITE_RECEIPT_PROVIDER_STATES = vocabulary(["outcome_unknown", "verifie
|
|
|
225
229
|
var CEAL_WRITE_RECEIPT_PROVIDER_READBACKS = vocabulary(["outcome_unknown", "verified", "not_applied"]);
|
|
226
230
|
var CEAL_PROTOCOL_VERSION = "1.4.0";
|
|
227
231
|
var CEAL_OPERATION_ACCESS_SCHEMA_VERSION = "ceal.operation_access.v1";
|
|
232
|
+
var CEAL_GATEWAY_CONTINUATION_CURSOR_PHASES = vocabulary(["scan", "delivery", "terminal"]);
|
|
228
233
|
var CEAL_GATEWAY_ACTIVE_CALL_PHASES = vocabulary([
|
|
229
234
|
"audited_call_pending",
|
|
230
235
|
"continuation_read_pending",
|
|
@@ -248,6 +253,7 @@ var CEAL_GATEWAY_ORGANIZATION_ROUTE_FACT_CALL_KEYS = Object.freeze([
|
|
|
248
253
|
// packages/ceal-client-protocol/src/operation-next-actions.ts
|
|
249
254
|
var CEAL_REPEAT_REQUEST_NEXT_ACTION = "repeat_request";
|
|
250
255
|
var CEAL_INSPECT_RECEIPT_NEXT_ACTION = "inspect_receipt";
|
|
256
|
+
var CEAL_ANSWER_WITH_GAP_NEXT_ACTION = "answer_with_gap";
|
|
251
257
|
var CALLER_RECOVERABLE_NEXT_ACTION_VALUES = [
|
|
252
258
|
"select_did_you_mean",
|
|
253
259
|
"choose_new_output_path",
|
|
@@ -301,10 +307,20 @@ var CALLER_RECOVERABLE_NEXT_ACTION_VALUES = [
|
|
|
301
307
|
"retry_after",
|
|
302
308
|
// The ref names a container of rows (a Notion database or data source), which
|
|
303
309
|
// has no body of its own; the caller searches its rows instead of reading it.
|
|
304
|
-
"search_container_rows"
|
|
310
|
+
"search_container_rows",
|
|
311
|
+
// A settled unknown write names the exact receipt Operation that can inspect
|
|
312
|
+
// its outcome without issuing another provider mutation.
|
|
313
|
+
"write_receipt.get",
|
|
314
|
+
// Command resolution carries the exact required Operation sequence beside
|
|
315
|
+
// this action, so the caller can execute each declared input in order.
|
|
316
|
+
"follow_required_action_sequence",
|
|
317
|
+
// The command needs caller-owned local materialization (for example an
|
|
318
|
+
// @path file) before the Gateway can receive the argument.
|
|
319
|
+
"materialize_local_argument"
|
|
305
320
|
];
|
|
306
321
|
var CEAL_CALLER_RECOVERABLE_NEXT_ACTIONS = Object.freeze(CALLER_RECOVERABLE_NEXT_ACTION_VALUES);
|
|
307
322
|
var NON_REMEDY_NEXT_ACTION_VALUES = [
|
|
323
|
+
CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
308
324
|
// Only an administrator can take the named step -- approving a place for an
|
|
309
325
|
// Operation, granting a withheld capability -- so a non-administrator asks
|
|
310
326
|
// one. The refusal names the administrator step; this word is the ask.
|
|
@@ -352,7 +368,58 @@ var NON_REMEDY_NEXT_ACTION_VALUES = [
|
|
|
352
368
|
"select_exact_approval"
|
|
353
369
|
];
|
|
354
370
|
var CEAL_NON_REMEDY_NEXT_ACTIONS = Object.freeze(NON_REMEDY_NEXT_ACTION_VALUES);
|
|
355
|
-
var
|
|
371
|
+
var CEAL_DECLARED_NEXT_ACTIONS = Object.freeze([...CEAL_CALLER_RECOVERABLE_NEXT_ACTIONS, ...CEAL_NON_REMEDY_NEXT_ACTIONS]);
|
|
372
|
+
var DECLARED = new Set(CEAL_DECLARED_NEXT_ACTIONS);
|
|
373
|
+
function defineCealNonModelNextActions(actions) {
|
|
374
|
+
return actions;
|
|
375
|
+
}
|
|
376
|
+
var NON_MODEL_NEXT_ACTION_VALUES = defineCealNonModelNextActions([
|
|
377
|
+
CEAL_INSPECT_RECEIPT_NEXT_ACTION,
|
|
378
|
+
"await_pending_approval",
|
|
379
|
+
"inspect_gateway",
|
|
380
|
+
"operator_action",
|
|
381
|
+
"none",
|
|
382
|
+
"check_gateway",
|
|
383
|
+
"inspect_context_store",
|
|
384
|
+
"inspect_local_cache",
|
|
385
|
+
"inspect_local_client",
|
|
386
|
+
"inspect_local_command",
|
|
387
|
+
"inspect_local_output",
|
|
388
|
+
"inspect_local_route",
|
|
389
|
+
"inspect_request",
|
|
390
|
+
"reconcile",
|
|
391
|
+
"reconcile_occurrence",
|
|
392
|
+
"retry_after_owner_recovery",
|
|
393
|
+
"select_exact_approval"
|
|
394
|
+
]);
|
|
395
|
+
var NON_MODEL_NEXT_ACTIONS = new Set(NON_MODEL_NEXT_ACTION_VALUES);
|
|
396
|
+
function isCealModelActionableNonRemedyNextAction(action) {
|
|
397
|
+
return !NON_MODEL_NEXT_ACTIONS.has(action);
|
|
398
|
+
}
|
|
399
|
+
var CEAL_MODEL_ACTIONABLE_NEXT_ACTIONS = Object.freeze([
|
|
400
|
+
...CEAL_CALLER_RECOVERABLE_NEXT_ACTIONS,
|
|
401
|
+
...CEAL_NON_REMEDY_NEXT_ACTIONS.filter(isCealModelActionableNonRemedyNextAction)
|
|
402
|
+
]);
|
|
403
|
+
var MODEL_ACTIONABLE = new Set(CEAL_MODEL_ACTIONABLE_NEXT_ACTIONS);
|
|
404
|
+
var NON_MODEL_ACTION_TO_MODEL_ACTION = Object.freeze({
|
|
405
|
+
[CEAL_INSPECT_RECEIPT_NEXT_ACTION]: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
406
|
+
await_pending_approval: "ask_admin",
|
|
407
|
+
inspect_gateway: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
408
|
+
operator_action: "ask_admin",
|
|
409
|
+
none: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
410
|
+
check_gateway: "ask_admin",
|
|
411
|
+
inspect_context_store: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
412
|
+
inspect_local_cache: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
413
|
+
inspect_local_client: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
414
|
+
inspect_local_command: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
415
|
+
inspect_local_output: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
416
|
+
inspect_local_route: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
417
|
+
inspect_request: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
418
|
+
reconcile: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
419
|
+
reconcile_occurrence: "ask_admin",
|
|
420
|
+
retry_after_owner_recovery: "ask_admin",
|
|
421
|
+
select_exact_approval: "ask_admin"
|
|
422
|
+
});
|
|
356
423
|
var RETRY_PROMISE_NEXT_ACTIONS = ["retry", "retry_after"];
|
|
357
424
|
var RETRY_PROMISE = new Set(RETRY_PROMISE_NEXT_ACTIONS);
|
|
358
425
|
var RECOVERABLE = new Set(CEAL_CALLER_RECOVERABLE_NEXT_ACTIONS);
|
|
@@ -409,9 +476,9 @@ var CEAL_OPERATION_APPROVAL_DISPOSITIONS = vocabulary([
|
|
|
409
476
|
var CealProtocolValidationError = class extends Error {
|
|
410
477
|
name = "CealProtocolValidationError";
|
|
411
478
|
code;
|
|
412
|
-
constructor(
|
|
413
|
-
super(
|
|
414
|
-
this.code =
|
|
479
|
+
constructor(code2) {
|
|
480
|
+
super(code2 === "invalid_gateway_request" ? "Ceal Gateway request is invalid." : "Ceal client response is invalid.");
|
|
481
|
+
this.code = code2;
|
|
415
482
|
}
|
|
416
483
|
};
|
|
417
484
|
var SAFE_REF = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
@@ -444,10 +511,10 @@ function assertSafeJsonArray(value, options, depth, count) {
|
|
|
444
511
|
if (value.length > CEAL_WIRE_SAFE_ARRAY_MAX_ITEMS) invalidByContext(options);
|
|
445
512
|
for (const item of value) assertSafeJsonValue(item, options, depth + 1, count);
|
|
446
513
|
}
|
|
447
|
-
function assertSafeJsonRecord(
|
|
448
|
-
const entries = Object.entries(
|
|
514
|
+
function assertSafeJsonRecord(record5, options, depth, count) {
|
|
515
|
+
const entries = Object.entries(record5);
|
|
449
516
|
if (entries.length > CEAL_WIRE_SAFE_ARRAY_MAX_ITEMS) invalidByContext(options);
|
|
450
|
-
const sourceUrlColumn = options.allowHttpsUrl ? compactSourceUrlColumn(
|
|
517
|
+
const sourceUrlColumn = options.allowHttpsUrl ? compactSourceUrlColumn(record5.fields) : null;
|
|
451
518
|
for (const [key, child] of entries) {
|
|
452
519
|
if (key === "credential_material_included" && child !== false) invalidByContext(options);
|
|
453
520
|
if (!isSafeNegativeMaterialAssertion(key, child)) assertSafeJsonKey(key, options);
|
|
@@ -622,21 +689,15 @@ var CEAL_OPERATION_ACCESS_CLASSES = vocabulary(["read", "write"]);
|
|
|
622
689
|
// packages/ceal-client-protocol/src/domain-vocabulary.ts
|
|
623
690
|
var CEAL_CONNECTOR_STATUSES = vocabulary(["active", "disabled", "disconnected"]);
|
|
624
691
|
var CEAL_CONNECTOR_CUSTODIES = vocabulary(["organization", "personal"]);
|
|
625
|
-
var CEAL_PROVIDER_BOUNDARY_KINDS = vocabulary([
|
|
626
|
-
"github_account",
|
|
627
|
-
"github_organization",
|
|
628
|
-
"google_workspace_domain",
|
|
629
|
-
"notion_workspace",
|
|
630
|
-
"slack_workspace"
|
|
631
|
-
]);
|
|
632
|
-
var CEAL_PROVIDER_BOUNDARY_EVIDENCE = vocabulary(["provider_readback", "operator_stated"]);
|
|
633
692
|
var CEAL_ACCESS_LEVELS = vocabulary(["none", "read", "read_write"]);
|
|
634
693
|
var CEAL_PERSONAL_ACCESS_CEILINGS = vocabulary(["disabled", "read", "read_write"]);
|
|
635
694
|
var CEAL_TARGET_REF_PATTERN = "^[^\\p{Cc}]+$";
|
|
636
695
|
var TARGET_REF = new RegExp(CEAL_TARGET_REF_PATTERN, "u");
|
|
637
696
|
var CEAL_TARGET_VISIBILITIES = vocabulary(["public", "private"]);
|
|
638
|
-
var CEAL_PRINCIPAL_KINDS = vocabulary(["human", "
|
|
697
|
+
var CEAL_PRINCIPAL_KINDS = vocabulary(["human", "runtime"]);
|
|
698
|
+
var CEAL_PERSISTED_PRINCIPAL_KINDS = vocabulary(["human", "service", "runtime"]);
|
|
639
699
|
var CEAL_ORGANIZATION_ROLES = vocabulary(["member", "admin"]);
|
|
700
|
+
var CEAL_CALENDAR_PERIODS = vocabulary(["day", "week", "month"]);
|
|
640
701
|
var LABEL_EXPECTATION = `public-safe display text of at most ${CEAL_PUBLIC_DISPLAY_NAME_MAX_BYTES} bytes`;
|
|
641
702
|
|
|
642
703
|
// packages/ceal-client-protocol/src/safe-json-budget.ts
|
|
@@ -812,7 +873,7 @@ var CEAL_SCHEMA_LITERALS = [
|
|
|
812
873
|
"ceal.operation_receipt.v1",
|
|
813
874
|
"ceal.operation_result.v1",
|
|
814
875
|
"ceal.operation_result.compact.v1",
|
|
815
|
-
"ceal.
|
|
876
|
+
"ceal.local_result_file.v1",
|
|
816
877
|
"ceal.local_result_root.v1",
|
|
817
878
|
"ceal.owner_bootstrap_exchange_request.v1",
|
|
818
879
|
"ceal.owner_bootstrap_exchange_result.v1",
|
|
@@ -842,6 +903,9 @@ var CEAL_BOUNDED_SERIES_CATALOG = Object.freeze({
|
|
|
842
903
|
gateway_personal_client_access_audit: (root) => join(root, "access-audit.jsonl")
|
|
843
904
|
});
|
|
844
905
|
|
|
906
|
+
// packages/ceal-client-protocol/src/canonical-http-boundary.ts
|
|
907
|
+
var CEAL_CANONICAL_HTTP_MAX_RESPONSE_BYTES = CEAL_COMMAND_RESPONSE_MAX_BYTES;
|
|
908
|
+
|
|
845
909
|
// packages/ceal-client-protocol/src/gateway-installation-activation.ts
|
|
846
910
|
var CEAL_GATEWAY_INSTALLATION_ACTIVATION_CODE_DATA_CHARACTERS = 32;
|
|
847
911
|
var CEAL_GATEWAY_INSTALLATION_ACTIVATION_CODE_CHECK_CHARACTERS = 8;
|
|
@@ -906,41 +970,6 @@ var CEAL_VERIFIED_ARTIFACT_DESCRIPTOR_JSON_SCHEMA = Object.freeze({
|
|
|
906
970
|
}
|
|
907
971
|
});
|
|
908
972
|
|
|
909
|
-
// packages/ceal-client-protocol/src/provider-operation-vocabulary.ts
|
|
910
|
-
var CEAL_FILE_TYPE_FAMILIES = vocabulary(["all", "image", "pdf"]);
|
|
911
|
-
var CEAL_GITHUB_ISSUE_STATES = vocabulary(["open", "closed", "all"]);
|
|
912
|
-
var CEAL_NOTION_UPDATABLE_BLOCK_TYPES = vocabulary([
|
|
913
|
-
"paragraph",
|
|
914
|
-
"heading_1",
|
|
915
|
-
"heading_2",
|
|
916
|
-
"heading_3",
|
|
917
|
-
"bulleted_list_item",
|
|
918
|
-
"numbered_list_item",
|
|
919
|
-
"to_do",
|
|
920
|
-
"code"
|
|
921
|
-
]);
|
|
922
|
-
var CEAL_NOTION_NEUTRAL_TYPES = vocabulary([
|
|
923
|
-
"text",
|
|
924
|
-
"number",
|
|
925
|
-
"boolean",
|
|
926
|
-
"date",
|
|
927
|
-
"select",
|
|
928
|
-
"multi_select",
|
|
929
|
-
"people",
|
|
930
|
-
"url",
|
|
931
|
-
"email",
|
|
932
|
-
"phone",
|
|
933
|
-
"unsupported"
|
|
934
|
-
]);
|
|
935
|
-
var CEAL_NOTION_IDENTITY_GAP_REASONS = vocabulary([
|
|
936
|
-
"identity_unavailable",
|
|
937
|
-
"identity_stale",
|
|
938
|
-
"identity_ambiguous",
|
|
939
|
-
"identity_revoked",
|
|
940
|
-
"identity_unlinked"
|
|
941
|
-
]);
|
|
942
|
-
var CEAL_NOTION_STRING_VALUE_TYPES = vocabulary(["text", "select", "url", "email", "phone"]);
|
|
943
|
-
|
|
944
973
|
// packages/ceal-client-protocol/src/opaque-reference.ts
|
|
945
974
|
var CEAL_OPAQUE_HANDLE_KINDS = vocabulary(["target", "message", "thread", "artifact", "document", "block", "object"]);
|
|
946
975
|
var CEAL_OPAQUE_REF_KINDS = vocabulary([...CEAL_OPAQUE_HANDLE_KINDS, "result", "operation-receipt", "reply-receipt", "field", "notion-data-source"]);
|
|
@@ -973,6 +1002,7 @@ var CEAL_ACTIVITY_TIMESTAMP_BASES = vocabulary([
|
|
|
973
1002
|
"resource_modified"
|
|
974
1003
|
]);
|
|
975
1004
|
var CEAL_ACTIVITY_CONNECTOR_KINDS = vocabulary(["slack", "github", "google-calendar", "google-drive", "notion"]);
|
|
1005
|
+
var CEAL_ACTIVITY_UNSUPPORTED_EVENT_KIND_CODE = "unsupported_event_kind";
|
|
976
1006
|
var CEAL_ACTIVITY_GAP_CODES = vocabulary([
|
|
977
1007
|
"provider_page_budget",
|
|
978
1008
|
"call_deadline",
|
|
@@ -980,7 +1010,7 @@ var CEAL_ACTIVITY_GAP_CODES = vocabulary([
|
|
|
980
1010
|
"provider_cursor_unavailable",
|
|
981
1011
|
"malformed_provider_page",
|
|
982
1012
|
"visibility_incomplete",
|
|
983
|
-
|
|
1013
|
+
CEAL_ACTIVITY_UNSUPPORTED_EVENT_KIND_CODE,
|
|
984
1014
|
"direct_messages_excluded"
|
|
985
1015
|
]);
|
|
986
1016
|
var CEAL_ACTIVITY_EVENT_KINDS = vocabulary([
|
|
@@ -1045,6 +1075,8 @@ var RESOURCE_ROW_KEY_RULES = Object.freeze({
|
|
|
1045
1075
|
modified_at: { required: false, timestamp: true },
|
|
1046
1076
|
modified_at_local: { required: false, timestamp: true },
|
|
1047
1077
|
viewed_by_me_at: { required: false, timestamp: true },
|
|
1078
|
+
own_ceal_write: { required: false, timestamp: false },
|
|
1079
|
+
owned_by_me: { required: false, timestamp: false },
|
|
1048
1080
|
visibility: { required: false, timestamp: false },
|
|
1049
1081
|
start: { required: false, timestamp: false },
|
|
1050
1082
|
end: { required: false, timestamp: false },
|
|
@@ -1078,6 +1110,15 @@ function cealExcludedTargetCoverageJsonSchema() {
|
|
|
1078
1110
|
};
|
|
1079
1111
|
}
|
|
1080
1112
|
|
|
1113
|
+
// packages/ceal-client-protocol/src/gateway-write-identity.ts
|
|
1114
|
+
var CEAL_GATEWAY_WRITE_IDENTITY_ROLES = Object.freeze(["replay_identity", "lookup_handle", "collision_evidence"]);
|
|
1115
|
+
var CEAL_GATEWAY_WRITE_REQUEST_REF_PATTERN = /^gateway-write-request:[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/u;
|
|
1116
|
+
var CEAL_GATEWAY_WRITE_IDENTITY_FIELDS = Object.freeze({
|
|
1117
|
+
replay_identity: "idempotency_claim_sha256",
|
|
1118
|
+
lookup_handle: "write_request_sha256",
|
|
1119
|
+
collision_evidence: "normalized_mutation_sha256"
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1081
1122
|
// packages/ceal-client-protocol/src/write-safety-finding.ts
|
|
1082
1123
|
var CEAL_WRITE_SAFETY_FINDING_STATUSES = vocabulary(["warning", "unchecked"]);
|
|
1083
1124
|
var CEAL_WRITE_SAFETY_AUDIENCES = vocabulary(["workspace", "person", "members", "unknown"]);
|
|
@@ -1105,7 +1146,11 @@ var CEAL_REFRESH_FAILURE_CODES = vocabulary([
|
|
|
1105
1146
|
"refresh_revoked",
|
|
1106
1147
|
"authority_replaced",
|
|
1107
1148
|
"refresh_recovery_unavailable",
|
|
1108
|
-
"refresh_recovery_expired"
|
|
1149
|
+
"refresh_recovery_expired",
|
|
1150
|
+
// The serving gate answers this while the Gateway is still starting and the
|
|
1151
|
+
// refresh was never attempted; the same attempt stays retryable. It is never
|
|
1152
|
+
// stored by the session owner, only answered at the serving edge.
|
|
1153
|
+
"gateway_starting"
|
|
1109
1154
|
]);
|
|
1110
1155
|
|
|
1111
1156
|
// packages/ceal-client-protocol/src/request-validation-primitives.ts
|
|
@@ -1138,7 +1183,7 @@ var CANCELLATION_STATES = vocabulary(["requested", "cancelled"]);
|
|
|
1138
1183
|
var CEAL_REQUEST_KEYS = Object.freeze(["schema_version", "ceal_request_ref", "created_at", "authority", "prompt_ref", "host_session_ref", "host_turn_ref", "host_source_ref"]);
|
|
1139
1184
|
var CEAL_REQUEST_JSON_SCHEMA = {
|
|
1140
1185
|
type: "object",
|
|
1141
|
-
maxProperties:
|
|
1186
|
+
maxProperties: 11,
|
|
1142
1187
|
additionalProperties: false,
|
|
1143
1188
|
properties: {
|
|
1144
1189
|
schema_version: { type: "string", const: CEAL_REQUEST_SCHEMA_VERSION, maxLength: 32 },
|
|
@@ -1162,7 +1207,8 @@ var CEAL_REQUEST_JSON_SCHEMA = {
|
|
|
1162
1207
|
host_turn_ref: { type: "string", minLength: 1, maxLength: 128, pattern: SAFE_REF.source },
|
|
1163
1208
|
host_input_ref: { type: "string", minLength: 1, maxLength: 128, pattern: SAFE_REF.source },
|
|
1164
1209
|
host_source_ref: { type: "string", minLength: 1, maxLength: 128, pattern: SAFE_REF.source },
|
|
1165
|
-
requester_timezone: { type: "string", minLength: 1, maxLength: 128, description: "IANA timezone observed for the requester on this Request." }
|
|
1210
|
+
requester_timezone: { type: "string", minLength: 1, maxLength: 128, description: "IANA timezone observed for the requester on this Request." },
|
|
1211
|
+
device_ref: { type: "string", minLength: 1, maxLength: 128, pattern: SAFE_REF.source, description: "Device that submitted this Request through its client session; absent for Host turns and older records." }
|
|
1166
1212
|
},
|
|
1167
1213
|
required: CEAL_REQUEST_KEYS
|
|
1168
1214
|
};
|
|
@@ -1221,6 +1267,7 @@ var REQUEST_EXECUTION_PREPARE_REFUSAL_PROPERTIES = {
|
|
|
1221
1267
|
retryable: { type: "boolean" },
|
|
1222
1268
|
source: { type: "string", enum: ["provider", "gateway"] },
|
|
1223
1269
|
retry_after_ms: { type: "integer", minimum: 0, maximum: CEAL_RETRY_AFTER_MAX_MS },
|
|
1270
|
+
write_request_ref: { type: "string", pattern: CEAL_GATEWAY_WRITE_REQUEST_REF_PATTERN.source, maxLength: 128 },
|
|
1224
1271
|
excluded_targets: cealExcludedTargetCoverageJsonSchema(),
|
|
1225
1272
|
repaired_input: {},
|
|
1226
1273
|
unrepaired_pointers: { type: "array", maxItems: 128, items: { type: "string", minLength: 1, maxLength: 256 } },
|
|
@@ -1279,12 +1326,55 @@ function isCealHostInputKind(value) {
|
|
|
1279
1326
|
return typeof value === "string" && CEAL_HOST_INPUT_KINDS.some((kind) => kind === value);
|
|
1280
1327
|
}
|
|
1281
1328
|
var CEAL_HOST_OBSERVATION_REF_PREFIX = "host-observation:";
|
|
1329
|
+
var HOST_OBSERVATION_PRODUCER = /^[a-z][a-z0-9]{0,31}$/u;
|
|
1330
|
+
var HOST_OBSERVATION_DIGEST = /^[a-f0-9]{8,64}$/u;
|
|
1331
|
+
function cealHostObservationRef(producer, digest2) {
|
|
1332
|
+
if (!HOST_OBSERVATION_PRODUCER.test(producer) || !HOST_OBSERVATION_DIGEST.test(digest2)) invalid("Ceal Host observation reference parts are invalid.");
|
|
1333
|
+
return `${CEAL_HOST_OBSERVATION_REF_PREFIX}${producer}-${digest2}`;
|
|
1334
|
+
}
|
|
1335
|
+
var HOST_TOKEN_OBSERVATION_BREAKDOWN_KEYS = vocabulary(["cached_input", "cache_write_input", "reasoning_output"]);
|
|
1336
|
+
function createHostTokenObservationValue(counts, source) {
|
|
1337
|
+
const core = validTokenCore(counts);
|
|
1338
|
+
if (typeof source !== "string" || source.length === 0 || source.length > 256) invalid("Ceal Host token observation source is invalid.");
|
|
1339
|
+
const value = { status: "available", unit: "tokens", input: core.input, output: core.output, total: core.total, source };
|
|
1340
|
+
for (const breakdown of validTokenBreakdowns(counts)) value[breakdown.key] = breakdown.tokens;
|
|
1341
|
+
assertHostObservationValue(value, "token");
|
|
1342
|
+
return value;
|
|
1343
|
+
}
|
|
1344
|
+
function validTokenCore(counts) {
|
|
1345
|
+
if (!isOperationRecord(counts) || !safeCount(counts.input) || !safeCount(counts.output) || !safeCount(counts.total)) invalid("Ceal Host token observation counts are invalid.");
|
|
1346
|
+
if (counts.total !== counts.input + counts.output) invalid("Ceal Host token observation counts are invalid.");
|
|
1347
|
+
return { input: counts.input, output: counts.output, total: counts.total };
|
|
1348
|
+
}
|
|
1349
|
+
function validTokenBreakdowns(counts) {
|
|
1350
|
+
const breakdowns = [
|
|
1351
|
+
{ key: "cached_input", tokens: counts.cached_input },
|
|
1352
|
+
{ key: "cache_write_input", tokens: counts.cache_write_input },
|
|
1353
|
+
{ key: "reasoning_output", tokens: counts.reasoning_output }
|
|
1354
|
+
];
|
|
1355
|
+
const stated = breakdowns.filter((breakdown) => breakdown.tokens !== void 0).map((breakdown) => ({ key: breakdown.key, tokens: Number(breakdown.tokens) }));
|
|
1356
|
+
for (const breakdown of stated) {
|
|
1357
|
+
if (!safeCount(breakdown.tokens)) invalid("Ceal Host token observation counts are invalid.");
|
|
1358
|
+
}
|
|
1359
|
+
const cached = counts.cached_input ?? 0;
|
|
1360
|
+
const created = counts.cache_write_input ?? 0;
|
|
1361
|
+
const reasoning = counts.reasoning_output ?? 0;
|
|
1362
|
+
if (cached + created > counts.input || reasoning > counts.output) invalid("Ceal Host token observation counts are invalid.");
|
|
1363
|
+
return stated;
|
|
1364
|
+
}
|
|
1365
|
+
function hostObservationNotRecorded(reason) {
|
|
1366
|
+
if (typeof reason !== "string" || reason.length === 0 || reason.length > 256) invalid("Ceal Host observation gap reason is invalid.");
|
|
1367
|
+
return { status: "not_recorded", reason };
|
|
1368
|
+
}
|
|
1369
|
+
function safeCount(value) {
|
|
1370
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
1371
|
+
}
|
|
1282
1372
|
function validateCealHostObservationDraft(value) {
|
|
1283
|
-
const
|
|
1284
|
-
requirePrefixedRef(
|
|
1285
|
-
requireClosedValue(
|
|
1286
|
-
requireIsoTimestamp(
|
|
1287
|
-
assertHostObservationValue(
|
|
1373
|
+
const record5 = operationRecord(value, "Ceal Host observation draft is invalid.");
|
|
1374
|
+
requirePrefixedRef(record5.observation_ref, CEAL_HOST_OBSERVATION_REF_PREFIX, "Ceal Host observation reference is invalid.");
|
|
1375
|
+
requireClosedValue(record5.observation_kind, CEAL_HOST_OBSERVATION_KINDS, "Ceal Host observation kind is invalid.");
|
|
1376
|
+
requireIsoTimestamp(record5.observed_at, "Ceal Host observation time is invalid.");
|
|
1377
|
+
assertHostObservationValue(record5.value, record5.observation_kind);
|
|
1288
1378
|
}
|
|
1289
1379
|
var CEAL_REQUEST_MAX_OBSERVATION_VALUE_BYTES = 64 * 1024;
|
|
1290
1380
|
var CEAL_HOST_OBSERVATION_KINDS = vocabulary(["prompt", "session", "turn", "model", "token", "cost", "timing"]);
|
|
@@ -1314,6 +1404,10 @@ function inspectHostKeys(value) {
|
|
|
1314
1404
|
// packages/ceal-client-protocol/src/host-prompt-ref.ts
|
|
1315
1405
|
var CEAL_HOST_PROMPT_DIGEST_PATTERN = "^[a-f0-9]{64}$";
|
|
1316
1406
|
var PROMPT_DIGEST = new RegExp(CEAL_HOST_PROMPT_DIGEST_PATTERN, "u");
|
|
1407
|
+
function hostProducerPromptRef(promptSha256) {
|
|
1408
|
+
if (!PROMPT_DIGEST.test(promptSha256)) throw new TypeError("Canonical Host prompt digest is invalid.");
|
|
1409
|
+
return `ceal-host-prompt:${promptSha256}`;
|
|
1410
|
+
}
|
|
1317
1411
|
|
|
1318
1412
|
// packages/ceal-client-protocol/src/request-effect-approval.ts
|
|
1319
1413
|
var CEAL_EFFECT_APPROVAL_DECISION_CHECK_PHRASES = Object.freeze([
|
|
@@ -1427,6 +1521,55 @@ var CEAL_HOST_MINT_REQUEST_CONTEXT_REFUSAL_REASONS = vocabulary([
|
|
|
1427
1521
|
"host_input_mismatch",
|
|
1428
1522
|
"host_prompt_mismatch"
|
|
1429
1523
|
]);
|
|
1524
|
+
var CEAL_HOST_AUTHENTICATION_CAUSES = vocabulary([
|
|
1525
|
+
"invalid_arguments",
|
|
1526
|
+
"options_invalid",
|
|
1527
|
+
"session_unavailable",
|
|
1528
|
+
"context_unavailable",
|
|
1529
|
+
"context_replaced",
|
|
1530
|
+
"no_binding",
|
|
1531
|
+
"context_stale",
|
|
1532
|
+
"context_expired",
|
|
1533
|
+
"host_source_refused",
|
|
1534
|
+
"host_credential_refused",
|
|
1535
|
+
"gateway_unavailable",
|
|
1536
|
+
"gateway_rate_limited",
|
|
1537
|
+
"gateway_refused",
|
|
1538
|
+
"gateway_response_invalid",
|
|
1539
|
+
"producer_frame_invalid",
|
|
1540
|
+
"relay_permission_denied",
|
|
1541
|
+
"relay_unavailable",
|
|
1542
|
+
"transient_retryable",
|
|
1543
|
+
"mint_failed",
|
|
1544
|
+
"relay_failed",
|
|
1545
|
+
"host_refusal_invalid"
|
|
1546
|
+
]);
|
|
1547
|
+
var CEAL_HOST_AUTHENTICATION_POLICY = Object.freeze({
|
|
1548
|
+
invalid_arguments: Object.freeze({ classification: "host_arguments_invalid", next_action: "answer_with_gap" }),
|
|
1549
|
+
options_invalid: Object.freeze({ classification: "host_configuration_invalid", next_action: "answer_with_gap" }),
|
|
1550
|
+
session_unavailable: Object.freeze({ classification: "host_session_unavailable", next_action: "answer_with_gap" }),
|
|
1551
|
+
context_unavailable: Object.freeze({ classification: "host_context_unavailable", next_action: "refresh_host_invocation_context", remedy: "the Host has no invocation context from the current transcript turn; run `ceal-host install` to repair the Host hook registration, then retry this command on a fresh Host turn" }),
|
|
1552
|
+
context_replaced: Object.freeze({ classification: "host_invocation_context_stale", next_action: "refresh_host_invocation_context" }),
|
|
1553
|
+
no_binding: Object.freeze({ classification: "host_binding_unavailable", next_action: "refresh_host_invocation_context" }),
|
|
1554
|
+
context_stale: Object.freeze({ classification: "host_invocation_context_stale", next_action: "refresh_host_invocation_context" }),
|
|
1555
|
+
context_expired: Object.freeze({ classification: "host_invocation_context_expired", next_action: "refresh_host_invocation_context" }),
|
|
1556
|
+
host_source_refused: Object.freeze({ classification: "host_source_refused", next_action: "answer_with_gap" }),
|
|
1557
|
+
host_credential_refused: Object.freeze({ classification: "host_registration_credential_refused", next_action: "ask_admin" }),
|
|
1558
|
+
gateway_unavailable: Object.freeze({ classification: "host_mint_gateway_unavailable", next_action: "ask_admin" }),
|
|
1559
|
+
gateway_rate_limited: Object.freeze({ classification: "host_mint_gateway_rate_limited", next_action: "answer_with_gap" }),
|
|
1560
|
+
gateway_refused: Object.freeze({ classification: "host_mint_gateway_refused", next_action: "answer_with_gap" }),
|
|
1561
|
+
gateway_response_invalid: Object.freeze({ classification: "host_mint_gateway_response_invalid", next_action: "answer_with_gap" }),
|
|
1562
|
+
producer_frame_invalid: Object.freeze({ classification: "host_producer_frame_invalid", next_action: "ask_admin", remedy: "ask an administrator to inspect and repair the producer frame, then start a fresh Host turn" }),
|
|
1563
|
+
relay_permission_denied: Object.freeze({ classification: "host_relay_permission_denied", next_action: "rerun_outside_agent_sandbox", remedy: "an agent sandbox denied the Host Relay socket; run `ceal` as the only command, with no pipe, redirect, or `&&`, so the Host allow rule runs it outside the sandbox" }),
|
|
1564
|
+
relay_unavailable: Object.freeze({ classification: "host_relay_unavailable", next_action: "answer_with_gap" }),
|
|
1565
|
+
transient_retryable: Object.freeze({ classification: "host_mint_transient_retryable", next_action: "retry" }),
|
|
1566
|
+
mint_failed: Object.freeze({ classification: "host_mint_failed", next_action: "answer_with_gap" }),
|
|
1567
|
+
relay_failed: Object.freeze({ classification: "host_relay_failed", next_action: "answer_with_gap" }),
|
|
1568
|
+
host_refusal_invalid: Object.freeze({ classification: "host_refusal_invalid", next_action: "answer_with_gap" })
|
|
1569
|
+
});
|
|
1570
|
+
function isCealHostAuthenticationCause(value) {
|
|
1571
|
+
return isVocabularyWord(CEAL_HOST_AUTHENTICATION_CAUSES, value);
|
|
1572
|
+
}
|
|
1430
1573
|
function decodeCealHostInvocationAuthenticationRequest(value) {
|
|
1431
1574
|
if (!isCealJsonRecord(value) || !exactKeys(value, ["schema_version", "client_nonce"]) || value.schema_version !== CEAL_HOST_INVOCATION_AUTHENTICATION_REQUEST_SCHEMA || typeof value.client_nonce !== "string" || !CEAL_HOST_CLIENT_NONCE_PATTERN.test(value.client_nonce)) {
|
|
1432
1575
|
throw new TypeError("The Host invocation authentication request is invalid.");
|
|
@@ -1448,11 +1591,37 @@ function exactKeys(value, expected) {
|
|
|
1448
1591
|
return keys.length === expected.length && expected.every((key) => Object.hasOwn(value, key));
|
|
1449
1592
|
}
|
|
1450
1593
|
|
|
1594
|
+
// packages/ceal-client-protocol/src/host-service-diagnostic.ts
|
|
1595
|
+
var CEAL_HOST_SERVICE_DIAGNOSTIC_SCHEMA = "ceal.host_service_diagnostic.v1";
|
|
1596
|
+
var CEAL_HOST_SERVICE_DIAGNOSTIC_PRODUCERS = vocabulary(["claude", "codex"]);
|
|
1597
|
+
var CEAL_HOST_SERVICE_DIAGNOSTIC_EVENTS = vocabulary([
|
|
1598
|
+
"hook_invoked",
|
|
1599
|
+
"registration_written",
|
|
1600
|
+
"hook_failed",
|
|
1601
|
+
"watcher_turn_observed",
|
|
1602
|
+
"current_prompt_registered",
|
|
1603
|
+
"current_prompt_registration_failed",
|
|
1604
|
+
"activation_recorded",
|
|
1605
|
+
"activation_failed",
|
|
1606
|
+
"watcher_scan_failed",
|
|
1607
|
+
"activation_wait_skipped",
|
|
1608
|
+
"activation_wait_engaged",
|
|
1609
|
+
"activation_wait_completed",
|
|
1610
|
+
"activation_wait_exhausted"
|
|
1611
|
+
]);
|
|
1612
|
+
var CEAL_HOST_SERVICE_DIAGNOSTIC_FAILURE_KINDS = vocabulary([
|
|
1613
|
+
"malformed_line",
|
|
1614
|
+
"other",
|
|
1615
|
+
"payload_rejected",
|
|
1616
|
+
"read_error",
|
|
1617
|
+
"size_budget",
|
|
1618
|
+
"symlink"
|
|
1619
|
+
]);
|
|
1620
|
+
|
|
1451
1621
|
// packages/ceal-client-protocol/src/client-result-materialization.ts
|
|
1452
1622
|
var CEAL_UNREAD_REASONS = vocabulary(["blocked", "unavailable", "too_large", "unsupported", "download_failed", "permission_denied"]);
|
|
1453
|
-
var
|
|
1454
|
-
var
|
|
1455
|
-
var FIELDS = /* @__PURE__ */ new Set([...REQUIRED_FIELDS2, "local_result_root", "continue_argv", "model_disclosure"]);
|
|
1623
|
+
var REQUIRED_FIELDS2 = Object.freeze(["schema_version", "operation_id", "path", "sha256"]);
|
|
1624
|
+
var FIELDS = /* @__PURE__ */ new Set([...REQUIRED_FIELDS2, "items_path", "row_count", "result_complete", "coverage", "local_result_root", "model_disclosure", "continue_argv", "how_to_read"]);
|
|
1456
1625
|
|
|
1457
1626
|
// packages/ceal-client-protocol/src/gateway-cache-origin-validation.ts
|
|
1458
1627
|
var CEAL_MAX_CACHE_ORIGIN_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
@@ -1482,14 +1651,6 @@ var CEAL_GATEWAY_ANNOUNCEMENT_POLICY_NON_CLAIMS = vocabulary([
|
|
|
1482
1651
|
"target_specific_scope_not_declared"
|
|
1483
1652
|
]);
|
|
1484
1653
|
|
|
1485
|
-
// packages/ceal-client-protocol/src/gateway-write-identity.ts
|
|
1486
|
-
var CEAL_GATEWAY_WRITE_IDENTITY_ROLES = Object.freeze(["replay_identity", "lookup_handle", "collision_evidence"]);
|
|
1487
|
-
var CEAL_GATEWAY_WRITE_IDENTITY_FIELDS = Object.freeze({
|
|
1488
|
-
replay_identity: "idempotency_claim_sha256",
|
|
1489
|
-
lookup_handle: "write_request_sha256",
|
|
1490
|
-
collision_evidence: "normalized_mutation_sha256"
|
|
1491
|
-
});
|
|
1492
|
-
|
|
1493
1654
|
// packages/ceal-client-protocol/src/gateway-scoped-identity-projection-validation.ts
|
|
1494
1655
|
var PROJECTION_REVISION_KEYS = Object.freeze(["graph_revision", "subject_key_revision", "projection_revision"]);
|
|
1495
1656
|
|
|
@@ -1502,6 +1663,18 @@ var CEAL_SUPPORTED_GATEWAY_PROTOCOL_RANGE = Object.freeze({
|
|
|
1502
1663
|
// packages/ceal-client-protocol/src/admin-projection-vocabulary.ts
|
|
1503
1664
|
var ADMIN_PROJECTION_STATES = vocabulary(["ready", "empty", "partial", "unknown"]);
|
|
1504
1665
|
var ADMIN_VIEW_KINDS = vocabulary(["request_detail", "resource_access_timeline", "weekly_usage"]);
|
|
1666
|
+
var ADMIN_RECORD_PROJECTION_KINDS = vocabulary([
|
|
1667
|
+
"request_list",
|
|
1668
|
+
"usage_series",
|
|
1669
|
+
"connector_health",
|
|
1670
|
+
"person_connections",
|
|
1671
|
+
"client_sessions",
|
|
1672
|
+
"knowledge_base",
|
|
1673
|
+
"change_history",
|
|
1674
|
+
"approval_queue",
|
|
1675
|
+
"access_group_list",
|
|
1676
|
+
"capability_catalogue"
|
|
1677
|
+
]);
|
|
1505
1678
|
var ADMIN_GAP_KINDS = vocabulary(["unknown", "unavailable", "partial"]);
|
|
1506
1679
|
var ADMIN_UNAVAILABLE_REASONS = vocabulary([
|
|
1507
1680
|
"not_recorded",
|
|
@@ -1525,7 +1698,11 @@ var ADMIN_SOURCE_OWNERS = vocabulary([
|
|
|
1525
1698
|
"person",
|
|
1526
1699
|
"resource",
|
|
1527
1700
|
"prompt",
|
|
1528
|
-
"usage"
|
|
1701
|
+
"usage",
|
|
1702
|
+
"connector",
|
|
1703
|
+
"session",
|
|
1704
|
+
"knowledge",
|
|
1705
|
+
"policy"
|
|
1529
1706
|
]);
|
|
1530
1707
|
var ADMIN_COMPLETENESS_STATES = vocabulary(["complete", "partial", "unknown"]);
|
|
1531
1708
|
var ADMIN_OPERATION_OUTCOMES = vocabulary(["completed", "failed", "denied", "cancelled", "unknown"]);
|
|
@@ -1554,12 +1731,12 @@ var FAILURE_RESPONSE_KEYS = Object.freeze(["error", "ok", "proof_ref_or_unavaila
|
|
|
1554
1731
|
var MAX_DETAIL_CHARACTERS = 240;
|
|
1555
1732
|
var HOST_OWNED_CODE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+){0,7}$/u;
|
|
1556
1733
|
function hostTerminalFailure(error, component) {
|
|
1557
|
-
const
|
|
1734
|
+
const code2 = errorCode(error);
|
|
1558
1735
|
const detail = failureDetail(error);
|
|
1559
|
-
if (
|
|
1560
|
-
if (
|
|
1561
|
-
if (
|
|
1562
|
-
if (
|
|
1736
|
+
if (code2 === "ENOENT") return { code: "host_dependency_unavailable", message: `A command or file the ${component} Host needs is unavailable: ${detail}`, next_action: "Install the missing dependency or restore the missing file, then rerun this command." };
|
|
1737
|
+
if (code2 === "EACCES" || code2 === "EPERM") return { code: "host_permission_denied", message: `The ${component} Host was refused access to a local path: ${detail}`, next_action: "Repair the ownership or mode of the named path, then rerun this command." };
|
|
1738
|
+
if (code2 === "ENOSPC") return { code: "host_storage_unavailable", message: `The ${component} Host could not write local state: ${detail}`, next_action: "Free local disk space, then rerun this command." };
|
|
1739
|
+
if (code2 !== null && HOST_OWNED_CODE.test(code2)) return { code: code2, message: `The ${component} Host command failed: ${detail}`, next_action: `Resolve the named failure above; run \`ceal-host doctor\` only if it reports installed ${component} Host drift.` };
|
|
1563
1740
|
return { code: "host_failure_unclassified", message: `The ${component} Host command failed for a cause it cannot classify: ${detail}`, next_action: `Run \`ceal-host doctor\`; if it reports no drift then the ${component} Host configuration is not the cause, and the message above is the only evidence of what is.` };
|
|
1564
1741
|
}
|
|
1565
1742
|
var DOCTOR_FLAGS = Object.freeze({
|
|
@@ -1632,6 +1809,9 @@ function hostClientProbeRefusal(probe, subject) {
|
|
|
1632
1809
|
return { code: "ceal_output_unreadable", message: `The ${invocation} command returned output the ${subject.component} Host cannot read.`, next_action: `Verify the installed Ceal client matches the protocol this ${subject.component} Host expects, then run this doctor again.` };
|
|
1633
1810
|
}
|
|
1634
1811
|
|
|
1812
|
+
// host/invocation-authentication-command.ts
|
|
1813
|
+
import { performance } from "node:perf_hooks";
|
|
1814
|
+
|
|
1635
1815
|
// host/relay/src/producer-frame.ts
|
|
1636
1816
|
var HOST_PRODUCER_FRAME_SCHEMA = "ceal.host_producer_frame.v1";
|
|
1637
1817
|
var HOST_PRODUCER_MAX_JSON_BYTES = 262144;
|
|
@@ -1643,11 +1823,20 @@ var FRAME_KINDS = new Set(HOST_PRODUCER_FRAME_KINDS);
|
|
|
1643
1823
|
function isHostProducerFrameKind(value) {
|
|
1644
1824
|
return typeof value === "string" && FRAME_KINDS.has(value);
|
|
1645
1825
|
}
|
|
1826
|
+
var HOST_PRODUCER_FRAME_ERROR_CODES = [
|
|
1827
|
+
"invalid_frame_encoding",
|
|
1828
|
+
"invalid_frame_length",
|
|
1829
|
+
"frame_truncated",
|
|
1830
|
+
"unsupported_frame_schema",
|
|
1831
|
+
"invalid_frame_shape",
|
|
1832
|
+
"invalid_producer_identity",
|
|
1833
|
+
"frame_too_large"
|
|
1834
|
+
];
|
|
1646
1835
|
var HostProducerFrameError = class extends Error {
|
|
1647
1836
|
code;
|
|
1648
|
-
constructor(
|
|
1837
|
+
constructor(code2, message = code2) {
|
|
1649
1838
|
super(message);
|
|
1650
|
-
this.code =
|
|
1839
|
+
this.code = code2;
|
|
1651
1840
|
}
|
|
1652
1841
|
};
|
|
1653
1842
|
function encodeHostProducerFrame(frame) {
|
|
@@ -1677,6 +1866,9 @@ function decodeHostProducerFrame(input) {
|
|
|
1677
1866
|
validateFrame(value);
|
|
1678
1867
|
return value;
|
|
1679
1868
|
}
|
|
1869
|
+
function isHostSafeRef(value) {
|
|
1870
|
+
return safeRef(value);
|
|
1871
|
+
}
|
|
1680
1872
|
function validateFrame(value) {
|
|
1681
1873
|
if (!record(value)) throw new HostProducerFrameError("invalid_frame_shape");
|
|
1682
1874
|
if (value.schema_version !== HOST_PRODUCER_FRAME_SCHEMA) throw new HostProducerFrameError("unsupported_frame_schema", `unsupported_frame_schema: ${refusalFact("schema_version", value.schema_version, HOST_PRODUCER_FRAME_SCHEMA)}`);
|
|
@@ -1764,6 +1956,15 @@ function exactOptional(value, required, optional) {
|
|
|
1764
1956
|
return required.every((key) => keys.includes(key)) && keys.every((key) => required.includes(key) || optional.includes(key));
|
|
1765
1957
|
}
|
|
1766
1958
|
|
|
1959
|
+
// host/relay/src/host-producer-core.ts
|
|
1960
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1961
|
+
import { O_NOFOLLOW, O_RDONLY as O_RDONLY2 } from "node:constants";
|
|
1962
|
+
import { mkdir as mkdir2, open as open2, readFile, rename as rename2, writeFile } from "node:fs/promises";
|
|
1963
|
+
import { join as join2 } from "node:path";
|
|
1964
|
+
|
|
1965
|
+
// host/relay/src/relay-ipc.ts
|
|
1966
|
+
import { createConnection, createServer } from "node:net";
|
|
1967
|
+
|
|
1767
1968
|
// host/relay/src/durable-atomic-replacement.ts
|
|
1768
1969
|
import { O_RDONLY } from "node:constants";
|
|
1769
1970
|
import { lstat, mkdir, open, rename, unlink } from "node:fs/promises";
|
|
@@ -1852,17 +2053,6 @@ function errorCode2(error) {
|
|
|
1852
2053
|
// host/relay/src/relay-request-binding-store.ts
|
|
1853
2054
|
var MAX_STATE_BYTES = 8 * 1024 * 1024;
|
|
1854
2055
|
|
|
1855
|
-
// host/relay/src/relay-quarantine-store.ts
|
|
1856
|
-
var RELAY_QUARANTINE_MAX_BYTES = 16 * 1024 * 1024;
|
|
1857
|
-
|
|
1858
|
-
// host/relay/src/relay-admission-store.ts
|
|
1859
|
-
var DEFAULT_LIMITS = { global_count: 8192, global_bytes: 64 * 1024 * 1024, producer_count: 4096, producer_bytes: 32 * 1024 * 1024 };
|
|
1860
|
-
var RETRY_SECONDS = [1, 2, 4, 8, 16, 32, 60];
|
|
1861
|
-
var MAX_RETRYABLE_ATTEMPTS = RETRY_SECONDS.length + 10;
|
|
1862
|
-
|
|
1863
|
-
// host/relay/src/relay-ipc.ts
|
|
1864
|
-
import { createConnection, createServer } from "node:net";
|
|
1865
|
-
|
|
1866
2056
|
// host/relay/src/current-prompt-custody.ts
|
|
1867
2057
|
var HOST_CURRENT_PROMPT_MAX_BYTES = 256 * 1024;
|
|
1868
2058
|
|
|
@@ -1885,6 +2075,8 @@ var HOST_RELAY_REQUEST_BINDING_KEYS = Object.freeze([
|
|
|
1885
2075
|
var HOST_RELAY_IPC_RESPONSE_SCHEMA = "ceal.host_relay_admission.v1";
|
|
1886
2076
|
var HOST_RELAY_INVOCATION_CONTEXT_QUERY_SCHEMA = "ceal.host_relay_invocation_context_query.v1";
|
|
1887
2077
|
var HOST_RELAY_INVOCATION_CONTEXT_RESULT_SCHEMA = "ceal.host_relay_invocation_context_result.v1";
|
|
2078
|
+
var HOST_RELAY_CURRENT_PROMPT_REGISTER_SCHEMA = "ceal.host_relay_current_prompt_register.v1";
|
|
2079
|
+
var HOST_RELAY_CURRENT_PROMPT_RESULT_SCHEMA = "ceal.host_relay_current_prompt_result.v1";
|
|
1888
2080
|
var HOST_RELAY_IPC_LENGTH_BYTES = Uint32Array.BYTES_PER_ELEMENT;
|
|
1889
2081
|
var HOST_RELAY_ADMISSION_RESPONSE_MAX_BYTES = 4096;
|
|
1890
2082
|
var HOST_RELAY_QUERY_RESPONSE_MAX_BYTES = 8192;
|
|
@@ -1896,6 +2088,8 @@ var HOST_RELAY_INVOCATION_MINT_REFUSAL_REASONS = vocabulary([
|
|
|
1896
2088
|
"gateway_unavailable",
|
|
1897
2089
|
"gateway_refused",
|
|
1898
2090
|
"gateway_response_invalid",
|
|
2091
|
+
"gateway_rate_limited",
|
|
2092
|
+
"producer_frame_invalid",
|
|
1899
2093
|
"deadline",
|
|
1900
2094
|
"cancelled",
|
|
1901
2095
|
"mint_failed",
|
|
@@ -1909,6 +2103,17 @@ var HOST_RELAY_INVOCATION_MINT_REFUSAL_REASONS = vocabulary([
|
|
|
1909
2103
|
"host_input_mismatch",
|
|
1910
2104
|
"host_prompt_mismatch"
|
|
1911
2105
|
]);
|
|
2106
|
+
var HOST_RELAY_DIRECT_WIRE_MINT_REASONS = vocabulary([
|
|
2107
|
+
"context_stale",
|
|
2108
|
+
"context_expired",
|
|
2109
|
+
"host_source_refused",
|
|
2110
|
+
"host_credential_refused",
|
|
2111
|
+
"gateway_unavailable",
|
|
2112
|
+
"gateway_rate_limited",
|
|
2113
|
+
"gateway_refused",
|
|
2114
|
+
"gateway_response_invalid",
|
|
2115
|
+
"producer_frame_invalid"
|
|
2116
|
+
]);
|
|
1912
2117
|
var HOST_RELAY_INVOCATION_MINT_OUTCOMES = vocabulary(["completed", "caller_cancelled", "deadline", "failed", "no_binding"]);
|
|
1913
2118
|
var HOST_RELAY_EXCHANGE_FAILURES = vocabulary([
|
|
1914
2119
|
"permission_denied",
|
|
@@ -1924,9 +2129,9 @@ var HostRelayIpcError = class extends Error {
|
|
|
1924
2129
|
code;
|
|
1925
2130
|
/** Set only when the client-side socket exchange itself failed. */
|
|
1926
2131
|
failure;
|
|
1927
|
-
constructor(
|
|
2132
|
+
constructor(code2, message = code2, failure2) {
|
|
1928
2133
|
super(message);
|
|
1929
|
-
this.code =
|
|
2134
|
+
this.code = code2;
|
|
1930
2135
|
this.failure = failure2;
|
|
1931
2136
|
}
|
|
1932
2137
|
};
|
|
@@ -1968,6 +2173,19 @@ var HostRelayIpcAdmissionPort = class {
|
|
|
1968
2173
|
return { code: response2.code, admitted_through_sequence: response2.admitted_through_sequence, delivered_through_sequence: response2.delivered_through_sequence };
|
|
1969
2174
|
}
|
|
1970
2175
|
};
|
|
2176
|
+
var HostRelayCurrentPromptPort = class {
|
|
2177
|
+
endpoint;
|
|
2178
|
+
timeout_ms;
|
|
2179
|
+
constructor(endpoint, timeoutMs = 5e3) {
|
|
2180
|
+
this.endpoint = endpoint;
|
|
2181
|
+
this.timeout_ms = timeoutMs;
|
|
2182
|
+
}
|
|
2183
|
+
async register(input) {
|
|
2184
|
+
const request2 = { schema_version: HOST_RELAY_CURRENT_PROMPT_REGISTER_SCHEMA, ...input };
|
|
2185
|
+
const response2 = await exchange(this.endpoint, encodeLengthPrefixed(request2), this.timeout_ms, decodeCurrentPromptResponseBuffer);
|
|
2186
|
+
if (response2.status !== "registered") throw new HostRelayIpcError("relay_unavailable", response2.status);
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
1971
2189
|
function submitHostProducerFrame(endpoint, frame, timeoutMs = 5e3) {
|
|
1972
2190
|
return exchange(endpoint, encodeHostProducerFrame(frame), timeoutMs, decodeResponseBuffer);
|
|
1973
2191
|
}
|
|
@@ -2055,17 +2273,26 @@ function decodeInvocationContextResponseBuffer(buffer) {
|
|
|
2055
2273
|
if (!validInvocationContextResponse(value)) throw new HostRelayIpcError("relay_unavailable", "Host Relay invocation context response is invalid.");
|
|
2056
2274
|
return value;
|
|
2057
2275
|
}
|
|
2276
|
+
function decodeCurrentPromptResponseBuffer(buffer) {
|
|
2277
|
+
const value = decodeLengthPrefixedJson(buffer, HOST_RELAY_ADMISSION_RESPONSE_MAX_BYTES);
|
|
2278
|
+
if (value === null) return null;
|
|
2279
|
+
if (!record2(value) || !exactKeys3(value, ["schema_version", "status"]) || value.schema_version !== HOST_RELAY_CURRENT_PROMPT_RESULT_SCHEMA || value.status !== "registered" && value.status !== "current_prompt_unavailable") throw new HostRelayIpcError("relay_unavailable");
|
|
2280
|
+
return { schema_version: HOST_RELAY_CURRENT_PROMPT_RESULT_SCHEMA, status: value.status };
|
|
2281
|
+
}
|
|
2058
2282
|
function encodeInvocationContextQuery(query) {
|
|
2059
2283
|
const value = { schema_version: HOST_RELAY_INVOCATION_CONTEXT_QUERY_SCHEMA, ...query };
|
|
2060
2284
|
if (!validInvocationContextQueryRequest(value)) throw new HostRelayIpcError("invalid_frame_shape");
|
|
2061
2285
|
return encodeLengthPrefixed(value);
|
|
2062
2286
|
}
|
|
2063
2287
|
function validInvocationContextQueryRequest(value) {
|
|
2064
|
-
return validQueryIdentity(value, HOST_RELAY_INVOCATION_CONTEXT_QUERY_SCHEMA) && exactKeys3(value, ["schema_version", "producer_id", "producer_installation_ref", "local_session_ref", "local_turn_ref", "client_nonce"]) && typeof value.client_nonce === "string" && CEAL_HOST_CLIENT_NONCE_PATTERN.test(value.client_nonce);
|
|
2288
|
+
return validQueryIdentity(value, HOST_RELAY_INVOCATION_CONTEXT_QUERY_SCHEMA) && exactKeys3(value, ["schema_version", "producer_id", "producer_installation_ref", "local_session_ref", "local_turn_ref", "client_nonce"], ["visible_output_max_bytes"]) && typeof value.client_nonce === "string" && CEAL_HOST_CLIENT_NONCE_PATTERN.test(value.client_nonce) && (value.visible_output_max_bytes === void 0 || validVisibleOutputBudget(value.visible_output_max_bytes));
|
|
2065
2289
|
}
|
|
2066
2290
|
function validQueryIdentity(value, schema) {
|
|
2067
2291
|
return record2(value) && value.schema_version === schema && (value.producer_id === "codex" || value.producer_id === "claude") && safeRef2(value.producer_installation_ref) && safeRef2(value.local_session_ref) && safeRef2(value.local_turn_ref);
|
|
2068
2292
|
}
|
|
2293
|
+
function validVisibleOutputBudget(value) {
|
|
2294
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= CEAL_CANONICAL_HTTP_MAX_RESPONSE_BYTES;
|
|
2295
|
+
}
|
|
2069
2296
|
function validInvocationContextResponse(value) {
|
|
2070
2297
|
if (!record2(value) || value.schema_version !== HOST_RELAY_INVOCATION_CONTEXT_RESULT_SCHEMA) return false;
|
|
2071
2298
|
if (value.status === "no_binding") return Object.keys(value).length === 2;
|
|
@@ -2097,8 +2324,9 @@ function encodeLengthPrefixed(value) {
|
|
|
2097
2324
|
body.copy(output, HOST_RELAY_IPC_LENGTH_BYTES);
|
|
2098
2325
|
return output;
|
|
2099
2326
|
}
|
|
2100
|
-
function exactKeys3(value,
|
|
2101
|
-
|
|
2327
|
+
function exactKeys3(value, required, optional = []) {
|
|
2328
|
+
const keys = Object.keys(value);
|
|
2329
|
+
return required.every((key) => Object.hasOwn(value, key)) && keys.every((key) => required.includes(key) || optional.includes(key));
|
|
2102
2330
|
}
|
|
2103
2331
|
function safeRef2(value) {
|
|
2104
2332
|
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value);
|
|
@@ -2107,15 +2335,359 @@ function errorCode3(error) {
|
|
|
2107
2335
|
return record2(error) && typeof error.code === "string" ? error.code : null;
|
|
2108
2336
|
}
|
|
2109
2337
|
|
|
2338
|
+
// host/relay/src/host-producer-core.ts
|
|
2339
|
+
var HOST_TRANSCRIPT_CURSOR_REASONS = vocabulary(["transcript_replaced", "transcript_truncated", "transcript_rewritten", "transcript_malformed"]);
|
|
2340
|
+
function isHostTranscriptCursorReason(value) {
|
|
2341
|
+
return typeof value === "string" && HOST_TRANSCRIPT_CURSOR_REASONS.some((reason) => reason === value);
|
|
2342
|
+
}
|
|
2343
|
+
var TRANSCRIPT_MISSING_REASON = "transcript_missing";
|
|
2344
|
+
var HOST_TRANSCRIPT_READ_CHUNK_BYTES = 256 * 1024;
|
|
2345
|
+
async function readTranscriptCompleteChunk(handle, offset, size) {
|
|
2346
|
+
const available = Math.max(0, size - offset);
|
|
2347
|
+
if (available === 0) return "empty";
|
|
2348
|
+
const chunks = [];
|
|
2349
|
+
let position = offset;
|
|
2350
|
+
let total = 0;
|
|
2351
|
+
let lastNewline = -1;
|
|
2352
|
+
while (position < size) {
|
|
2353
|
+
const buffer = Buffer.alloc(Math.min(HOST_TRANSCRIPT_READ_CHUNK_BYTES, size - position));
|
|
2354
|
+
const read = await handle.read(buffer, 0, buffer.length, position);
|
|
2355
|
+
if (read.bytesRead === 0) break;
|
|
2356
|
+
const chunk = buffer.subarray(0, read.bytesRead);
|
|
2357
|
+
chunks.push(chunk);
|
|
2358
|
+
const newline = chunk.lastIndexOf(10);
|
|
2359
|
+
if (newline >= 0) lastNewline = total + newline;
|
|
2360
|
+
total += chunk.length;
|
|
2361
|
+
position += chunk.length;
|
|
2362
|
+
if (lastNewline >= 0) break;
|
|
2363
|
+
}
|
|
2364
|
+
if (lastNewline < 0) return "partial";
|
|
2365
|
+
return Buffer.concat(chunks, total).subarray(0, lastNewline + 1);
|
|
2366
|
+
}
|
|
2367
|
+
async function readTranscriptAnchor(handle, offset, anchorBytes) {
|
|
2368
|
+
const start = Math.max(0, offset - anchorBytes);
|
|
2369
|
+
const anchor = Buffer.alloc(offset - start);
|
|
2370
|
+
if (anchor.length > 0) await handle.read(anchor, 0, anchor.length, start);
|
|
2371
|
+
return { start, sha256: transcriptSha256(anchor) };
|
|
2372
|
+
}
|
|
2373
|
+
async function transcriptAnchorMatches(handle, cursor) {
|
|
2374
|
+
const buffer = Buffer.alloc(cursor.offset - cursor.anchor_start);
|
|
2375
|
+
if (buffer.length > 0) await handle.read(buffer, 0, buffer.length, cursor.anchor_start);
|
|
2376
|
+
return transcriptSha256(buffer) === cursor.anchor_sha256;
|
|
2377
|
+
}
|
|
2378
|
+
function transcriptCursorIdentityReason(cursor, transcriptPath, device, inode) {
|
|
2379
|
+
if (cursor.device !== device || cursor.inode !== inode || cursor.transcript_path !== transcriptPath) return "transcript_replaced";
|
|
2380
|
+
return null;
|
|
2381
|
+
}
|
|
2382
|
+
async function transcriptCursorBoundsReason(handle, cursor, size) {
|
|
2383
|
+
if (size < cursor.offset) return "transcript_truncated";
|
|
2384
|
+
if (cursor.offset > 0 && !await transcriptAnchorMatches(handle, cursor)) return "transcript_rewritten";
|
|
2385
|
+
return null;
|
|
2386
|
+
}
|
|
2387
|
+
function transcriptSha256(value) {
|
|
2388
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
2389
|
+
}
|
|
2390
|
+
function transcriptObjectSha256(value) {
|
|
2391
|
+
return createHash2("sha256").update(JSON.stringify(value), "utf8").digest("hex");
|
|
2392
|
+
}
|
|
2393
|
+
function isTranscriptRecord(value) {
|
|
2394
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2395
|
+
}
|
|
2396
|
+
function isMissingTranscriptError(error) {
|
|
2397
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2398
|
+
}
|
|
2399
|
+
function transcriptTimestamp(now, fail) {
|
|
2400
|
+
const value = now();
|
|
2401
|
+
if (!Number.isFinite(value.getTime())) throw fail();
|
|
2402
|
+
return value.toISOString();
|
|
2403
|
+
}
|
|
2404
|
+
function transcriptExpiration(now, retentionMs, fail) {
|
|
2405
|
+
return new Date(Date.parse(transcriptTimestamp(now, fail)) + retentionMs).toISOString();
|
|
2406
|
+
}
|
|
2407
|
+
function createTranscriptWatcherBase(options, owner) {
|
|
2408
|
+
if (typeof options.outbox_dir !== "string" || options.outbox_dir.length === 0) throw owner.invalid_outbox();
|
|
2409
|
+
return {
|
|
2410
|
+
outboxRetentionMs: options.retention_ms ?? owner.default_retention_ms,
|
|
2411
|
+
now: options.now ?? (() => /* @__PURE__ */ new Date()),
|
|
2412
|
+
log: options.log ?? ((line) => {
|
|
2413
|
+
process.stderr.write(`${line}
|
|
2414
|
+
`);
|
|
2415
|
+
}),
|
|
2416
|
+
producerInstallationRef: options.producer_installation_ref
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
async function scanTranscriptRegistrations(registrations, scanOne) {
|
|
2420
|
+
let records = 0;
|
|
2421
|
+
let enqueued = 0;
|
|
2422
|
+
let quarantined = 0;
|
|
2423
|
+
for (const registration of registrations) {
|
|
2424
|
+
const result = await scanOne(registration);
|
|
2425
|
+
records += result.records;
|
|
2426
|
+
enqueued += result.enqueued;
|
|
2427
|
+
if (result.quarantined) quarantined += 1;
|
|
2428
|
+
}
|
|
2429
|
+
return { registrations: registrations.length, records, enqueued, quarantined };
|
|
2430
|
+
}
|
|
2431
|
+
function formatRetiredTranscriptLog(producerLabel, transcriptPath) {
|
|
2432
|
+
return `${producerLabel} retired a transcript registration: transcript_path=${JSON.stringify(transcriptPath)}, reason=${TRANSCRIPT_MISSING_REASON}.`;
|
|
2433
|
+
}
|
|
2434
|
+
var HostTranscriptWatcher = class {
|
|
2435
|
+
outboxRetentionMs;
|
|
2436
|
+
now;
|
|
2437
|
+
log;
|
|
2438
|
+
producerInstallationRef;
|
|
2439
|
+
clockFailure;
|
|
2440
|
+
constructor(options, owner) {
|
|
2441
|
+
const base = createTranscriptWatcherBase(options, owner);
|
|
2442
|
+
this.outboxRetentionMs = base.outboxRetentionMs;
|
|
2443
|
+
this.now = base.now;
|
|
2444
|
+
this.log = base.log;
|
|
2445
|
+
this.producerInstallationRef = base.producerInstallationRef;
|
|
2446
|
+
this.clockFailure = owner.clock_failure;
|
|
2447
|
+
}
|
|
2448
|
+
async scanOnce() {
|
|
2449
|
+
return scanTranscriptRegistrations(await this.listRegistrations(), (registration) => this.scanRegistration(registration));
|
|
2450
|
+
}
|
|
2451
|
+
async scanRegistration(registration) {
|
|
2452
|
+
return this.openTranscript(registration, (handle) => this.scanOpened(handle, registration), {
|
|
2453
|
+
missing: () => this.retireMissing(registration),
|
|
2454
|
+
failed: (handle, error) => this.scanFailed(handle, registration, error)
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
async openTranscript(registration, scan, handlers) {
|
|
2458
|
+
let handle;
|
|
2459
|
+
try {
|
|
2460
|
+
handle = await open2(this.transcriptPathOf(registration), O_RDONLY2 | O_NOFOLLOW);
|
|
2461
|
+
return await scan(handle);
|
|
2462
|
+
} catch (error) {
|
|
2463
|
+
if (handle === void 0 && isMissingTranscriptError(error)) return handlers.missing();
|
|
2464
|
+
if (hostRelayExchangeFailure(error) !== void 0) throw error;
|
|
2465
|
+
return handlers.failed(handle, error);
|
|
2466
|
+
} finally {
|
|
2467
|
+
await handle?.close();
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* A transcript the Host deleted never comes back, so retrying its
|
|
2472
|
+
* registration once per reconcile interval is a failure with no end.
|
|
2473
|
+
* Retire it at the scan that finds it missing and say so once. Only
|
|
2474
|
+
* ENOENT means gone; a permission or busy failure is about this moment,
|
|
2475
|
+
* not about the file, and keeps the registration for the next scan.
|
|
2476
|
+
*/
|
|
2477
|
+
async retireMissingTranscript(transcriptPath, producerLabel, retire) {
|
|
2478
|
+
await retire();
|
|
2479
|
+
this.log(formatRetiredTranscriptLog(producerLabel, transcriptPath));
|
|
2480
|
+
return { records: 0, enqueued: 0, quarantined: false };
|
|
2481
|
+
}
|
|
2482
|
+
stamp() {
|
|
2483
|
+
return transcriptTimestamp(this.now, this.clockFailure);
|
|
2484
|
+
}
|
|
2485
|
+
expire(retentionMs) {
|
|
2486
|
+
return transcriptExpiration(this.now, retentionMs, this.clockFailure);
|
|
2487
|
+
}
|
|
2488
|
+
sha(value) {
|
|
2489
|
+
return transcriptSha256(value);
|
|
2490
|
+
}
|
|
2491
|
+
objectSha(value) {
|
|
2492
|
+
return transcriptObjectSha256(value);
|
|
2493
|
+
}
|
|
2494
|
+
isRecord(value) {
|
|
2495
|
+
return isTranscriptRecord(value);
|
|
2496
|
+
}
|
|
2497
|
+
readChunk(handle, offset, size) {
|
|
2498
|
+
return readTranscriptCompleteChunk(handle, offset, size);
|
|
2499
|
+
}
|
|
2500
|
+
readAnchor(handle, offset, anchorBytes) {
|
|
2501
|
+
return readTranscriptAnchor(handle, offset, anchorBytes);
|
|
2502
|
+
}
|
|
2503
|
+
cursorIdentityReason(cursor, transcriptPath, device, inode) {
|
|
2504
|
+
return transcriptCursorIdentityReason(cursor, transcriptPath, device, inode);
|
|
2505
|
+
}
|
|
2506
|
+
cursorBoundsReason(handle, cursor, size) {
|
|
2507
|
+
return transcriptCursorBoundsReason(handle, cursor, size);
|
|
2508
|
+
}
|
|
2509
|
+
};
|
|
2510
|
+
var HOST_PRODUCER_STATE_IDENTITY = {
|
|
2511
|
+
codex: { file_name: "codex-producer-state.json", schema: "ceal.codex_producer_state.v1" },
|
|
2512
|
+
claude: { file_name: "claude-producer-state.json", schema: "ceal.claude_producer_state.v1" }
|
|
2513
|
+
};
|
|
2514
|
+
var HostProducerDrainCore = class {
|
|
2515
|
+
host;
|
|
2516
|
+
relay;
|
|
2517
|
+
statePath;
|
|
2518
|
+
stateSchema;
|
|
2519
|
+
installationRef;
|
|
2520
|
+
epoch;
|
|
2521
|
+
now;
|
|
2522
|
+
tail = Promise.resolve();
|
|
2523
|
+
constructor(host, runtime) {
|
|
2524
|
+
if (!isHostSafeRef(runtime.producer_installation_ref)) throw host.fail("configuration", `${host.producer_label} producer installation identity is invalid.`);
|
|
2525
|
+
const epoch = runtime.producer_epoch ?? 1;
|
|
2526
|
+
if (!Number.isSafeInteger(epoch) || epoch <= 0) throw host.fail("configuration", `${host.producer_label} producer epoch is invalid.`);
|
|
2527
|
+
this.host = host;
|
|
2528
|
+
this.relay = runtime.relay;
|
|
2529
|
+
this.statePath = join2(host.outbox_directory, HOST_PRODUCER_STATE_IDENTITY[host.producer_id].file_name);
|
|
2530
|
+
this.stateSchema = HOST_PRODUCER_STATE_IDENTITY[host.producer_id].schema;
|
|
2531
|
+
this.installationRef = runtime.producer_installation_ref;
|
|
2532
|
+
this.epoch = epoch;
|
|
2533
|
+
this.now = runtime.now ?? (() => /* @__PURE__ */ new Date());
|
|
2534
|
+
}
|
|
2535
|
+
drainOutbox() {
|
|
2536
|
+
const run = this.tail.then(() => this.drainLocked());
|
|
2537
|
+
this.tail = run.then(() => void 0, () => void 0);
|
|
2538
|
+
return run;
|
|
2539
|
+
}
|
|
2540
|
+
async drainLocked() {
|
|
2541
|
+
const read = await this.host.listEnvelopes();
|
|
2542
|
+
let admitted = 0;
|
|
2543
|
+
let expired = 0;
|
|
2544
|
+
let retained = 0;
|
|
2545
|
+
for (const envelope of read.entries) {
|
|
2546
|
+
const reserved = await this.hasReservation(this.host.submissionRefOf(envelope));
|
|
2547
|
+
if (!reserved && Date.parse(this.host.expiresAtOf(envelope)) <= this.now().getTime()) {
|
|
2548
|
+
await this.host.removeEnvelope(envelope);
|
|
2549
|
+
expired += 1;
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
const frames = await this.reserve(envelope);
|
|
2553
|
+
let complete = true;
|
|
2554
|
+
for (const frame of frames) {
|
|
2555
|
+
const outcome = await this.relay.admit(frame);
|
|
2556
|
+
if (outcome.code !== "admitted" && outcome.code !== "already_admitted") {
|
|
2557
|
+
complete = false;
|
|
2558
|
+
break;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
if (!complete) {
|
|
2562
|
+
retained += 1;
|
|
2563
|
+
break;
|
|
2564
|
+
}
|
|
2565
|
+
await this.host.removeEnvelope(envelope);
|
|
2566
|
+
await this.release(this.host.submissionRefOf(envelope));
|
|
2567
|
+
admitted += 1;
|
|
2568
|
+
}
|
|
2569
|
+
return { admitted, retained: retained + Math.max(0, read.entries.length - admitted - expired - retained), expired, malformed: read.malformed };
|
|
2570
|
+
}
|
|
2571
|
+
async hasReservation(submissionRef2) {
|
|
2572
|
+
return (await this.readState()).pending[submissionRef2] !== void 0;
|
|
2573
|
+
}
|
|
2574
|
+
async reserve(envelope) {
|
|
2575
|
+
const state = await this.readState();
|
|
2576
|
+
const submissionRef2 = this.host.submissionRefOf(envelope);
|
|
2577
|
+
const existing = state.pending[submissionRef2];
|
|
2578
|
+
if (existing !== void 0) return existing;
|
|
2579
|
+
const drafts = await this.host.mapEnvelope(envelope);
|
|
2580
|
+
const frames = drafts.map((draft, offset) => ({
|
|
2581
|
+
schema_version: "ceal.host_producer_frame.v1",
|
|
2582
|
+
producer_id: this.host.producer_id,
|
|
2583
|
+
producer_installation_ref: this.installationRef,
|
|
2584
|
+
producer_epoch: this.epoch,
|
|
2585
|
+
sequence: state.next_sequence + offset,
|
|
2586
|
+
frame_kind: draft.frame_kind,
|
|
2587
|
+
occurred_at: this.host.createdAtOf(envelope),
|
|
2588
|
+
payload: draft.payload
|
|
2589
|
+
}));
|
|
2590
|
+
await this.writeState({ ...state, next_sequence: state.next_sequence + frames.length, pending: { ...state.pending, [submissionRef2]: frames } });
|
|
2591
|
+
return frames;
|
|
2592
|
+
}
|
|
2593
|
+
async release(submissionRef2) {
|
|
2594
|
+
const state = await this.readState();
|
|
2595
|
+
if (state.pending[submissionRef2] === void 0) return;
|
|
2596
|
+
const pending = { ...state.pending };
|
|
2597
|
+
delete pending[submissionRef2];
|
|
2598
|
+
await this.writeState({ ...state, pending });
|
|
2599
|
+
}
|
|
2600
|
+
async readState() {
|
|
2601
|
+
try {
|
|
2602
|
+
const value = JSON.parse(await readFile(this.statePath, "utf8"));
|
|
2603
|
+
return this.decodeState(value);
|
|
2604
|
+
} catch (error) {
|
|
2605
|
+
if (errorCode4(error) !== "ENOENT") throw error;
|
|
2606
|
+
return { schema_version: this.stateSchema, producer_installation_ref: this.installationRef, producer_epoch: this.epoch, next_sequence: 1, pending: {} };
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
async writeState(state) {
|
|
2610
|
+
await mkdir2(this.host.outbox_directory, { recursive: true, mode: 448 });
|
|
2611
|
+
const temporary = `${this.statePath}.${process.pid}.new`;
|
|
2612
|
+
await writeFile(temporary, JSON.stringify(state), { mode: 384 });
|
|
2613
|
+
await rename2(temporary, this.statePath);
|
|
2614
|
+
}
|
|
2615
|
+
decodeState(value) {
|
|
2616
|
+
const label = this.host.producer_label;
|
|
2617
|
+
const fail = (code2, message) => this.host.fail(code2, message);
|
|
2618
|
+
if (!isDrainStateHeader(value, this.stateSchema, this.installationRef)) {
|
|
2619
|
+
throw fail("storage", `${label} producer state header is malformed or belongs to another installation (${refusalFacts([
|
|
2620
|
+
{ name: "schema_version", value: isTranscriptRecord(value) ? value.schema_version : void 0, expected: this.stateSchema },
|
|
2621
|
+
{ name: "producer_installation_ref", value: isTranscriptRecord(value) ? value.producer_installation_ref : void 0, expected: this.installationRef }
|
|
2622
|
+
])}).`);
|
|
2623
|
+
}
|
|
2624
|
+
if (Number(value.producer_epoch) > this.epoch) throw fail("storage", `${label} producer state epoch is ahead of this installation (${refusalFact("producer_epoch", value.producer_epoch, `at most ${this.epoch}`)}).`);
|
|
2625
|
+
const pending = {};
|
|
2626
|
+
const storedEpoch = Number(value.producer_epoch);
|
|
2627
|
+
for (const [submissionRef2, frames] of Object.entries(value.pending)) {
|
|
2628
|
+
if (!isHostSafeRef(submissionRef2) || !Array.isArray(frames) || frames.length === 0) throw fail("storage", `${label} producer pending reservation is malformed.`);
|
|
2629
|
+
pending[submissionRef2] = frames.map((frame) => this.decodeStoredFrame(frame, storedEpoch));
|
|
2630
|
+
}
|
|
2631
|
+
return { schema_version: this.stateSchema, producer_installation_ref: this.installationRef, producer_epoch: this.epoch, next_sequence: Number(value.next_sequence), pending };
|
|
2632
|
+
}
|
|
2633
|
+
decodeStoredFrame(value, storedEpoch) {
|
|
2634
|
+
const label = this.host.producer_label;
|
|
2635
|
+
const json = Buffer.from(JSON.stringify(value), "utf8");
|
|
2636
|
+
const encoded = Buffer.allocUnsafe(json.length + 4);
|
|
2637
|
+
encoded.writeUInt32BE(json.length, 0);
|
|
2638
|
+
json.copy(encoded, 4);
|
|
2639
|
+
const frame = decodeHostProducerFrame(encoded);
|
|
2640
|
+
if (frame.producer_id !== this.host.producer_id || frame.producer_installation_ref !== this.installationRef || frame.producer_epoch > storedEpoch) throw this.host.fail("storage", `${label} producer pending frame identity is malformed (${refusalFacts([
|
|
2641
|
+
{ name: "producer_id", value: frame.producer_id, expected: this.host.producer_id },
|
|
2642
|
+
{ name: "producer_installation_ref", value: frame.producer_installation_ref, expected: this.installationRef },
|
|
2643
|
+
{ name: "producer_epoch", value: frame.producer_epoch, expected: `at most ${storedEpoch}` }
|
|
2644
|
+
])}).`);
|
|
2645
|
+
return frame;
|
|
2646
|
+
}
|
|
2647
|
+
};
|
|
2648
|
+
function isDrainStateHeader(value, schema, installationRef) {
|
|
2649
|
+
return isTranscriptRecord(value) && value.schema_version === schema && value.producer_installation_ref === installationRef && Number.isSafeInteger(value.producer_epoch) && Number(value.producer_epoch) > 0 && Number.isSafeInteger(value.next_sequence) && Number(value.next_sequence) > 0 && isTranscriptRecord(value.pending);
|
|
2650
|
+
}
|
|
2651
|
+
function errorCode4(error) {
|
|
2652
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : null;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
// host/relay/src/relay-quarantine-store.ts
|
|
2656
|
+
var RELAY_QUARANTINE_MAX_BYTES = 16 * 1024 * 1024;
|
|
2657
|
+
var RELAY_QUARANTINE_FRAME_REASONS = ["frame_identity_conflict", "sequence_gap", "gateway_refused"];
|
|
2658
|
+
var RELAY_QUARANTINE_REASONS = [...RELAY_QUARANTINE_FRAME_REASONS, ...HOST_PRODUCER_FRAME_ERROR_CODES];
|
|
2659
|
+
|
|
2660
|
+
// host/relay/src/relay-admission-store.ts
|
|
2661
|
+
var DEFAULT_LIMITS = { global_count: 8192, global_bytes: 64 * 1024 * 1024, producer_count: 4096, producer_bytes: 32 * 1024 * 1024 };
|
|
2662
|
+
var RETRY_SECONDS = [1, 2, 4, 8, 16, 32, 60];
|
|
2663
|
+
var MAX_RETRYABLE_ATTEMPTS = RETRY_SECONDS.length + 10;
|
|
2664
|
+
|
|
2665
|
+
// host/service-log.ts
|
|
2666
|
+
function writeHostServiceDiagnostic(producer_id, event, failure_kind) {
|
|
2667
|
+
try {
|
|
2668
|
+
process.stderr.write(`${JSON.stringify({ schema_version: CEAL_HOST_SERVICE_DIAGNOSTIC_SCHEMA, producer_id, event, ...event === "watcher_scan_failed" && failure_kind !== void 0 ? { failure_kind } : {} })}
|
|
2669
|
+
`);
|
|
2670
|
+
} catch {
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
var HOST_SERVICE_LOG_MAX_BYTES = 1024 * 1024;
|
|
2674
|
+
var HOST_SERVICE_LOG_TAIL_BYTES = 64 * 1024;
|
|
2675
|
+
|
|
2110
2676
|
// host/invocation-authentication-command.ts
|
|
2111
2677
|
var HOST_INVOCATION_AUTHENTICATION_COMMAND_PREFIX = ["authenticate", "--client-nonce"];
|
|
2112
|
-
var
|
|
2113
|
-
|
|
2678
|
+
var HOST_AUTHENTICATION_BUDGET_ENVIRONMENT = "CEAL_HOST_AUTHENTICATION_BUDGET_MS";
|
|
2679
|
+
var HOST_CONTEXT_ACTIVATION_WAIT_MAX_MS = 2e4;
|
|
2680
|
+
var CLAUDE_CONTEXT_ACTIVATION_WAIT_MAX_MS = HOST_CONTEXT_ACTIVATION_WAIT_MAX_MS;
|
|
2681
|
+
var HOST_AUTHENTICATION_COMPLETION_RESERVE_MS = 5e3;
|
|
2682
|
+
var INVOCATION_CONTEXT_POLL_INTERVAL_MS = 250;
|
|
2683
|
+
var REFUSAL_REASON_PATTERN = /^[a-z][a-z0-9_]{0,63}$/u;
|
|
2684
|
+
function installedHostInvocationAuthenticationCommandOptions(config, contextBudgetMs, maxContextWaitMs) {
|
|
2114
2685
|
const port = new HostRelayInvocationContextPort(config.relay_endpoint);
|
|
2115
2686
|
return {
|
|
2116
2687
|
host_source_ref: config.host_source_ref,
|
|
2117
2688
|
outbox_dir: config.outbox_dir,
|
|
2118
2689
|
producer_installation_ref: config.producer_installation_ref,
|
|
2690
|
+
context_activation_wait_ms: boundedContextActivationWait(contextBudgetMs, config.ipc_timeout_ms, maxContextWaitMs),
|
|
2119
2691
|
mint_invocation_context: (input) => port.mintInvocationContext({ ...input, producer_id: input.producer_id })
|
|
2120
2692
|
};
|
|
2121
2693
|
}
|
|
@@ -2133,65 +2705,150 @@ async function runHostInvocationAuthenticationCommand(descriptor, args, options)
|
|
|
2133
2705
|
}
|
|
2134
2706
|
function authenticationRefusalReason(error) {
|
|
2135
2707
|
const message = error instanceof Error ? error.message : "";
|
|
2136
|
-
|
|
2708
|
+
if (!REFUSAL_REASON_PATTERN.test(message)) return "host_refusal_invalid";
|
|
2709
|
+
return isCealHostAuthenticationCause(message) ? message : "host_refusal_invalid";
|
|
2137
2710
|
}
|
|
2138
2711
|
async function authenticateHostInvocation(descriptor, input, options) {
|
|
2139
|
-
if (input.length !== HOST_INVOCATION_AUTHENTICATION_COMMAND_PREFIX.length + 1 || input[0] !== HOST_INVOCATION_AUTHENTICATION_COMMAND_PREFIX[0] || input[1] !== HOST_INVOCATION_AUTHENTICATION_COMMAND_PREFIX[1]) throw new TypeError("
|
|
2712
|
+
if (input.length !== HOST_INVOCATION_AUTHENTICATION_COMMAND_PREFIX.length + 1 || input[0] !== HOST_INVOCATION_AUTHENTICATION_COMMAND_PREFIX[0] || input[1] !== HOST_INVOCATION_AUTHENTICATION_COMMAND_PREFIX[1]) throw new TypeError("invalid_arguments");
|
|
2140
2713
|
const request2 = decodeCealHostInvocationAuthenticationRequest({ schema_version: CEAL_HOST_INVOCATION_AUTHENTICATION_REQUEST_SCHEMA, client_nonce: input[2] });
|
|
2141
2714
|
const config = validateOptions(options);
|
|
2142
|
-
const now =
|
|
2715
|
+
const now = config.now ?? (() => /* @__PURE__ */ new Date());
|
|
2143
2716
|
const { store, current } = await currentInvocation(descriptor, config, now);
|
|
2144
|
-
if (config.mint_invocation_context === void 0) throw new TypeError("
|
|
2717
|
+
if (config.mint_invocation_context === void 0) throw new TypeError("relay_failed");
|
|
2145
2718
|
const query = {
|
|
2146
2719
|
producer_id: descriptor.producer_id,
|
|
2147
2720
|
producer_installation_ref: config.producer_installation_ref,
|
|
2148
2721
|
local_session_ref: current.identity.host_session_ref,
|
|
2149
2722
|
local_turn_ref: current.identity.host_turn_ref,
|
|
2150
|
-
client_nonce: request2.client_nonce
|
|
2723
|
+
client_nonce: request2.client_nonce,
|
|
2724
|
+
...await visibleOutputBudget(descriptor)
|
|
2151
2725
|
};
|
|
2152
2726
|
const response2 = await mintedResult(config.mint_invocation_context, query);
|
|
2153
|
-
const
|
|
2154
|
-
|
|
2155
|
-
|
|
2727
|
+
const completedAt = now();
|
|
2728
|
+
const rechecked = await store.readInvocationContext(current.identity.host_source_ref, current.identity.host_session_ref, completedAt);
|
|
2729
|
+
if (rechecked === null || !sameTurn(current, rechecked)) throw new TypeError("context_replaced");
|
|
2730
|
+
return decodeCealHostInvocationAuthenticationResult(response2.result, completedAt);
|
|
2731
|
+
}
|
|
2732
|
+
async function visibleOutputBudget(descriptor) {
|
|
2733
|
+
const bytes = await descriptor.resolve_visible_output_max_bytes?.();
|
|
2734
|
+
return bytes === void 0 ? {} : { visible_output_max_bytes: bytes };
|
|
2156
2735
|
}
|
|
2157
2736
|
async function mintedResult(mint, query) {
|
|
2158
2737
|
let response2;
|
|
2159
2738
|
try {
|
|
2160
2739
|
response2 = await mint(query);
|
|
2161
2740
|
} catch (error) {
|
|
2162
|
-
throw new TypeError(
|
|
2741
|
+
throw new TypeError(exchangeFailureCause(hostRelayExchangeFailure(error) ?? "unavailable"));
|
|
2163
2742
|
}
|
|
2164
|
-
if (response2.status === "refused") throw new TypeError(
|
|
2165
|
-
if (response2.status !== "ready") throw new TypeError(
|
|
2743
|
+
if (response2.status === "refused") throw new TypeError(mintRefusalCause(response2.reason));
|
|
2744
|
+
if (response2.status !== "ready") throw new TypeError("no_binding");
|
|
2166
2745
|
return response2;
|
|
2167
2746
|
}
|
|
2747
|
+
function mintRefusalCause(reason) {
|
|
2748
|
+
if (isDirectMintRefusalCause(reason)) return reason;
|
|
2749
|
+
if (reason === "deadline" || reason === "cancelled") return "transient_retryable";
|
|
2750
|
+
if (reason === "mint_failed" || reason === "relay_failed") return reason;
|
|
2751
|
+
return isVocabularyWord(CEAL_HOST_MINT_REQUEST_CONTEXT_REFUSAL_REASONS, reason) ? "context_stale" : "host_refusal_invalid";
|
|
2752
|
+
}
|
|
2753
|
+
function isDirectMintRefusalCause(reason) {
|
|
2754
|
+
return isVocabularyWord(HOST_RELAY_DIRECT_WIRE_MINT_REASONS, reason);
|
|
2755
|
+
}
|
|
2756
|
+
function exchangeFailureCause(failure2) {
|
|
2757
|
+
if (failure2 === "permission_denied") return "relay_permission_denied";
|
|
2758
|
+
if (failure2 === "timed_out") return "transient_retryable";
|
|
2759
|
+
return "relay_unavailable";
|
|
2760
|
+
}
|
|
2168
2761
|
async function currentInvocation(descriptor, config, now) {
|
|
2169
2762
|
const store = descriptor.create_store(config.outbox_dir);
|
|
2170
2763
|
const identity = resolveIdentity(descriptor, config);
|
|
2171
2764
|
if (identity.status !== "ready") throw new TypeError(identity.status);
|
|
2172
|
-
const
|
|
2173
|
-
|
|
2174
|
-
|
|
2765
|
+
const waitBudgetMs = config.context_activation_wait_ms ?? 0;
|
|
2766
|
+
const monotonicNow = config.monotonic_now ?? (() => performance.now());
|
|
2767
|
+
const deadline = monotonicNow() + waitBudgetMs;
|
|
2768
|
+
let firstCheck = true;
|
|
2769
|
+
let waitEngaged = false;
|
|
2770
|
+
while (true) {
|
|
2771
|
+
if (!firstCheck && monotonicNow() >= deadline) {
|
|
2772
|
+
recordActivationWaitExhaustion(descriptor.producer_id, waitEngaged, waitBudgetMs);
|
|
2773
|
+
throw new TypeError("context_unavailable");
|
|
2774
|
+
}
|
|
2775
|
+
firstCheck = false;
|
|
2776
|
+
const observedAt = now();
|
|
2777
|
+
const current = await readLiveSessionContext(store, identity.identity.host_source_ref, identity.identity.host_session_ref, observedAt);
|
|
2778
|
+
if (current !== null) {
|
|
2779
|
+
recordActivationWaitCompletion(descriptor.producer_id, waitEngaged);
|
|
2780
|
+
return { store, current };
|
|
2781
|
+
}
|
|
2782
|
+
const remainingMs = deadline - monotonicNow();
|
|
2783
|
+
if (remainingMs <= 0) {
|
|
2784
|
+
recordActivationWaitExhaustion(descriptor.producer_id, waitEngaged, waitBudgetMs);
|
|
2785
|
+
throw new TypeError("context_unavailable");
|
|
2786
|
+
}
|
|
2787
|
+
recordActivationWaitEngagement(descriptor.producer_id, waitEngaged);
|
|
2788
|
+
waitEngaged = true;
|
|
2789
|
+
const intervalMs = Math.min(INVOCATION_CONTEXT_POLL_INTERVAL_MS, remainingMs);
|
|
2790
|
+
await (config.wait ?? waitForContextActivation)(intervalMs);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
function recordActivationWaitExhaustion(producer, engaged, budgetMs) {
|
|
2794
|
+
if (engaged || budgetMs > 0) writeHostServiceDiagnostic(producer, "activation_wait_exhausted");
|
|
2795
|
+
else writeHostServiceDiagnostic(producer, "activation_wait_skipped");
|
|
2796
|
+
}
|
|
2797
|
+
function recordActivationWaitCompletion(producer, engaged) {
|
|
2798
|
+
if (engaged) writeHostServiceDiagnostic(producer, "activation_wait_completed");
|
|
2799
|
+
}
|
|
2800
|
+
function recordActivationWaitEngagement(producer, engaged) {
|
|
2801
|
+
if (!engaged) writeHostServiceDiagnostic(producer, "activation_wait_engaged");
|
|
2802
|
+
}
|
|
2803
|
+
async function readLiveSessionContext(store, hostSourceRef, hostSessionRef, now) {
|
|
2804
|
+
const current = await store.readInvocationContext(hostSourceRef, hostSessionRef, now);
|
|
2805
|
+
if (current !== null) {
|
|
2806
|
+
if (Date.parse(current.expires_at) <= now.getTime()) throw new TypeError("context_expired");
|
|
2807
|
+
return current;
|
|
2808
|
+
}
|
|
2809
|
+
const raw = await store.readRawInvocationContext?.(hostSourceRef, hostSessionRef);
|
|
2810
|
+
if (raw !== null && raw !== void 0 && Date.parse(raw.expires_at) <= now.getTime()) throw new TypeError("context_expired");
|
|
2811
|
+
return null;
|
|
2175
2812
|
}
|
|
2176
2813
|
function resolveIdentity(descriptor, options) {
|
|
2177
2814
|
const sessionId = options.session_id === null ? void 0 : options.session_id ?? process.env[descriptor.session_id_env];
|
|
2178
2815
|
if (isHostSessionId(sessionId)) return { status: "ready", identity: { host_source_ref: options.host_source_ref, host_session_ref: descriptor.session_ref(options.host_source_ref, sessionId) } };
|
|
2179
|
-
return { status: "
|
|
2816
|
+
return { status: "session_unavailable" };
|
|
2180
2817
|
}
|
|
2181
2818
|
async function resolveOptions(descriptor, args, options) {
|
|
2182
2819
|
if (options !== void 0) return { input: [...args], options };
|
|
2183
2820
|
const configPath = descriptor.parse_config_flag(args);
|
|
2821
|
+
const config = await descriptor.read_producer_config(configPath);
|
|
2184
2822
|
return {
|
|
2185
2823
|
input: args.filter((argument) => !argument.startsWith(`${descriptor.config_flag}=`)),
|
|
2186
|
-
options: installedHostInvocationAuthenticationCommandOptions(
|
|
2824
|
+
options: installedHostInvocationAuthenticationCommandOptions(config, authenticationBudgetMsFromEnvironment(), descriptor.context_activation_wait_max_ms)
|
|
2187
2825
|
};
|
|
2188
2826
|
}
|
|
2827
|
+
function authenticationBudgetMsFromEnvironment() {
|
|
2828
|
+
const value = process.env[HOST_AUTHENTICATION_BUDGET_ENVIRONMENT];
|
|
2829
|
+
if (value === void 0) return 0;
|
|
2830
|
+
if (!/^(?:0|[1-9][0-9]{0,8})$/u.test(value)) throw new TypeError("invalid_arguments");
|
|
2831
|
+
return Number(value);
|
|
2832
|
+
}
|
|
2833
|
+
function boundedContextActivationWait(contextBudgetMs, relayTimeoutMs, maxContextWaitMs) {
|
|
2834
|
+
if (maxContextWaitMs === void 0 || contextBudgetMs <= 0) return 0;
|
|
2835
|
+
const availableWaitMs = contextBudgetMs - relayTimeoutMs - HOST_AUTHENTICATION_COMPLETION_RESERVE_MS;
|
|
2836
|
+
return Math.max(0, Math.min(maxContextWaitMs, availableWaitMs));
|
|
2837
|
+
}
|
|
2838
|
+
function waitForContextActivation(milliseconds) {
|
|
2839
|
+
return new Promise((resolve) => {
|
|
2840
|
+
setTimeout(resolve, milliseconds);
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2189
2843
|
function validateOptions(options) {
|
|
2190
|
-
if (typeof options.outbox_dir !== "string" || options.outbox_dir.length === 0 || typeof options.host_source_ref !== "string" || !SAFE_REF.test(options.host_source_ref) || typeof options.producer_installation_ref !== "string" || !SAFE_REF.test(options.producer_installation_ref) || !validOptionalSession(options.session_id)) {
|
|
2191
|
-
throw new TypeError("
|
|
2844
|
+
if (typeof options.outbox_dir !== "string" || options.outbox_dir.length === 0 || typeof options.host_source_ref !== "string" || !SAFE_REF.test(options.host_source_ref) || typeof options.producer_installation_ref !== "string" || !SAFE_REF.test(options.producer_installation_ref) || !validContextActivationWait(options.context_activation_wait_ms) || !validOptionalSession(options.session_id)) {
|
|
2845
|
+
throw new TypeError("options_invalid");
|
|
2192
2846
|
}
|
|
2193
2847
|
return options;
|
|
2194
2848
|
}
|
|
2849
|
+
function validContextActivationWait(value) {
|
|
2850
|
+
return value === void 0 || Number.isSafeInteger(value) && value >= 0 && value <= HOST_CONTEXT_ACTIVATION_WAIT_MAX_MS;
|
|
2851
|
+
}
|
|
2195
2852
|
function isHostSessionId(value) {
|
|
2196
2853
|
return typeof value === "string" && value.length > 0 && value.length <= 256 && !value.includes("\0");
|
|
2197
2854
|
}
|
|
@@ -2210,7 +2867,7 @@ function writeResult(value, sink) {
|
|
|
2210
2867
|
import { posix as posix3, win32 as win323 } from "node:path";
|
|
2211
2868
|
|
|
2212
2869
|
// host/runtime-source-paths.ts
|
|
2213
|
-
import { createHash as
|
|
2870
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2214
2871
|
import { posix as posix2, win32 as win322 } from "node:path";
|
|
2215
2872
|
|
|
2216
2873
|
// host/absolute-path.ts
|
|
@@ -2232,7 +2889,7 @@ function hostRuntimeSourcePaths(stateRoot, sourceRef, platform = process.platfor
|
|
|
2232
2889
|
const sourcesRoot = path.join(stateRoot, "sources");
|
|
2233
2890
|
const sourceRoot = path.join(sourcesRoot, sourceId);
|
|
2234
2891
|
const runtimeRoot = path.join(stateRoot, "run");
|
|
2235
|
-
const endpoint = platform === "win32" ? `\\\\.\\pipe\\ceal-host-${
|
|
2892
|
+
const endpoint = platform === "win32" ? `\\\\.\\pipe\\ceal-host-${createHash3("sha256").update(`${stateRoot}\0${sourceRef}`).digest("hex").slice(0, 24)}` : path.join(runtimeRoot, `${sourceId}.sock`);
|
|
2236
2893
|
assertHostRuntimeIpcLength(endpoint, platform);
|
|
2237
2894
|
return { sourceId, sourcesRoot, sourceRoot, relayRoot: path.join(sourceRoot, "relay"), codexOutbox: path.join(sourceRoot, "producers", "codex", "outbox"), claudeOutbox: path.join(sourceRoot, "producers", "claude", "outbox"), endpoint, runtimeRoot };
|
|
2238
2895
|
}
|
|
@@ -2248,19 +2905,24 @@ function assertHostProducerSourcePaths(stateRoot, config, producer, platform = p
|
|
|
2248
2905
|
|
|
2249
2906
|
// host/producer-service.ts
|
|
2250
2907
|
import { constants } from "node:fs";
|
|
2251
|
-
import { lstat as lstat2, open as
|
|
2908
|
+
import { lstat as lstat2, open as open3 } from "node:fs/promises";
|
|
2252
2909
|
var SAFE_REF3 = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
2253
2910
|
var HOST_PRODUCER_CONFIG_FLAG = "--ceal-host-config";
|
|
2254
2911
|
var HostProducerServiceError = class extends Error {
|
|
2255
2912
|
code;
|
|
2256
|
-
constructor(
|
|
2257
|
-
super(message ===
|
|
2913
|
+
constructor(code2, message = code2) {
|
|
2914
|
+
super(message === code2 ? code2 : `${code2}: ${message}`);
|
|
2258
2915
|
this.name = "HostProducerServiceError";
|
|
2259
|
-
this.code =
|
|
2916
|
+
this.code = code2;
|
|
2260
2917
|
}
|
|
2261
2918
|
};
|
|
2262
2919
|
var HOST_PRODUCER_CONFIG_KEYS = ["schema_version", "host_source_ref", "producer_installation_ref", "producer_epoch", "outbox_dir", "relay_endpoint"];
|
|
2263
2920
|
var HOST_PRODUCER_TIMING_DEFAULTS = Object.freeze({ reconcile_interval_ms: 250, retry_backoff_ms: 1e3, ipc_timeout_ms: 5e3 });
|
|
2921
|
+
function hostProducerExtraPath(extras, key) {
|
|
2922
|
+
const value = extras[key];
|
|
2923
|
+
if (value === void 0) throw hostProducerError("invalid_config");
|
|
2924
|
+
return value;
|
|
2925
|
+
}
|
|
2264
2926
|
function parseHostProducerConfigFlag(args, platform = process.platform) {
|
|
2265
2927
|
return parseProducerHostConfigFlag(args, HOST_PRODUCER_CONFIG_FLAG, platform, hostProducerError);
|
|
2266
2928
|
}
|
|
@@ -2281,8 +2943,8 @@ function decodeHostProducerConfigText(descriptor, text, platform = process.platf
|
|
|
2281
2943
|
const { extra_paths: extras, ...core } = decoded;
|
|
2282
2944
|
return Object.freeze(descriptor.config({ schema_version: descriptor.schema, ...core }, extras));
|
|
2283
2945
|
}
|
|
2284
|
-
function hostProducerError(
|
|
2285
|
-
return new HostProducerServiceError(
|
|
2946
|
+
function hostProducerError(code2, message = code2) {
|
|
2947
|
+
return new HostProducerServiceError(code2, message);
|
|
2286
2948
|
}
|
|
2287
2949
|
function parseProducerHostConfigFlag(args, flag, platform, error) {
|
|
2288
2950
|
const prefix = `${flag}=`;
|
|
@@ -2354,7 +3016,7 @@ function validateConfigFile(value, platform, error) {
|
|
|
2354
3016
|
}
|
|
2355
3017
|
async function openConfigFile(configPath, flags, error) {
|
|
2356
3018
|
try {
|
|
2357
|
-
return await
|
|
3019
|
+
return await open3(configPath, flags);
|
|
2358
3020
|
} catch {
|
|
2359
3021
|
throw error("config_unavailable");
|
|
2360
3022
|
}
|
|
@@ -2415,43 +3077,56 @@ function boundedMilliseconds(value) {
|
|
|
2415
3077
|
}
|
|
2416
3078
|
|
|
2417
3079
|
// host/claude/src/claude-decoder.ts
|
|
2418
|
-
import { createHash as
|
|
3080
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2419
3081
|
|
|
2420
3082
|
// host/claude/src/claude-types.ts
|
|
2421
|
-
var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "Stop", "StopFailure"];
|
|
2422
3083
|
var CLAUDE_SESSION_ID_ENVIRONMENT = "CLAUDE_CODE_SESSION_ID";
|
|
2423
|
-
var CLAUDE_HOST_ERROR_CODES = ["invalid_input", "storage", "configuration"];
|
|
3084
|
+
var CLAUDE_HOST_ERROR_CODES = ["invalid_input", "payload_rejected", "storage", "configuration"];
|
|
2424
3085
|
function isClaudeHostErrorCode(value) {
|
|
2425
|
-
return typeof value === "string" && CLAUDE_HOST_ERROR_CODES.some((
|
|
3086
|
+
return typeof value === "string" && CLAUDE_HOST_ERROR_CODES.some((code2) => code2 === value);
|
|
2426
3087
|
}
|
|
2427
|
-
var CLAUDE_OUTBOX_SCHEMA = "ceal.claude_hook_outbox.v1";
|
|
2428
3088
|
var CLAUDE_INVOCATION_CONTEXT_SCHEMA_VERSION = "ceal.claude_invocation_context.v1";
|
|
3089
|
+
var CLAUDE_OUTBOX_SCHEMA_VERSION = "ceal.claude_observation_outbox.v1";
|
|
3090
|
+
var CLAUDE_SESSION_SCHEMA_VERSION = "ceal.claude_session.v1";
|
|
3091
|
+
var CLAUDE_TRANSCRIPT_REGISTRATION_SCHEMA_VERSION = "ceal.claude_transcript_registration.v1";
|
|
3092
|
+
var CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION = "ceal.claude_transcript_cursor.v1";
|
|
3093
|
+
var CLAUDE_DEFAULT_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
3094
|
+
var CLAUDE_TRANSCRIPT_MAX_TURN_MESSAGES = 1024;
|
|
3095
|
+
var CLAUDE_TRANSCRIPT_MAX_SUBAGENT_FILES = 64;
|
|
3096
|
+
var CLAUDE_TRANSCRIPT_ANCHOR_BYTES = 256;
|
|
2429
3097
|
var ClaudeHostError = class extends Error {
|
|
2430
3098
|
name = "ClaudeHostError";
|
|
2431
3099
|
code;
|
|
2432
|
-
|
|
3100
|
+
diagnostic_kind;
|
|
3101
|
+
constructor(code2, message, diagnostic_kind) {
|
|
2433
3102
|
super(message);
|
|
2434
|
-
if (!isClaudeHostErrorCode(
|
|
2435
|
-
this.code =
|
|
3103
|
+
if (!isClaudeHostErrorCode(code2)) throw new TypeError("Claude Host error code is invalid.");
|
|
3104
|
+
this.code = code2;
|
|
3105
|
+
this.diagnostic_kind = diagnostic_kind;
|
|
2436
3106
|
}
|
|
2437
3107
|
};
|
|
2438
|
-
function isClaudeHookEvent(value) {
|
|
2439
|
-
return typeof value === "string" && CLAUDE_HOOK_EVENTS.some((event) => event === value);
|
|
2440
|
-
}
|
|
2441
3108
|
|
|
2442
3109
|
// host/claude/src/claude-decoder.ts
|
|
2443
3110
|
var MAX_HOOK_BYTES = 256 * 1024;
|
|
2444
|
-
var MAX_TEXT = 128 * 1024;
|
|
2445
3111
|
function claudeLocalSessionRef(sessionId) {
|
|
2446
3112
|
return `claude-session:${digest(sessionId)}`;
|
|
2447
3113
|
}
|
|
3114
|
+
function claudeLocalTurnRef(sessionId, turnId) {
|
|
3115
|
+
return `claude-turn:${cealRequestSha256({ session_id: sessionId, prompt_id: turnId })}`;
|
|
3116
|
+
}
|
|
3117
|
+
function claudeLocalInputRef(sessionId, turnId) {
|
|
3118
|
+
return `claude-input:${cealRequestSha256({ session_id: sessionId, prompt_id: turnId })}`;
|
|
3119
|
+
}
|
|
3120
|
+
function claudeWorkspaceRef(cwd) {
|
|
3121
|
+
return `workspace:${digest(cwd)}`;
|
|
3122
|
+
}
|
|
2448
3123
|
function digest(value) {
|
|
2449
|
-
return
|
|
3124
|
+
return createHash4("sha256").update(value, "utf8").digest("hex");
|
|
2450
3125
|
}
|
|
2451
3126
|
|
|
2452
3127
|
// host/claude/src/claude-outbox.ts
|
|
2453
|
-
import {
|
|
2454
|
-
import { join as
|
|
3128
|
+
import { mkdir as mkdir3, readdir, readFile as readFile2, unlink as unlink2 } from "node:fs/promises";
|
|
3129
|
+
import { isAbsolute, join as join3 } from "node:path";
|
|
2455
3130
|
|
|
2456
3131
|
// host/invocation-context.ts
|
|
2457
3132
|
var HOST_INVOCATION_IDENTITY_KEYS = ["host_session_ref", "host_turn_ref", "host_source_ref"];
|
|
@@ -2465,17 +3140,32 @@ function hasHostInvocationIdentityKeys(value) {
|
|
|
2465
3140
|
function hasHostInvocationContextKeys(value, optional = []) {
|
|
2466
3141
|
return requiredKeys(value, HOST_INVOCATION_CONTEXT_REQUIRED_KEYS, optional);
|
|
2467
3142
|
}
|
|
3143
|
+
async function readLatestSessionProjection(file, hostSourceRef, hostSessionRef, readProjection) {
|
|
3144
|
+
const context = await readProjection(file);
|
|
3145
|
+
if (context === null || context.identity.host_source_ref !== hostSourceRef || context.identity.host_session_ref !== hostSessionRef) return null;
|
|
3146
|
+
return context;
|
|
3147
|
+
}
|
|
3148
|
+
function isLiveSessionProjection(context, now) {
|
|
3149
|
+
return context !== null && Date.parse(context.expires_at) > now.getTime();
|
|
3150
|
+
}
|
|
2468
3151
|
function invocationContextFromTurn(identity, promptRef, expiresAt, descriptor) {
|
|
2469
3152
|
const context = {
|
|
2470
3153
|
schema_version: descriptor.schema_version,
|
|
2471
3154
|
identity,
|
|
2472
3155
|
prompt_ref: promptRef,
|
|
2473
3156
|
expires_at: expiresAt,
|
|
3157
|
+
...descriptor.turn_generation_ref === void 0 ? {} : { turn_generation_ref: descriptor.turn_generation_ref },
|
|
2474
3158
|
...descriptor.working_directory === void 0 ? {} : { working_directory: descriptor.working_directory }
|
|
2475
3159
|
};
|
|
2476
3160
|
descriptor.validate(context);
|
|
2477
3161
|
return Object.freeze(context);
|
|
2478
3162
|
}
|
|
3163
|
+
function isHostInvocationTurnSelection(value) {
|
|
3164
|
+
return isRecord2(value) && Object.keys(value).length === 5 && value.schema_version === "ceal.host_invocation_turn_selection.v1" && ["host_source_ref", "host_session_ref", "prompt_ref", "turn_generation_ref"].every((key) => safeRef3(value[key]));
|
|
3165
|
+
}
|
|
3166
|
+
function invocationTurnSelectionMatches(context, selection) {
|
|
3167
|
+
return selection === null || context !== null && selection.host_source_ref === context.identity.host_source_ref && selection.host_session_ref === context.identity.host_session_ref && selection.prompt_ref === context.prompt_ref && selection.turn_generation_ref === context.turn_generation_ref;
|
|
3168
|
+
}
|
|
2479
3169
|
function exactKeys4(value, keys) {
|
|
2480
3170
|
const actual = Object.keys(value);
|
|
2481
3171
|
return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key)) && actual.every((key) => keys.includes(key));
|
|
@@ -2485,10 +3175,16 @@ function requiredKeys(value, required, optional) {
|
|
|
2485
3175
|
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
2486
3176
|
return actual.length >= required.length && actual.length <= allowed.size && required.every((key) => Object.hasOwn(value, key)) && actual.every((key) => allowed.has(key));
|
|
2487
3177
|
}
|
|
3178
|
+
function isRecord2(value) {
|
|
3179
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3180
|
+
}
|
|
3181
|
+
function safeRef3(value) {
|
|
3182
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value);
|
|
3183
|
+
}
|
|
2488
3184
|
|
|
2489
3185
|
// host/claude/src/claude-invocation-context.ts
|
|
2490
|
-
function claudeInvocationContextFromTurn(identity, promptRef, expiresAt, workingDirectory) {
|
|
2491
|
-
return invocationContextFromTurn(identity, promptRef, expiresAt, { schema_version: CLAUDE_INVOCATION_CONTEXT_SCHEMA_VERSION, validate: validateClaudeHostInvocationContext, working_directory: workingDirectory });
|
|
3186
|
+
function claudeInvocationContextFromTurn(identity, promptRef, expiresAt, workingDirectory, turnGenerationRef) {
|
|
3187
|
+
return invocationContextFromTurn(identity, promptRef, expiresAt, { schema_version: CLAUDE_INVOCATION_CONTEXT_SCHEMA_VERSION, validate: validateClaudeHostInvocationContext, working_directory: workingDirectory, turn_generation_ref: turnGenerationRef });
|
|
2492
3188
|
}
|
|
2493
3189
|
function validateClaudeHostInvocationContext(value) {
|
|
2494
3190
|
if (!contextShape(value)) throw new ClaudeHostError("storage", "Claude Host invocation context is malformed.");
|
|
@@ -2497,23 +3193,23 @@ function validateClaudeHostInvocationContext(value) {
|
|
|
2497
3193
|
validateContextTimes(value);
|
|
2498
3194
|
}
|
|
2499
3195
|
function contextShape(value) {
|
|
2500
|
-
return record3(value) && value.schema_version === CLAUDE_INVOCATION_CONTEXT_SCHEMA_VERSION && hasHostInvocationContextKeys(value, ["request_ref", "bound_at", "working_directory"]);
|
|
3196
|
+
return record3(value) && value.schema_version === CLAUDE_INVOCATION_CONTEXT_SCHEMA_VERSION && hasHostInvocationContextKeys(value, ["request_ref", "bound_at", "working_directory", "turn_generation_ref"]);
|
|
2501
3197
|
}
|
|
2502
3198
|
function validateIdentity2(value) {
|
|
2503
3199
|
if (!validIdentity(value)) throw new ClaudeHostError("storage", "Claude Host invocation identity is malformed.");
|
|
2504
3200
|
}
|
|
2505
3201
|
function validateContextReferences(value) {
|
|
2506
|
-
if (!
|
|
3202
|
+
if (!safeRef4(value.prompt_ref) || !optionalSafeRef(value.request_ref) || !optionalSafeRef(value.turn_generation_ref)) throw new ClaudeHostError("storage", "Claude Host invocation context is malformed.");
|
|
2507
3203
|
}
|
|
2508
3204
|
function validateContextTimes(value) {
|
|
2509
3205
|
if (!timestamp2(value.expires_at) || !optionalTimestamp(value.bound_at) || !optionalDirectory(value.working_directory)) throw new ClaudeHostError("storage", "Claude Host invocation context is malformed.");
|
|
2510
3206
|
if (typeof value.bound_at === "string" && Date.parse(value.bound_at) > Date.parse(String(value.expires_at))) throw new ClaudeHostError("storage", `Claude Host invocation context has invalid binding time (${refusalFact("bound_at", value.bound_at, `at or before ${String(value.expires_at)}`)}).`);
|
|
2511
3207
|
}
|
|
2512
3208
|
function validIdentity(value) {
|
|
2513
|
-
return record3(value) && hasHostInvocationIdentityKeys(value) && [value.host_session_ref, value.host_turn_ref, value.host_source_ref].every(
|
|
3209
|
+
return record3(value) && hasHostInvocationIdentityKeys(value) && [value.host_session_ref, value.host_turn_ref, value.host_source_ref].every(safeRef4);
|
|
2514
3210
|
}
|
|
2515
3211
|
function optionalSafeRef(value) {
|
|
2516
|
-
return value === void 0 ||
|
|
3212
|
+
return value === void 0 || safeRef4(value);
|
|
2517
3213
|
}
|
|
2518
3214
|
function optionalTimestamp(value) {
|
|
2519
3215
|
return value === void 0 || timestamp2(value);
|
|
@@ -2521,7 +3217,7 @@ function optionalTimestamp(value) {
|
|
|
2521
3217
|
function optionalDirectory(value) {
|
|
2522
3218
|
return value === void 0 || typeof value === "string" && value.startsWith("/") && !value.includes("\0") && value.length <= 4096;
|
|
2523
3219
|
}
|
|
2524
|
-
function
|
|
3220
|
+
function safeRef4(value) {
|
|
2525
3221
|
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value);
|
|
2526
3222
|
}
|
|
2527
3223
|
function timestamp2(value) {
|
|
@@ -2533,29 +3229,67 @@ function record3(value) {
|
|
|
2533
3229
|
|
|
2534
3230
|
// host/claude/src/claude-outbox.ts
|
|
2535
3231
|
var SAFE_REF4 = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
3232
|
+
var CLAUDE_TRANSCRIPT_PATH_MAX_CHARACTERS = 4096;
|
|
3233
|
+
var CLAUDE_TRANSCRIPT_SUBAGENT_NAME_MAX_CHARACTERS = 256;
|
|
2536
3234
|
var ClaudeOutboxStore = class {
|
|
2537
|
-
|
|
3235
|
+
envelopes;
|
|
3236
|
+
sessions;
|
|
3237
|
+
registrations;
|
|
3238
|
+
cursors;
|
|
2538
3239
|
directory;
|
|
2539
3240
|
activeContexts;
|
|
3241
|
+
turnSelections;
|
|
2540
3242
|
constructor(directory) {
|
|
2541
3243
|
if (!directory.startsWith("/")) throw new ClaudeHostError("configuration", "Claude outbox directory must be absolute.");
|
|
2542
3244
|
this.directory = directory;
|
|
2543
|
-
this.
|
|
2544
|
-
this.
|
|
3245
|
+
this.envelopes = join3(directory, "envelopes");
|
|
3246
|
+
this.sessions = join3(directory, "sessions");
|
|
3247
|
+
this.registrations = join3(directory, "transcript-registrations");
|
|
3248
|
+
this.cursors = join3(directory, "transcript-cursors");
|
|
3249
|
+
this.activeContexts = join3(directory, "active-contexts");
|
|
3250
|
+
this.turnSelections = join3(directory, "turn-selections");
|
|
2545
3251
|
}
|
|
2546
3252
|
async prepare() {
|
|
2547
|
-
|
|
2548
|
-
|
|
3253
|
+
for (const path of [this.envelopes, this.sessions, this.registrations, this.cursors, this.activeContexts, this.turnSelections]) {
|
|
3254
|
+
await mkdir3(path, { recursive: true, mode: 448 });
|
|
3255
|
+
}
|
|
2549
3256
|
}
|
|
2550
|
-
async activateInvocationContext(identity, promptRef, expiresAt, workingDirectory) {
|
|
2551
|
-
const context = claudeInvocationContextFromTurn(identity, promptRef, expiresAt, workingDirectory);
|
|
3257
|
+
async activateInvocationContext(identity, promptRef, expiresAt, workingDirectory, turnGenerationRef) {
|
|
3258
|
+
const context = claudeInvocationContextFromTurn(identity, promptRef, expiresAt, workingDirectory, turnGenerationRef);
|
|
2552
3259
|
await this.prepare();
|
|
2553
3260
|
await durableAtomicReplace(this.activeContextPath(identity), JSON.stringify(context));
|
|
2554
3261
|
}
|
|
2555
3262
|
async readInvocationContext(hostSourceRef, hostSessionRef, now = /* @__PURE__ */ new Date()) {
|
|
2556
3263
|
validateInvocationContextReadArguments(hostSourceRef, hostSessionRef, now);
|
|
3264
|
+
const context = await this.readRawInvocationContext(hostSourceRef, hostSessionRef);
|
|
3265
|
+
return isLiveSessionProjection(context, now) ? context : null;
|
|
3266
|
+
}
|
|
3267
|
+
async writeInvocationTurnSelection(selection) {
|
|
3268
|
+
if (!isHostInvocationTurnSelection(selection)) throw new ClaudeHostError("invalid_input", "Claude Host turn selection is invalid.");
|
|
3269
|
+
await this.prepare();
|
|
3270
|
+
await durableAtomicReplace(this.turnSelectionPath(selection.host_source_ref, selection.host_session_ref), JSON.stringify(selection));
|
|
3271
|
+
}
|
|
3272
|
+
async attachInvocationTurnGeneration(identity, promptRef, turnGenerationRef) {
|
|
3273
|
+
const selection = await this.readInvocationTurnSelection(identity.host_source_ref, identity.host_session_ref);
|
|
3274
|
+
if (selection === null || selection.prompt_ref !== promptRef || selection.turn_generation_ref !== turnGenerationRef) return false;
|
|
3275
|
+
const path = this.activeContextPath(identity);
|
|
3276
|
+
const active = await readProjectionFile(path);
|
|
3277
|
+
if (active === null || active.prompt_ref !== promptRef || active.identity.host_turn_ref !== identity.host_turn_ref) return false;
|
|
3278
|
+
const updated = { ...active, turn_generation_ref: turnGenerationRef };
|
|
3279
|
+
validateClaudeHostInvocationContext(updated);
|
|
3280
|
+
await durableAtomicReplace(path, JSON.stringify(updated));
|
|
3281
|
+
return true;
|
|
3282
|
+
}
|
|
3283
|
+
/**
|
|
3284
|
+
* The latest Claude session projection even when past expiry, so the
|
|
3285
|
+
* invocation authenticator can report an expired context distinctly from
|
|
3286
|
+
* a missing one. Every other caller keeps the fail-closed read above.
|
|
3287
|
+
*/
|
|
3288
|
+
async readRawInvocationContext(hostSourceRef, hostSessionRef) {
|
|
3289
|
+
if (!SAFE_REF4.test(hostSourceRef) || !SAFE_REF4.test(hostSessionRef)) throw new ClaudeHostError("invalid_input", "Claude Host context identity or clock is invalid.");
|
|
2557
3290
|
await this.prepare();
|
|
2558
|
-
|
|
3291
|
+
const context = await readLatestSessionProjection(this.activeContextPath({ host_source_ref: hostSourceRef, host_session_ref: hostSessionRef }), hostSourceRef, hostSessionRef, readProjectionFile);
|
|
3292
|
+
return invocationTurnSelectionMatches(context, await this.readInvocationTurnSelection(hostSourceRef, hostSessionRef)) ? context : null;
|
|
2559
3293
|
}
|
|
2560
3294
|
async listInvocationContexts(now = /* @__PURE__ */ new Date()) {
|
|
2561
3295
|
await this.prepare();
|
|
@@ -2563,106 +3297,175 @@ var ClaudeOutboxStore = class {
|
|
|
2563
3297
|
for (const name of await readdir(this.activeContexts)) {
|
|
2564
3298
|
if (!name.endsWith(".json")) continue;
|
|
2565
3299
|
try {
|
|
2566
|
-
const value = JSON.parse(await
|
|
3300
|
+
const value = JSON.parse(await readFile2(join3(this.activeContexts, name), "utf8"));
|
|
2567
3301
|
validateClaudeHostInvocationContext(value);
|
|
2568
|
-
if (Date.parse(value.expires_at) > now.getTime()) contexts.push(value);
|
|
3302
|
+
if (Date.parse(value.expires_at) > now.getTime() && invocationTurnSelectionMatches(value, await this.readInvocationTurnSelection(value.identity.host_source_ref, value.identity.host_session_ref))) contexts.push(value);
|
|
2569
3303
|
} catch {
|
|
2570
3304
|
}
|
|
2571
3305
|
}
|
|
2572
3306
|
return contexts;
|
|
2573
3307
|
}
|
|
2574
|
-
async
|
|
2575
|
-
|
|
3308
|
+
async putEnvelope(entry) {
|
|
3309
|
+
validateEnvelope(entry);
|
|
2576
3310
|
await this.prepare();
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
3311
|
+
await durableAtomicReplace(this.envelopePath(entry.submission_ref), JSON.stringify(entry));
|
|
3312
|
+
}
|
|
3313
|
+
async listEnvelopes() {
|
|
3314
|
+
await this.prepare();
|
|
3315
|
+
const entries = [];
|
|
3316
|
+
let malformed = 0;
|
|
3317
|
+
for (const name of await readdir(this.envelopes)) {
|
|
3318
|
+
if (!name.endsWith(".json")) continue;
|
|
2580
3319
|
try {
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
3320
|
+
const value = JSON.parse(await readFile2(join3(this.envelopes, name), "utf8"));
|
|
3321
|
+
validateEnvelope(value);
|
|
3322
|
+
entries.push(value);
|
|
3323
|
+
} catch {
|
|
3324
|
+
malformed += 1;
|
|
2585
3325
|
}
|
|
3326
|
+
}
|
|
3327
|
+
entries.sort((left, right) => left.created_at.localeCompare(right.created_at));
|
|
3328
|
+
return { entries, malformed };
|
|
3329
|
+
}
|
|
3330
|
+
async removeEnvelope(entry) {
|
|
3331
|
+
validateEnvelope(entry);
|
|
3332
|
+
try {
|
|
3333
|
+
await unlink2(this.envelopePath(entry.submission_ref));
|
|
2586
3334
|
} catch (error) {
|
|
2587
|
-
if (code(error) !== "
|
|
2588
|
-
const existing = JSON.parse(await readFile(path, "utf8"));
|
|
2589
|
-
validateBatch(existing);
|
|
2590
|
-
if (JSON.stringify(existing) !== JSON.stringify(batch)) throw new ClaudeHostError("storage", "Claude hook batch identity conflicts with different bytes.");
|
|
3335
|
+
if (code(error) !== "ENOENT") throw error;
|
|
2591
3336
|
}
|
|
2592
3337
|
}
|
|
2593
|
-
async
|
|
3338
|
+
async saveSession(snapshot) {
|
|
3339
|
+
validateSession(snapshot);
|
|
2594
3340
|
await this.prepare();
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
3341
|
+
await durableAtomicReplace(join3(this.sessions, `${cealRequestSha256({ source: snapshot.host_source_ref, session: snapshot.host_session_ref })}.json`), JSON.stringify(snapshot));
|
|
3342
|
+
}
|
|
3343
|
+
async readSession(sourceRef, sessionRef) {
|
|
3344
|
+
if (!SAFE_REF4.test(sourceRef) || !SAFE_REF4.test(sessionRef)) throw new ClaudeHostError("invalid_input", "Claude session identity is invalid.");
|
|
3345
|
+
await this.prepare();
|
|
3346
|
+
try {
|
|
3347
|
+
const value = JSON.parse(await readFile2(join3(this.sessions, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`), "utf8"));
|
|
3348
|
+
validateSession(value);
|
|
3349
|
+
return value;
|
|
3350
|
+
} catch (error) {
|
|
3351
|
+
if (code(error) === "ENOENT") return null;
|
|
3352
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Claude session snapshot is malformed.");
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
async registerTranscript(registration) {
|
|
3356
|
+
validateRegistration(registration);
|
|
3357
|
+
await this.prepare();
|
|
3358
|
+
const path = this.registrationPath(registration.host_source_ref, registration.host_session_ref);
|
|
3359
|
+
const existing = await this.readRegistrationFile(path);
|
|
3360
|
+
if (existing !== null && (existing.claude_session_id !== registration.claude_session_id || existing.transcript_path !== registration.transcript_path)) {
|
|
3361
|
+
throw new ClaudeHostError("storage", "Claude transcript registration conflicts with the source-bound session.");
|
|
3362
|
+
}
|
|
3363
|
+
await durableAtomicReplace(path, JSON.stringify(registration));
|
|
3364
|
+
}
|
|
3365
|
+
/**
|
|
3366
|
+
* End a registration whose transcript the Host has deleted. The cursor is
|
|
3367
|
+
* keyed by the same session and describes byte offsets of a file that no
|
|
3368
|
+
* longer exists, so it leaves with the registration.
|
|
3369
|
+
*/
|
|
3370
|
+
async retireTranscriptRegistration(registration) {
|
|
3371
|
+
validateRegistration(registration);
|
|
3372
|
+
await this.prepare();
|
|
3373
|
+
await this.removeStateFile(this.registrationPath(registration.host_source_ref, registration.host_session_ref));
|
|
3374
|
+
await this.removeStateFile(this.cursorPath(registration.host_source_ref, registration.host_session_ref));
|
|
3375
|
+
}
|
|
3376
|
+
async listTranscriptRegistrations() {
|
|
3377
|
+
await this.prepare();
|
|
3378
|
+
const registrations = [];
|
|
3379
|
+
for (const name of await readdir(this.registrations)) {
|
|
2598
3380
|
if (!name.endsWith(".json")) continue;
|
|
2599
3381
|
try {
|
|
2600
|
-
const value = JSON.parse(await
|
|
2601
|
-
|
|
2602
|
-
|
|
3382
|
+
const value = JSON.parse(await readFile2(join3(this.registrations, name), "utf8"));
|
|
3383
|
+
validateRegistration(value);
|
|
3384
|
+
registrations.push(value);
|
|
2603
3385
|
} catch {
|
|
2604
|
-
malformed += 1;
|
|
2605
3386
|
}
|
|
2606
3387
|
}
|
|
2607
|
-
|
|
2608
|
-
return { entries: batches, malformed };
|
|
3388
|
+
return registrations.sort((left, right) => left.registered_at.localeCompare(right.registered_at));
|
|
2609
3389
|
}
|
|
2610
|
-
async
|
|
3390
|
+
async readInvocationTurnSelection(hostSourceRef, hostSessionRef) {
|
|
3391
|
+
if (!SAFE_REF4.test(hostSourceRef) || !SAFE_REF4.test(hostSessionRef)) throw new ClaudeHostError("invalid_input", "Claude Host turn selection identity is invalid.");
|
|
3392
|
+
await this.prepare();
|
|
2611
3393
|
try {
|
|
2612
|
-
await
|
|
3394
|
+
const value = JSON.parse(await readFile2(this.turnSelectionPath(hostSourceRef, hostSessionRef), "utf8"));
|
|
3395
|
+
if (!isHostInvocationTurnSelection(value) || value.host_source_ref !== hostSourceRef || value.host_session_ref !== hostSessionRef) throw new ClaudeHostError("storage", "Claude Host turn selection is malformed.");
|
|
3396
|
+
return value;
|
|
2613
3397
|
} catch (error) {
|
|
2614
|
-
if (code(error)
|
|
3398
|
+
if (code(error) === "ENOENT") return null;
|
|
3399
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Unable to read the Claude Host turn selection.");
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
async readTranscriptCursor(registration) {
|
|
3403
|
+
validateRegistration(registration);
|
|
3404
|
+
await this.prepare();
|
|
3405
|
+
try {
|
|
3406
|
+
const value = JSON.parse(await readFile2(this.cursorPath(registration.host_source_ref, registration.host_session_ref), "utf8"));
|
|
3407
|
+
validateCursor(value);
|
|
3408
|
+
return value;
|
|
3409
|
+
} catch (error) {
|
|
3410
|
+
if (code(error) === "ENOENT") return null;
|
|
3411
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Claude transcript cursor is malformed.");
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
async writeTranscriptCursor(cursor) {
|
|
3415
|
+
validateCursor(cursor);
|
|
3416
|
+
await this.prepare();
|
|
3417
|
+
await durableAtomicReplace(this.cursorPath(cursor.host_source_ref, cursor.host_session_ref), JSON.stringify(cursor));
|
|
3418
|
+
}
|
|
3419
|
+
envelopePath(ref) {
|
|
3420
|
+
return join3(this.envelopes, `${cealRequestSha256(ref)}.json`);
|
|
3421
|
+
}
|
|
3422
|
+
registrationPath(sourceRef, sessionRef) {
|
|
3423
|
+
return join3(this.registrations, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`);
|
|
3424
|
+
}
|
|
3425
|
+
turnSelectionPath(sourceRef, sessionRef) {
|
|
3426
|
+
return join3(this.turnSelections, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`);
|
|
3427
|
+
}
|
|
3428
|
+
cursorPath(sourceRef, sessionRef) {
|
|
3429
|
+
return join3(this.cursors, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`);
|
|
3430
|
+
}
|
|
3431
|
+
async readRegistrationFile(path) {
|
|
3432
|
+
try {
|
|
3433
|
+
const value = JSON.parse(await readFile2(path, "utf8"));
|
|
3434
|
+
validateRegistration(value);
|
|
3435
|
+
return value;
|
|
3436
|
+
} catch (error) {
|
|
3437
|
+
if (code(error) === "ENOENT") return null;
|
|
3438
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Claude transcript registration is malformed.");
|
|
2615
3439
|
}
|
|
2616
3440
|
}
|
|
2617
|
-
path
|
|
2618
|
-
|
|
3441
|
+
async removeStateFile(path) {
|
|
3442
|
+
try {
|
|
3443
|
+
await unlink2(path);
|
|
3444
|
+
} catch (error) {
|
|
3445
|
+
if (code(error) !== "ENOENT") throw error;
|
|
3446
|
+
}
|
|
2619
3447
|
}
|
|
2620
3448
|
activeContextPath(identity) {
|
|
2621
|
-
return
|
|
3449
|
+
return join3(this.activeContexts, `${invocationContextKey(identity)}.json`);
|
|
2622
3450
|
}
|
|
2623
3451
|
};
|
|
2624
3452
|
function validateInvocationContextReadArguments(hostSourceRef, hostSessionRef, now) {
|
|
2625
3453
|
if (!SAFE_REF4.test(hostSourceRef) || !SAFE_REF4.test(hostSessionRef) || !(now instanceof Date) || !Number.isFinite(now.getTime())) throw new ClaudeHostError("invalid_input", "Claude Host context identity or clock is invalid.");
|
|
2626
3454
|
}
|
|
2627
|
-
async function
|
|
3455
|
+
async function readProjectionFile(file) {
|
|
2628
3456
|
try {
|
|
2629
|
-
const context = JSON.parse(await
|
|
3457
|
+
const context = JSON.parse(await readFile2(file, "utf8"));
|
|
2630
3458
|
validateClaudeHostInvocationContext(context);
|
|
2631
|
-
return
|
|
3459
|
+
return context;
|
|
2632
3460
|
} catch (error) {
|
|
2633
3461
|
if (code(error) === "ENOENT") return null;
|
|
2634
3462
|
if (error instanceof ClaudeHostError) throw error;
|
|
2635
3463
|
throw new ClaudeHostError("storage", "Unable to read the Claude Host invocation context.");
|
|
2636
3464
|
}
|
|
2637
3465
|
}
|
|
2638
|
-
function
|
|
2639
|
-
return context.identity.host_source_ref === hostSourceRef && context.identity.host_session_ref === hostSessionRef;
|
|
2640
|
-
}
|
|
2641
|
-
function compareBatch(left, right) {
|
|
2642
|
-
return rank(left.hook_event) - rank(right.hook_event) || left.occurred_at.localeCompare(right.occurred_at) || left.batch_ref.localeCompare(right.batch_ref);
|
|
2643
|
-
}
|
|
2644
|
-
function rank(event) {
|
|
2645
|
-
return event === "SessionStart" ? 0 : event === "UserPromptSubmit" ? 1 : 2;
|
|
2646
|
-
}
|
|
2647
|
-
function validateBatch(value) {
|
|
2648
|
-
if (!validBatchHeader(value)) throw new ClaudeHostError("storage", "Claude hook batch is malformed.");
|
|
2649
|
-
for (const [index, draft] of value.frames.entries()) {
|
|
2650
|
-
if (!record4(draft) || !hostFrameKind(draft.frame_kind) || !record4(draft.payload)) throw new ClaudeHostError("storage", "Claude frame draft is malformed.");
|
|
2651
|
-
encodeHostProducerFrame({ schema_version: "ceal.host_producer_frame.v1", producer_id: "claude", producer_installation_ref: "validation", producer_epoch: 1, sequence: index + 1, frame_kind: draft.frame_kind, occurred_at: value.occurred_at, payload: draft.payload });
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
function validBatchHeader(value) {
|
|
2655
|
-
return record4(value) && value.schema_version === CLAUDE_OUTBOX_SCHEMA && safeRef4(value.batch_ref) && isClaudeHookEvent(value.hook_event) && safeRef4(value.local_session_ref) && optionalRef(value.local_turn_ref) && timestamp3(value.occurred_at) && Array.isArray(value.frames) && value.frames.length > 0 && value.frames.length <= 3;
|
|
2656
|
-
}
|
|
2657
|
-
function hostFrameKind(value) {
|
|
2658
|
-
return isHostProducerFrameKind(value);
|
|
2659
|
-
}
|
|
2660
|
-
function safeRef4(value) {
|
|
3466
|
+
function safeRef5(value) {
|
|
2661
3467
|
return typeof value === "string" && SAFE_REF4.test(value);
|
|
2662
3468
|
}
|
|
2663
|
-
function optionalRef(value) {
|
|
2664
|
-
return value === void 0 || typeof value === "string" && SAFE_REF4.test(value);
|
|
2665
|
-
}
|
|
2666
3469
|
function timestamp3(value) {
|
|
2667
3470
|
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
2668
3471
|
}
|
|
@@ -2672,133 +3475,766 @@ function record4(value) {
|
|
|
2672
3475
|
function code(error) {
|
|
2673
3476
|
return record4(error) && typeof error.code === "string" ? error.code : null;
|
|
2674
3477
|
}
|
|
3478
|
+
function validateEnvelope(value) {
|
|
3479
|
+
if (!record4(value)) throw new ClaudeHostError("storage", "Claude outbox entry is malformed.");
|
|
3480
|
+
if (!sameKeys(value, ["schema_version", "identity", "host_input_ref", "input_kind", "prompt_ref", "observations", "origin_input", "submission_ref", "created_at", "expires_at"], ["request_ref"])) {
|
|
3481
|
+
throw new ClaudeHostError("storage", `Claude outbox entry key set is malformed (${refusalFact("keys", Object.keys(value), "exactly the outbox entry key set")}).`);
|
|
3482
|
+
}
|
|
3483
|
+
if (value.schema_version !== CLAUDE_OUTBOX_SCHEMA_VERSION) throw new ClaudeHostError("storage", `Claude outbox entry is malformed (${refusalFact("schema_version", value.schema_version, `expected ${CLAUDE_OUTBOX_SCHEMA_VERSION}`)}).`);
|
|
3484
|
+
validateEnvelopeIdentity(value);
|
|
3485
|
+
validateEnvelopeObservations(value.observations);
|
|
3486
|
+
if (typeof value.origin_input !== "boolean") throw new ClaudeHostError("storage", "Claude outbox origin disposition is malformed.");
|
|
3487
|
+
if (!timestamp3(value.created_at) || !timestamp3(value.expires_at)) throw new ClaudeHostError("storage", "Claude outbox timestamps are malformed.");
|
|
3488
|
+
}
|
|
3489
|
+
function validateEnvelopeIdentity(value) {
|
|
3490
|
+
validateIdentity3(value.identity);
|
|
3491
|
+
if (!safeRef5(value.host_input_ref)) throw new ClaudeHostError("storage", "Claude outbox input reference is malformed.");
|
|
3492
|
+
if (!isCealHostInputKind(value.input_kind)) throw new ClaudeHostError("storage", "Claude outbox input provenance is malformed.");
|
|
3493
|
+
if (!safeRef5(value.prompt_ref) || !safeRef5(value.submission_ref)) throw new ClaudeHostError("storage", "Claude outbox references are malformed.");
|
|
3494
|
+
if (value.request_ref !== void 0 && !safeRef5(value.request_ref)) throw new ClaudeHostError("storage", "Claude outbox Request reference is malformed.");
|
|
3495
|
+
}
|
|
3496
|
+
function validateEnvelopeObservations(value) {
|
|
3497
|
+
if (!Array.isArray(value)) throw new ClaudeHostError("storage", "Claude outbox observations are malformed.");
|
|
3498
|
+
try {
|
|
3499
|
+
for (const observation2 of value) validateCealHostObservationDraft(observation2);
|
|
3500
|
+
} catch {
|
|
3501
|
+
throw new ClaudeHostError("storage", "Claude outbox observations are malformed.");
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
function validateSession(value) {
|
|
3505
|
+
if (!record4(value)) throw new ClaudeHostError("storage", "Claude session snapshot is malformed.");
|
|
3506
|
+
if (!sameKeys(value, ["schema_version", "host_session_ref", "host_source_ref", "observed_at", "value", "expires_at"])) {
|
|
3507
|
+
throw new ClaudeHostError("storage", `Claude session snapshot key set is malformed (${refusalFact("keys", Object.keys(value), "exactly the session snapshot key set")}).`);
|
|
3508
|
+
}
|
|
3509
|
+
if (value.schema_version !== CLAUDE_SESSION_SCHEMA_VERSION) throw new ClaudeHostError("storage", `Claude session snapshot is malformed (${refusalFact("schema_version", value.schema_version, `expected ${CLAUDE_SESSION_SCHEMA_VERSION}`)}).`);
|
|
3510
|
+
if (!safeRef5(value.host_session_ref) || !safeRef5(value.host_source_ref) || !timestamp3(value.observed_at) || !timestamp3(value.expires_at)) {
|
|
3511
|
+
throw new ClaudeHostError("storage", "Claude session snapshot fields are malformed.");
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
function validateRegistration(value) {
|
|
3515
|
+
if (!record4(value)) throw new ClaudeHostError("storage", "Claude transcript registration is malformed.");
|
|
3516
|
+
if (!sameKeys(value, ["schema_version", "host_source_ref", "host_session_ref", "claude_session_id", "transcript_path", "registered_at", "expires_at"], ["cwd"])) {
|
|
3517
|
+
throw new ClaudeHostError("storage", `Claude transcript registration key set is malformed (${refusalFact("keys", Object.keys(value), "exactly the registration key set")}).`);
|
|
3518
|
+
}
|
|
3519
|
+
if (value.schema_version !== CLAUDE_TRANSCRIPT_REGISTRATION_SCHEMA_VERSION) throw new ClaudeHostError("storage", "Claude transcript registration is malformed.");
|
|
3520
|
+
if (![value.host_source_ref, value.host_session_ref, value.claude_session_id].every((candidate) => typeof candidate === "string" && SAFE_REF4.test(candidate))) {
|
|
3521
|
+
throw new ClaudeHostError("storage", "Claude transcript registration references are malformed.");
|
|
3522
|
+
}
|
|
3523
|
+
validateRegistrationPaths(value.cwd, value.transcript_path);
|
|
3524
|
+
if (!timestamp3(value.registered_at) || !timestamp3(value.expires_at)) throw new ClaudeHostError("storage", "Claude transcript registration timestamps are malformed.");
|
|
3525
|
+
}
|
|
3526
|
+
function validateRegistrationPaths(cwd, transcriptPath) {
|
|
3527
|
+
if (cwd !== void 0 && (typeof cwd !== "string" || !isAbsolute(cwd) || cwd.length > CLAUDE_TRANSCRIPT_PATH_MAX_CHARACTERS || cwd.includes("\0"))) {
|
|
3528
|
+
throw new ClaudeHostError("storage", "Claude transcript registration working directory is malformed.");
|
|
3529
|
+
}
|
|
3530
|
+
if (typeof transcriptPath !== "string" || transcriptPath.length === 0 || transcriptPath.length > CLAUDE_TRANSCRIPT_PATH_MAX_CHARACTERS) {
|
|
3531
|
+
throw new ClaudeHostError("storage", "Claude transcript registration path is malformed.");
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
function validateCursor(value) {
|
|
3535
|
+
if (!record4(value)) throw new ClaudeHostError("storage", "Claude transcript cursor is malformed.");
|
|
3536
|
+
if (!sameKeys(value, ["schema_version", "host_source_ref", "host_session_ref", "transcript_path", "offset", "anchor_start", "anchor_sha256", "subagents", "status", "updated_at"], ["device", "inode", "open_turn", "unattributed", "session_observed", "reason"])) {
|
|
3537
|
+
throw new ClaudeHostError("storage", "Claude transcript cursor key set is malformed.");
|
|
3538
|
+
}
|
|
3539
|
+
if (value.schema_version !== CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION) throw new ClaudeHostError("storage", "Claude transcript cursor is malformed.");
|
|
3540
|
+
if (![value.host_source_ref, value.host_session_ref].every((candidate) => typeof candidate === "string" && SAFE_REF4.test(candidate))) throw new ClaudeHostError("storage", "Claude transcript cursor references are malformed.");
|
|
3541
|
+
if (typeof value.transcript_path !== "string" || value.transcript_path.length === 0) throw new ClaudeHostError("storage", "Claude transcript cursor path is malformed.");
|
|
3542
|
+
validateCursorPosition(value);
|
|
3543
|
+
validateCursorTurn(value.open_turn, value.unattributed, value.session_observed);
|
|
3544
|
+
validateSubagentCursors(value.subagents);
|
|
3545
|
+
if (value.reason !== void 0 && !isHostTranscriptCursorReason(value.reason)) throw new ClaudeHostError("storage", "Claude transcript cursor reason is malformed.");
|
|
3546
|
+
}
|
|
3547
|
+
function validateCursorPosition(value) {
|
|
3548
|
+
if (!validCursorPosition(value)) throw new ClaudeHostError("storage", "Claude transcript cursor position is malformed.");
|
|
3549
|
+
if (value.device !== void 0 && typeof value.device !== "string") throw new ClaudeHostError("storage", "Claude transcript cursor device is malformed.");
|
|
3550
|
+
if (value.inode !== void 0 && typeof value.inode !== "string") throw new ClaudeHostError("storage", "Claude transcript cursor inode is malformed.");
|
|
3551
|
+
if (value.device === void 0 && value.offset !== 0) throw new ClaudeHostError("storage", "Claude transcript cursor is unpinned past its start.");
|
|
3552
|
+
if (value.status !== "active" && value.status !== "quarantined" || !timestamp3(value.updated_at)) throw new ClaudeHostError("storage", "Claude transcript cursor status is malformed.");
|
|
3553
|
+
}
|
|
3554
|
+
function validateCursorTurn(openTurn, unattributed, sessionObserved) {
|
|
3555
|
+
if (openTurn !== void 0 && !validOpenTurn(openTurn)) throw new ClaudeHostError("storage", "Claude transcript turn cursor is malformed.");
|
|
3556
|
+
if (unattributed !== void 0 && !validUnattributed(unattributed)) throw new ClaudeHostError("storage", "Claude transcript unattributed cursor is malformed.");
|
|
3557
|
+
if (sessionObserved !== void 0 && typeof sessionObserved !== "boolean") throw new ClaudeHostError("storage", "Claude transcript cursor session marker is malformed.");
|
|
3558
|
+
}
|
|
3559
|
+
function validateSubagentCursors(value) {
|
|
3560
|
+
if (!record4(value)) throw new ClaudeHostError("storage", "Claude transcript subagent cursors are malformed.");
|
|
3561
|
+
for (const [name, entry] of Object.entries(value)) validateSubagentEntry(name, entry);
|
|
3562
|
+
}
|
|
3563
|
+
function validateSubagentEntry(name, entry) {
|
|
3564
|
+
if (name.length === 0 || name.length > CLAUDE_TRANSCRIPT_SUBAGENT_NAME_MAX_CHARACTERS || name.includes("/")) throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3565
|
+
if (name.includes("\0")) throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3566
|
+
if (!record4(entry) || !sameKeys(entry, ["offset", "anchor_start", "anchor_sha256"], ["device", "inode"]) || !nonNegativeInteger(entry.offset)) throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3567
|
+
validateSubagentIdentity(entry);
|
|
3568
|
+
}
|
|
3569
|
+
function validateSubagentIdentity(entry) {
|
|
3570
|
+
if (entry.device !== void 0 && typeof entry.device !== "string" || entry.inode !== void 0 && typeof entry.inode !== "string") {
|
|
3571
|
+
throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3572
|
+
}
|
|
3573
|
+
if (!nonNegativeInteger(entry.anchor_start) || typeof entry.anchor_sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(entry.anchor_sha256)) {
|
|
3574
|
+
throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3575
|
+
}
|
|
3576
|
+
if (entry.device === void 0 && Number(entry.offset) !== 0) throw new ClaudeHostError("storage", "Claude transcript subagent cursor is unpinned past its start.");
|
|
3577
|
+
}
|
|
3578
|
+
function validCursorPosition(value) {
|
|
3579
|
+
return nonNegativeInteger(value.offset) && nonNegativeInteger(value.anchor_start) && typeof value.anchor_sha256 === "string" && /^[a-f0-9]{64}$/u.test(value.anchor_sha256);
|
|
3580
|
+
}
|
|
3581
|
+
function validOpenTurn(value) {
|
|
3582
|
+
if (!record4(value) || !sameKeys(value, ["turn_key", "user_uuid", "prompt_ref", "host_input_ref", "started_at", "last_observed_at", "message_ids", "counts"], ["last_terminal_digest"])) return false;
|
|
3583
|
+
if (![value.turn_key, value.user_uuid, value.prompt_ref, value.host_input_ref].every((candidate) => typeof candidate === "string" && SAFE_REF4.test(candidate))) return false;
|
|
3584
|
+
if (!timestamp3(value.started_at) || !timestamp3(value.last_observed_at)) return false;
|
|
3585
|
+
if (!validMessageIds(value.message_ids)) return false;
|
|
3586
|
+
if (!validTurnCounts(value.counts)) return false;
|
|
3587
|
+
return value.last_terminal_digest === void 0 || typeof value.last_terminal_digest === "string" && /^[a-f0-9]{64}$/u.test(value.last_terminal_digest);
|
|
3588
|
+
}
|
|
3589
|
+
function validMessageIds(value) {
|
|
3590
|
+
if (!Array.isArray(value) || value.length > CLAUDE_TRANSCRIPT_MAX_TURN_MESSAGES) return false;
|
|
3591
|
+
if (!value.every((candidate) => typeof candidate === "string" && candidate.length > 0 && candidate.length <= 256)) return false;
|
|
3592
|
+
return new Set(value).size === value.length;
|
|
3593
|
+
}
|
|
3594
|
+
function validUnattributed(value) {
|
|
3595
|
+
if (!record4(value) || !sameKeys(value, ["input", "cached_input", "cache_write_input", "output", "message_ids", "last_observed_at"])) return false;
|
|
3596
|
+
if (!validMessageIds(value.message_ids) || value.message_ids.length === 0) return false;
|
|
3597
|
+
if (!timestamp3(value.last_observed_at)) return false;
|
|
3598
|
+
return validTurnCounts(value);
|
|
3599
|
+
}
|
|
3600
|
+
function validTurnCounts(value) {
|
|
3601
|
+
if (!record4(value) || !sameKeys(value, ["input", "cached_input", "cache_write_input", "output"])) return false;
|
|
3602
|
+
return [value.input, value.cached_input, value.cache_write_input, value.output].every(nonNegativeInteger);
|
|
3603
|
+
}
|
|
3604
|
+
function validateIdentity3(value) {
|
|
3605
|
+
if (!record4(value) || ![value.host_session_ref, value.host_turn_ref, value.host_source_ref].every((candidate) => typeof candidate === "string" && SAFE_REF4.test(candidate))) {
|
|
3606
|
+
throw new ClaudeHostError("storage", "Claude Host identity is malformed.");
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
function nonNegativeInteger(value) {
|
|
3610
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
3611
|
+
}
|
|
3612
|
+
function sameKeys(value, required, optional = []) {
|
|
3613
|
+
return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
|
|
3614
|
+
}
|
|
2675
3615
|
|
|
2676
3616
|
// host/claude/src/claude-producer.ts
|
|
2677
|
-
import { mkdir as mkdir3, readFile as readFile2, rename as rename2, writeFile } from "node:fs/promises";
|
|
2678
|
-
import { join as join3 } from "node:path";
|
|
2679
|
-
var STATE_SCHEMA = "ceal.claude_producer_state.v1";
|
|
2680
|
-
var SAFE_REF5 = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
2681
3617
|
var ClaudeProducer = class {
|
|
3618
|
+
producer_id = "claude";
|
|
3619
|
+
producer_label = "Claude";
|
|
3620
|
+
outbox_directory;
|
|
2682
3621
|
store;
|
|
2683
|
-
|
|
2684
|
-
installation;
|
|
2685
|
-
epoch;
|
|
2686
|
-
options;
|
|
2687
|
-
tail = Promise.resolve();
|
|
3622
|
+
core;
|
|
2688
3623
|
constructor(options) {
|
|
2689
|
-
if (!SAFE_REF5.test(options.producer_installation_ref)) throw new ClaudeHostError("configuration", "Claude producer installation identity is invalid.");
|
|
2690
|
-
this.options = options;
|
|
2691
|
-
this.epoch = options.producer_epoch ?? 1;
|
|
2692
|
-
if (!Number.isSafeInteger(this.epoch) || this.epoch <= 0) throw new ClaudeHostError("configuration", "Claude producer epoch is invalid.");
|
|
2693
3624
|
this.store = new ClaudeOutboxStore(options.outbox_dir);
|
|
2694
|
-
this.
|
|
2695
|
-
this.
|
|
3625
|
+
this.outbox_directory = this.store.directory;
|
|
3626
|
+
this.core = new HostProducerDrainCore(this, options);
|
|
2696
3627
|
}
|
|
2697
3628
|
drainOutbox() {
|
|
2698
|
-
|
|
2699
|
-
this.tail = run.then(() => void 0, () => void 0);
|
|
2700
|
-
return run;
|
|
3629
|
+
return this.core.drainOutbox();
|
|
2701
3630
|
}
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
3631
|
+
fail(code2, message) {
|
|
3632
|
+
return new ClaudeHostError(code2, message);
|
|
3633
|
+
}
|
|
3634
|
+
listEnvelopes() {
|
|
3635
|
+
return this.store.listEnvelopes();
|
|
3636
|
+
}
|
|
3637
|
+
removeEnvelope(envelope) {
|
|
3638
|
+
return this.store.removeEnvelope(envelope);
|
|
3639
|
+
}
|
|
3640
|
+
submissionRefOf(envelope) {
|
|
3641
|
+
return envelope.submission_ref;
|
|
3642
|
+
}
|
|
3643
|
+
expiresAtOf(envelope) {
|
|
3644
|
+
return envelope.expires_at;
|
|
3645
|
+
}
|
|
3646
|
+
createdAtOf(envelope) {
|
|
3647
|
+
return envelope.created_at;
|
|
3648
|
+
}
|
|
3649
|
+
async mapEnvelope(envelope) {
|
|
3650
|
+
const prompt = observation(envelope, "prompt");
|
|
3651
|
+
if (prompt !== void 0) return this.mapInput(envelope, prompt);
|
|
3652
|
+
return [mapTerminal(envelope)];
|
|
3653
|
+
}
|
|
3654
|
+
async mapInput(envelope, prompt) {
|
|
3655
|
+
const drafts = [];
|
|
3656
|
+
const session = observation(envelope, "session");
|
|
3657
|
+
if (session !== void 0) {
|
|
3658
|
+
const registrations = await this.store.listTranscriptRegistrations();
|
|
3659
|
+
const registration = registrations.find((item) => item.host_source_ref === envelope.identity.host_source_ref && item.host_session_ref === envelope.identity.host_session_ref);
|
|
3660
|
+
const directoryIdentity = registration?.cwd ?? envelope.identity.host_session_ref;
|
|
3661
|
+
drafts.push({
|
|
3662
|
+
frame_kind: "session_started",
|
|
3663
|
+
payload: compact({
|
|
3664
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3665
|
+
working_directory_ref: claudeWorkspaceRef(directoryIdentity),
|
|
3666
|
+
model: recordText(session.value, "model"),
|
|
3667
|
+
observations: [session]
|
|
3668
|
+
})
|
|
3669
|
+
});
|
|
3670
|
+
}
|
|
3671
|
+
const turn = observation(envelope, "turn");
|
|
3672
|
+
if (turn !== void 0) drafts.push({
|
|
3673
|
+
frame_kind: "turn_started",
|
|
3674
|
+
payload: compact({
|
|
3675
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3676
|
+
local_turn_ref: localTurnRef(envelope),
|
|
3677
|
+
observations: [turn]
|
|
3678
|
+
})
|
|
3679
|
+
});
|
|
3680
|
+
if (!isCealHostPromptObservationValue(prompt.value)) throw new ClaudeHostError("storage", "Claude prompt observation is malformed.");
|
|
3681
|
+
drafts.push({
|
|
3682
|
+
frame_kind: "user_input",
|
|
3683
|
+
payload: compact({
|
|
3684
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3685
|
+
local_turn_ref: localTurnRef(envelope),
|
|
3686
|
+
local_input_ref: envelope.host_input_ref,
|
|
3687
|
+
input_kind: envelope.input_kind,
|
|
3688
|
+
origin_input: envelope.origin_input,
|
|
3689
|
+
observations: [prompt],
|
|
3690
|
+
context_expires_at: envelope.expires_at
|
|
3691
|
+
})
|
|
3692
|
+
});
|
|
3693
|
+
return drafts;
|
|
3694
|
+
}
|
|
3695
|
+
};
|
|
3696
|
+
function mapTerminal(envelope) {
|
|
3697
|
+
const usage = envelope.observations.filter((item) => ["token", "cost"].includes(item.observation_kind));
|
|
3698
|
+
return {
|
|
3699
|
+
frame_kind: "turn_terminal",
|
|
3700
|
+
payload: compact({
|
|
3701
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3702
|
+
local_turn_ref: localTurnRef(envelope),
|
|
3703
|
+
terminal_state: "completed",
|
|
3704
|
+
usage: usage.length === 0 ? void 0 : Object.fromEntries(usage.map((item) => [item.observation_kind, item.value])),
|
|
3705
|
+
observations: envelope.observations
|
|
3706
|
+
})
|
|
3707
|
+
};
|
|
3708
|
+
}
|
|
3709
|
+
function observation(envelope, kind) {
|
|
3710
|
+
return envelope.observations.find((item) => item.observation_kind === kind);
|
|
3711
|
+
}
|
|
3712
|
+
function localTurnRef(envelope) {
|
|
3713
|
+
return envelope.identity.host_turn_ref;
|
|
3714
|
+
}
|
|
3715
|
+
function compact(value) {
|
|
3716
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== void 0));
|
|
3717
|
+
}
|
|
3718
|
+
function asRecord(value) {
|
|
3719
|
+
return isRecord3(value) ? value : void 0;
|
|
3720
|
+
}
|
|
3721
|
+
function recordText(value, key) {
|
|
3722
|
+
const record5 = asRecord(value);
|
|
3723
|
+
return typeof record5?.[key] === "string" ? record5[key] : void 0;
|
|
3724
|
+
}
|
|
3725
|
+
function isRecord3(value) {
|
|
3726
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
// host/claude/src/claude-transcript-watcher.ts
|
|
3730
|
+
import { O_NOFOLLOW as O_NOFOLLOW2, O_RDONLY as O_RDONLY3 } from "node:constants";
|
|
3731
|
+
import { open as open4, readdir as readdir2 } from "node:fs/promises";
|
|
3732
|
+
import { join as join4 } from "node:path";
|
|
3733
|
+
var TOKEN_OBSERVATION_SOURCE = "claude_transcript.message.usage";
|
|
3734
|
+
var ClaudeTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
3735
|
+
store;
|
|
3736
|
+
registerCurrentPrompt;
|
|
3737
|
+
diagnostic;
|
|
3738
|
+
constructor(options) {
|
|
3739
|
+
super(options, {
|
|
3740
|
+
default_retention_ms: CLAUDE_DEFAULT_RETENTION_MS,
|
|
3741
|
+
invalid_outbox: () => new ClaudeHostError("configuration", "Claude transcript watcher outbox is invalid."),
|
|
3742
|
+
clock_failure: () => new ClaudeHostError("storage", "Claude watcher clock is invalid.")
|
|
3743
|
+
});
|
|
3744
|
+
this.store = new ClaudeOutboxStore(options.outbox_dir);
|
|
3745
|
+
this.registerCurrentPrompt = options.register_current_prompt;
|
|
3746
|
+
this.diagnostic = options.diagnostic ?? (() => void 0);
|
|
3747
|
+
}
|
|
3748
|
+
transcriptPathOf(registration) {
|
|
3749
|
+
return registration.transcript_path;
|
|
3750
|
+
}
|
|
3751
|
+
async listRegistrations() {
|
|
3752
|
+
return this.store.listTranscriptRegistrations();
|
|
3753
|
+
}
|
|
3754
|
+
async scanOpened(handle, registration) {
|
|
3755
|
+
const { size, device, inode } = await ownedClaudeTranscriptFile(handle);
|
|
3756
|
+
const cursor = await this.preflight(registration, handle, size, device, inode);
|
|
3757
|
+
if (cursor === null) return { records: 0, enqueued: 0, quarantined: true };
|
|
3758
|
+
if (cursor.status === "quarantined") return { records: 0, enqueued: 0, quarantined: true };
|
|
3759
|
+
const draft = mutableCursor(cursor);
|
|
3760
|
+
await this.scanOpenedTranscript(registration, handle, draft, size, device, inode);
|
|
3761
|
+
return { records: draft.consumed, enqueued: draft.enqueued, quarantined: false };
|
|
3762
|
+
}
|
|
3763
|
+
async scanFailed(handle, registration, error) {
|
|
3764
|
+
this.diagnostic("watcher_scan_failed", claudeTranscriptScanFailureKind(error));
|
|
3765
|
+
if (handle === void 0 && isSymlinkTranscript(error)) return await this.quarantineUnopened(registration, "transcript_malformed");
|
|
3766
|
+
if (error instanceof ClaudeHostError && error.code === "payload_rejected") return await this.quarantineDraft(await this.preservedCursor(registration));
|
|
3767
|
+
await this.quarantineFailure(registration, handle);
|
|
3768
|
+
return { records: 0, enqueued: 0, quarantined: true };
|
|
3769
|
+
}
|
|
3770
|
+
async retireMissing(registration) {
|
|
3771
|
+
return this.retireMissingTranscript(registration.transcript_path, "Claude", () => this.store.retireTranscriptRegistration(registration));
|
|
3772
|
+
}
|
|
3773
|
+
initialCursor(registration) {
|
|
3774
|
+
return {
|
|
3775
|
+
schema_version: CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION,
|
|
3776
|
+
host_source_ref: registration.host_source_ref,
|
|
3777
|
+
host_session_ref: registration.host_session_ref,
|
|
3778
|
+
transcript_path: registration.transcript_path,
|
|
3779
|
+
offset: 0,
|
|
3780
|
+
anchor_start: 0,
|
|
3781
|
+
anchor_sha256: this.sha(Buffer.alloc(0)),
|
|
3782
|
+
subagents: {},
|
|
3783
|
+
status: "active",
|
|
3784
|
+
updated_at: this.stamp()
|
|
3785
|
+
};
|
|
3786
|
+
}
|
|
3787
|
+
async scanOpenedTranscript(registration, handle, draft, size, device, inode) {
|
|
3788
|
+
const main2 = await this.readChunk(handle, draft.offset, size);
|
|
3789
|
+
if (main2 !== "empty" && main2 !== "partial") {
|
|
3790
|
+
await this.consumeMainChunk(registration, main2, draft);
|
|
3791
|
+
draft.offset += main2.length;
|
|
3792
|
+
}
|
|
3793
|
+
draft.consumed += await this.consumeSubagents(registration, draft);
|
|
3794
|
+
await this.emitOpenTerminal(registration, draft);
|
|
3795
|
+
await this.commitCursor(registration, handle, draft, device, inode);
|
|
3796
|
+
}
|
|
3797
|
+
async preflight(registration, handle, size, device, inode) {
|
|
3798
|
+
const existing = await this.store.readTranscriptCursor(registration);
|
|
3799
|
+
if (existing?.status === "quarantined") return existing;
|
|
3800
|
+
const cursor = existing ?? this.initialCursor(registration);
|
|
3801
|
+
if (cursor.device === void 0) {
|
|
3802
|
+
if (cursor.offset !== 0) return await this.quarantineStored(cursor, device, inode, "transcript_replaced");
|
|
3803
|
+
return { ...cursor, device, inode };
|
|
3804
|
+
}
|
|
3805
|
+
const reason = this.cursorIdentityReason(cursor, registration.transcript_path, device, inode) ?? await this.cursorBoundsReason(handle, cursor, size);
|
|
3806
|
+
if (reason !== null) return await this.quarantineStored(cursor, device, inode, reason);
|
|
3807
|
+
return cursor;
|
|
3808
|
+
}
|
|
3809
|
+
async consumeMainChunk(registration, complete, draft) {
|
|
3810
|
+
for (const line of complete.toString("utf8").split("\n").filter((value) => value.length > 0)) {
|
|
3811
|
+
draft.consumed += 1;
|
|
3812
|
+
const record5 = parseTranscriptLine(line);
|
|
3813
|
+
if (record5 === null) throw new ClaudeHostError("payload_rejected", "Claude transcript line is not valid JSON.", "malformed_line");
|
|
3814
|
+
if (isUserTurnLine(record5)) await this.openTurn(registration, draft, record5);
|
|
3815
|
+
else if (isAssistantUsageLine(record5)) this.accumulateUsage(draft, toUsage(record5.uuid, record5.timestamp, record5.message.usage));
|
|
3816
|
+
else if (isAssistantLine(record5) && record5.message.usage !== void 0) throw new ClaudeHostError("payload_rejected", "Claude transcript usage is malformed.");
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
async openTurn(registration, draft, record5) {
|
|
3820
|
+
this.diagnostic("watcher_turn_observed");
|
|
3821
|
+
await this.closeTurn(registration, draft, record5.timestamp);
|
|
3822
|
+
const sessionRef = registration.host_session_ref;
|
|
3823
|
+
const turnRef = claudeLocalTurnRef(registration.claude_session_id, record5.uuid);
|
|
3824
|
+
const inputRef = claudeLocalInputRef(registration.claude_session_id, record5.uuid);
|
|
3825
|
+
const promptRef = hostProducerPromptRef(this.sha(record5.message.content));
|
|
3826
|
+
const identity = { host_source_ref: registration.host_source_ref, host_session_ref: sessionRef, host_turn_ref: turnRef };
|
|
3827
|
+
const observations = [
|
|
3828
|
+
{ observation_ref: observationRef("transcript_user_text", inputRef, record5.message.content), observation_kind: "prompt", observed_at: record5.timestamp, value: { body_sha256: this.sha(record5.message.content) } },
|
|
3829
|
+
{ observation_ref: observationRef("transcript_turn_started", turnRef, record5.uuid), observation_kind: "turn", observed_at: record5.timestamp, value: { status: "started", host_session_id_sha256: this.sha(registration.claude_session_id), host_turn_ref: turnRef } }
|
|
3830
|
+
];
|
|
3831
|
+
if (!draft.session_observed) {
|
|
3832
|
+
const session = await this.store.readSession(registration.host_source_ref, sessionRef);
|
|
3833
|
+
if (session !== null) observations.push({ observation_ref: observationRef("transcript_session", sessionRef, session.value), observation_kind: "session", observed_at: record5.timestamp, value: session.value });
|
|
3834
|
+
draft.session_observed = true;
|
|
3835
|
+
}
|
|
3836
|
+
await this.store.putEnvelope({
|
|
3837
|
+
schema_version: "ceal.claude_observation_outbox.v1",
|
|
3838
|
+
identity,
|
|
3839
|
+
host_input_ref: inputRef,
|
|
3840
|
+
input_kind: "human",
|
|
3841
|
+
prompt_ref: promptRef,
|
|
3842
|
+
observations,
|
|
3843
|
+
origin_input: true,
|
|
3844
|
+
submission_ref: submissionRef("input", turnRef, inputRef, promptRef),
|
|
3845
|
+
created_at: record5.timestamp,
|
|
3846
|
+
expires_at: this.expire(this.outboxRetentionMs)
|
|
3847
|
+
});
|
|
3848
|
+
draft.enqueued += 1;
|
|
3849
|
+
await this.registerPrompt(sessionRef, turnRef, promptRef, record5.message.content);
|
|
3850
|
+
const selection = await this.store.readInvocationTurnSelection(identity.host_source_ref, identity.host_session_ref);
|
|
3851
|
+
const turnGenerationRef = selection?.prompt_ref === promptRef ? selection.turn_generation_ref : void 0;
|
|
3852
|
+
await this.activateContext(identity, promptRef, turnGenerationRef);
|
|
3853
|
+
const folded = foldUnattributed(draft.unattributed);
|
|
3854
|
+
draft.open_turn = {
|
|
3855
|
+
turn_key: turnRef,
|
|
3856
|
+
user_uuid: record5.uuid,
|
|
3857
|
+
prompt_ref: promptRef,
|
|
3858
|
+
host_input_ref: inputRef,
|
|
3859
|
+
started_at: record5.timestamp,
|
|
3860
|
+
last_observed_at: folded?.last_observed_at ?? record5.timestamp,
|
|
3861
|
+
message_ids: folded?.message_ids ?? [],
|
|
3862
|
+
counts: folded?.counts ?? zeroCounts()
|
|
3863
|
+
};
|
|
3864
|
+
draft.unattributed = void 0;
|
|
3865
|
+
}
|
|
3866
|
+
async closeTurn(registration, draft, at) {
|
|
3867
|
+
const turn = draft.open_turn;
|
|
3868
|
+
draft.open_turn = void 0;
|
|
3869
|
+
if (turn === void 0) return;
|
|
3870
|
+
const digest2 = terminalDigest(turn);
|
|
3871
|
+
if (digest2 === turn.last_terminal_digest) return;
|
|
3872
|
+
await this.putTerminalEnvelope(registration, turn, digest2, turn.message_ids.length === 0 ? at : void 0);
|
|
3873
|
+
draft.enqueued += 1;
|
|
3874
|
+
}
|
|
3875
|
+
accumulateUsage(draft, usage) {
|
|
3876
|
+
const turn = draft.open_turn;
|
|
3877
|
+
if (turn === void 0) {
|
|
3878
|
+
draft.unattributed = accumulateUnattributed(draft.unattributed, usage);
|
|
3879
|
+
return;
|
|
3880
|
+
}
|
|
3881
|
+
if (turn.message_ids.includes(usage.uuid)) return;
|
|
3882
|
+
if (turn.message_ids.length >= CLAUDE_TRANSCRIPT_MAX_TURN_MESSAGES) throw new ClaudeHostError("payload_rejected", "Claude transcript turn exceeds its usage message bound.", "size_budget");
|
|
3883
|
+
draft.open_turn = {
|
|
3884
|
+
...turn,
|
|
3885
|
+
last_observed_at: usage.observed_at > turn.last_observed_at ? usage.observed_at : turn.last_observed_at,
|
|
3886
|
+
message_ids: [...turn.message_ids, usage.uuid],
|
|
3887
|
+
counts: addCounts(turn.counts, usage)
|
|
3888
|
+
};
|
|
3889
|
+
}
|
|
3890
|
+
/**
|
|
3891
|
+
* Subagent transcripts live beside the session transcript at
|
|
3892
|
+
* `<session-id>/subagents/*.jsonl` with a `.meta.json` sibling the scan
|
|
3893
|
+
* ignores. Their usage belongs to the turn open when the scan runs: a
|
|
3894
|
+
* subagent only runs while its parent turn is in flight. A straggler line
|
|
3895
|
+
* that lands after the next user message therefore attributes to the open
|
|
3896
|
+
* turn, never to a closed one.
|
|
3897
|
+
*/
|
|
3898
|
+
async consumeSubagents(registration, draft) {
|
|
3899
|
+
const directory = subagentsDirectory(registration.transcript_path);
|
|
3900
|
+
let names;
|
|
3901
|
+
try {
|
|
3902
|
+
names = (await readdir2(directory)).filter((name) => name.endsWith(".jsonl") && !name.endsWith(".meta.json")).sort();
|
|
3903
|
+
} catch (error) {
|
|
3904
|
+
if (isMissingTranscriptError(error) || isNotDirectory(error)) return 0;
|
|
3905
|
+
throw new ClaudeHostError("payload_rejected", "Claude subagent transcript directory is unavailable.");
|
|
3906
|
+
}
|
|
3907
|
+
if (names.length > CLAUDE_TRANSCRIPT_MAX_SUBAGENT_FILES) {
|
|
3908
|
+
this.log(`Claude ignored subagent transcripts past its file bound: transcript_path=${JSON.stringify(registration.transcript_path)}, files=${names.length}.`);
|
|
3909
|
+
names = names.slice(0, CLAUDE_TRANSCRIPT_MAX_SUBAGENT_FILES);
|
|
3910
|
+
}
|
|
3911
|
+
let consumed = 0;
|
|
3912
|
+
for (const name of names) {
|
|
3913
|
+
consumed += await this.consumeSubagentFile(draft, directory, name);
|
|
3914
|
+
}
|
|
3915
|
+
return consumed;
|
|
3916
|
+
}
|
|
3917
|
+
async consumeSubagentFile(draft, directory, name) {
|
|
3918
|
+
const path = join4(directory, name);
|
|
3919
|
+
let handle;
|
|
3920
|
+
try {
|
|
3921
|
+
handle = await open4(path, O_RDONLY3 | O_NOFOLLOW2);
|
|
3922
|
+
const { size, device, inode } = await ownedClaudeTranscriptFile(handle);
|
|
3923
|
+
const entry = draft.subagents[name] ?? { offset: 0, anchor_start: 0, anchor_sha256: this.sha(Buffer.alloc(0)) };
|
|
3924
|
+
const pinned = await this.preflightSubagent(entry, size, device, inode);
|
|
3925
|
+
const chunk = await this.readChunk(handle, pinned.offset, size);
|
|
3926
|
+
if (chunk === "empty" || chunk === "partial") {
|
|
3927
|
+
draft.subagents[name] = pinned;
|
|
3928
|
+
return 0;
|
|
2715
3929
|
}
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
3930
|
+
const consumed = this.consumeSubagentLines(draft, chunk);
|
|
3931
|
+
const offset = pinned.offset + chunk.length;
|
|
3932
|
+
const anchor = await this.readAnchor(handle, offset, CLAUDE_TRANSCRIPT_ANCHOR_BYTES);
|
|
3933
|
+
draft.subagents[name] = { device, inode, offset, anchor_start: anchor.start, anchor_sha256: anchor.sha256 };
|
|
3934
|
+
return consumed;
|
|
3935
|
+
} catch (error) {
|
|
3936
|
+
if (isMissingTranscriptError(error)) {
|
|
3937
|
+
delete draft.subagents[name];
|
|
3938
|
+
return 0;
|
|
2719
3939
|
}
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
3940
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("payload_rejected", "Claude subagent transcript is unavailable.");
|
|
3941
|
+
} finally {
|
|
3942
|
+
await handle?.close();
|
|
2723
3943
|
}
|
|
2724
|
-
return { admitted, retained: retained + Math.max(0, read.entries.length - admitted - retained), malformed: read.malformed };
|
|
2725
3944
|
}
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
const
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
3945
|
+
consumeSubagentLines(draft, chunk) {
|
|
3946
|
+
let consumed = 0;
|
|
3947
|
+
for (const line of chunk.toString("utf8").split("\n").filter((value) => value.length > 0)) {
|
|
3948
|
+
consumed += 1;
|
|
3949
|
+
const record5 = parseTranscriptLine(line);
|
|
3950
|
+
if (record5 === null) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript line is not valid JSON.", "malformed_line");
|
|
3951
|
+
if (isAssistantUsageLine(record5)) this.accumulateUsage(draft, toUsage(record5.uuid, record5.timestamp, record5.message.usage));
|
|
3952
|
+
else if (isAssistantLine(record5) && record5.message.usage !== void 0) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript usage is malformed.");
|
|
3953
|
+
}
|
|
3954
|
+
return consumed;
|
|
2733
3955
|
}
|
|
2734
|
-
async
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
3956
|
+
async preflightSubagent(entry, size, device, inode) {
|
|
3957
|
+
if (entry.device === void 0) {
|
|
3958
|
+
if (entry.offset !== 0) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript cursor is unpinned past its start.");
|
|
3959
|
+
return { device, inode, offset: 0, anchor_start: 0, anchor_sha256: this.sha(Buffer.alloc(0)) };
|
|
3960
|
+
}
|
|
3961
|
+
if (entry.device !== device || entry.inode !== inode) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript was replaced.");
|
|
3962
|
+
if (size < entry.offset) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript was truncated.");
|
|
3963
|
+
return { device, inode, offset: entry.offset, anchor_start: entry.anchor_start, anchor_sha256: entry.anchor_sha256 };
|
|
2740
3964
|
}
|
|
2741
|
-
|
|
3965
|
+
/**
|
|
3966
|
+
* The open turn's terminal is emitted optimistically on every scan whose
|
|
3967
|
+
* new lines changed its totals, so usage is visible before the next user
|
|
3968
|
+
* message closes the turn. The emission digest changes with the content,
|
|
3969
|
+
* so a repeated scan over unchanged state emits nothing.
|
|
3970
|
+
*/
|
|
3971
|
+
async emitOpenTerminal(registration, draft) {
|
|
3972
|
+
const turn = draft.open_turn;
|
|
3973
|
+
if (turn === void 0 || turn.message_ids.length === 0) return;
|
|
3974
|
+
const digest2 = terminalDigest(turn);
|
|
3975
|
+
if (digest2 === turn.last_terminal_digest) return;
|
|
3976
|
+
await this.putTerminalEnvelope(registration, turn, digest2, void 0);
|
|
3977
|
+
draft.enqueued += 1;
|
|
3978
|
+
draft.open_turn = { ...turn, last_terminal_digest: digest2 };
|
|
3979
|
+
}
|
|
3980
|
+
async putTerminalEnvelope(registration, turn, digest2, gapAt) {
|
|
3981
|
+
const observedAt = gapAt ?? turn.last_observed_at;
|
|
3982
|
+
const tokenValue = turn.message_ids.length === 0 ? hostObservationNotRecorded("claude_turn_usage_not_observed") : availableTokenValue(turn.counts);
|
|
3983
|
+
const identity = { host_source_ref: registration.host_source_ref, host_session_ref: registration.host_session_ref, host_turn_ref: turn.turn_key };
|
|
3984
|
+
await this.store.putEnvelope({
|
|
3985
|
+
schema_version: "ceal.claude_observation_outbox.v1",
|
|
3986
|
+
identity,
|
|
3987
|
+
host_input_ref: turn.host_input_ref,
|
|
3988
|
+
input_kind: "host_automatic",
|
|
3989
|
+
prompt_ref: turn.prompt_ref,
|
|
3990
|
+
observations: [
|
|
3991
|
+
{ observation_ref: observationRef("transcript_terminal", turn.turn_key, digest2), observation_kind: "turn", observed_at: observedAt, value: { status: "stopped", host_session_id_sha256: this.sha(registration.claude_session_id), host_turn_ref: turn.turn_key } },
|
|
3992
|
+
{ observation_ref: observationRef("transcript_token", turn.turn_key, digest2), observation_kind: "token", observed_at: observedAt, value: tokenValue },
|
|
3993
|
+
{ observation_ref: observationRef("transcript_cost", turn.turn_key, digest2), observation_kind: "cost", observed_at: observedAt, value: hostObservationNotRecorded("provider_cost_not_observed") }
|
|
3994
|
+
],
|
|
3995
|
+
origin_input: false,
|
|
3996
|
+
submission_ref: submissionRef("terminal", turn.turn_key, digest2, turn.prompt_ref),
|
|
3997
|
+
created_at: observedAt,
|
|
3998
|
+
expires_at: this.expire(this.outboxRetentionMs)
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
async commitCursor(registration, handle, draft, device, inode) {
|
|
4002
|
+
const anchor = await this.readAnchor(handle, draft.offset, CLAUDE_TRANSCRIPT_ANCHOR_BYTES);
|
|
4003
|
+
await this.store.writeTranscriptCursor({
|
|
4004
|
+
schema_version: CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION,
|
|
4005
|
+
host_source_ref: registration.host_source_ref,
|
|
4006
|
+
host_session_ref: registration.host_session_ref,
|
|
4007
|
+
transcript_path: registration.transcript_path,
|
|
4008
|
+
device,
|
|
4009
|
+
inode,
|
|
4010
|
+
offset: draft.offset,
|
|
4011
|
+
anchor_start: anchor.start,
|
|
4012
|
+
anchor_sha256: anchor.sha256,
|
|
4013
|
+
subagents: draft.subagents,
|
|
4014
|
+
...draft.open_turn === void 0 ? {} : { open_turn: draft.open_turn },
|
|
4015
|
+
...draft.unattributed === void 0 ? {} : { unattributed: draft.unattributed },
|
|
4016
|
+
...draft.session_observed ? { session_observed: true } : {},
|
|
4017
|
+
status: "active",
|
|
4018
|
+
updated_at: this.stamp()
|
|
4019
|
+
});
|
|
4020
|
+
}
|
|
4021
|
+
async registerPrompt(sessionRef, turnRef, promptRef, prompt) {
|
|
4022
|
+
if (this.producerInstallationRef === void 0 || this.registerCurrentPrompt === void 0) return;
|
|
2742
4023
|
try {
|
|
2743
|
-
|
|
4024
|
+
await this.registerCurrentPrompt({
|
|
4025
|
+
producer_id: "claude",
|
|
4026
|
+
producer_installation_ref: this.producerInstallationRef,
|
|
4027
|
+
local_session_ref: sessionRef,
|
|
4028
|
+
local_turn_ref: turnRef,
|
|
4029
|
+
prompt_ref: promptRef,
|
|
4030
|
+
current_prompt: prompt,
|
|
4031
|
+
context_expires_at: this.expire(this.outboxRetentionMs)
|
|
4032
|
+
});
|
|
4033
|
+
this.diagnostic("current_prompt_registered");
|
|
2744
4034
|
} catch (error) {
|
|
2745
|
-
|
|
2746
|
-
|
|
4035
|
+
this.diagnostic("current_prompt_registration_failed");
|
|
4036
|
+
throw error;
|
|
2747
4037
|
}
|
|
2748
4038
|
}
|
|
2749
|
-
async
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
4039
|
+
async activateContext(identity, promptRef, turnGenerationRef) {
|
|
4040
|
+
try {
|
|
4041
|
+
await this.store.activateInvocationContext(identity, promptRef, this.expire(this.outboxRetentionMs), void 0, turnGenerationRef);
|
|
4042
|
+
this.diagnostic("activation_recorded");
|
|
4043
|
+
} catch (error) {
|
|
4044
|
+
this.diagnostic("activation_failed");
|
|
4045
|
+
throw error;
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
async quarantineStored(cursor, device, inode, reason) {
|
|
4049
|
+
await this.store.writeTranscriptCursor({ ...cursor, device, inode, status: "quarantined", reason, updated_at: this.stamp() });
|
|
4050
|
+
return null;
|
|
4051
|
+
}
|
|
4052
|
+
async quarantineUnopened(registration, reason) {
|
|
4053
|
+
const existing = await this.store.readTranscriptCursor(registration).catch(() => null);
|
|
4054
|
+
await this.store.writeTranscriptCursor({ ...existing ?? this.initialCursor(registration), transcript_path: registration.transcript_path, status: "quarantined", reason, updated_at: this.stamp() });
|
|
4055
|
+
return { records: 0, enqueued: 0, quarantined: true };
|
|
4056
|
+
}
|
|
4057
|
+
async quarantineDraft(cursor) {
|
|
4058
|
+
await this.store.writeTranscriptCursor({ ...cursor, status: "quarantined", reason: "transcript_malformed", updated_at: this.stamp() });
|
|
4059
|
+
return { records: 0, enqueued: 0, quarantined: true };
|
|
4060
|
+
}
|
|
4061
|
+
async quarantineFailure(registration, handle) {
|
|
4062
|
+
if (handle === void 0) return;
|
|
4063
|
+
const stats = await handle.stat().catch(() => null);
|
|
4064
|
+
if (stats === null) return;
|
|
4065
|
+
const existing = await this.store.readTranscriptCursor(registration).catch(() => null);
|
|
4066
|
+
const cursor = existing ?? this.initialCursor(registration);
|
|
4067
|
+
await this.store.writeTranscriptCursor({ ...cursor, device: String(stats.dev), inode: String(stats.ino), status: "quarantined", reason: "transcript_malformed", updated_at: this.stamp() });
|
|
4068
|
+
}
|
|
4069
|
+
async preservedCursor(registration) {
|
|
4070
|
+
return await this.store.readTranscriptCursor(registration).catch(() => null) ?? this.initialCursor(registration);
|
|
2754
4071
|
}
|
|
2755
4072
|
};
|
|
2756
|
-
function
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
}
|
|
2767
|
-
return { schema_version: STATE_SCHEMA, producer_installation_ref: installation, producer_epoch: epoch, next_sequence: Number(value.next_sequence), pending };
|
|
2768
|
-
}
|
|
2769
|
-
function validStateHeader(value, installation) {
|
|
2770
|
-
return record5(value) && value.schema_version === STATE_SCHEMA && value.producer_installation_ref === installation && positiveInteger(value.producer_epoch) && positiveInteger(value.next_sequence) && record5(value.pending);
|
|
4073
|
+
function mutableCursor(cursor) {
|
|
4074
|
+
return {
|
|
4075
|
+
offset: cursor.offset,
|
|
4076
|
+
...cursor.open_turn === void 0 ? {} : { open_turn: cursor.open_turn },
|
|
4077
|
+
...cursor.unattributed === void 0 ? {} : { unattributed: cursor.unattributed },
|
|
4078
|
+
subagents: Object.fromEntries(Object.entries(cursor.subagents).map(([name, entry]) => [name, { ...entry }])),
|
|
4079
|
+
session_observed: cursor.session_observed ?? false,
|
|
4080
|
+
consumed: 0,
|
|
4081
|
+
enqueued: 0
|
|
4082
|
+
};
|
|
2771
4083
|
}
|
|
2772
|
-
function
|
|
2773
|
-
return
|
|
4084
|
+
function zeroCounts() {
|
|
4085
|
+
return { input: 0, cached_input: 0, cache_write_input: 0, output: 0 };
|
|
4086
|
+
}
|
|
4087
|
+
function terminalDigest(turn) {
|
|
4088
|
+
return transcriptObjectSha256({ turn: turn.turn_key, messages: turn.message_ids, counts: turn.counts });
|
|
4089
|
+
}
|
|
4090
|
+
function availableTokenValue(counts) {
|
|
4091
|
+
const input = counts.input;
|
|
4092
|
+
const output = counts.output;
|
|
4093
|
+
if (!Number.isSafeInteger(input + output)) throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
4094
|
+
return createHostTokenObservationValue({
|
|
4095
|
+
input,
|
|
4096
|
+
output,
|
|
4097
|
+
total: input + output,
|
|
4098
|
+
cached_input: counts.cached_input,
|
|
4099
|
+
cache_write_input: counts.cache_write_input
|
|
4100
|
+
}, TOKEN_OBSERVATION_SOURCE);
|
|
4101
|
+
}
|
|
4102
|
+
function addCounts(current, next) {
|
|
4103
|
+
const counts = {
|
|
4104
|
+
input: current.input + next.input,
|
|
4105
|
+
cached_input: current.cached_input + next.cached_input,
|
|
4106
|
+
cache_write_input: current.cache_write_input + next.cache_write_input,
|
|
4107
|
+
output: current.output + next.output
|
|
4108
|
+
};
|
|
4109
|
+
if (![counts.input, counts.cached_input, counts.cache_write_input, counts.output].every((value) => Number.isSafeInteger(value) && value >= 0)) {
|
|
4110
|
+
throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
4111
|
+
}
|
|
4112
|
+
return counts;
|
|
2774
4113
|
}
|
|
2775
|
-
function
|
|
2776
|
-
const
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
4114
|
+
function accumulateUnattributed(current, usage) {
|
|
4115
|
+
const base = current ?? { input: 0, cached_input: 0, cache_write_input: 0, output: 0, message_ids: [], last_observed_at: usage.observed_at };
|
|
4116
|
+
if (base.message_ids.includes(usage.uuid)) return base;
|
|
4117
|
+
if (base.message_ids.length >= CLAUDE_TRANSCRIPT_MAX_TURN_MESSAGES) throw new ClaudeHostError("payload_rejected", "Claude transcript turn exceeds its usage message bound.", "size_budget");
|
|
4118
|
+
const counts = addCounts(base, usage);
|
|
4119
|
+
return {
|
|
4120
|
+
...counts,
|
|
4121
|
+
message_ids: [...base.message_ids, usage.uuid],
|
|
4122
|
+
last_observed_at: usage.observed_at > base.last_observed_at ? usage.observed_at : base.last_observed_at
|
|
4123
|
+
};
|
|
2783
4124
|
}
|
|
2784
|
-
function
|
|
2785
|
-
|
|
4125
|
+
function foldUnattributed(bucket) {
|
|
4126
|
+
if (bucket === void 0) return void 0;
|
|
4127
|
+
if (![bucket.input, bucket.cached_input, bucket.cache_write_input, bucket.output].every((value) => Number.isSafeInteger(value) && value >= 0)) {
|
|
4128
|
+
throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
4129
|
+
}
|
|
4130
|
+
return { message_ids: [...bucket.message_ids], counts: { input: bucket.input, cached_input: bucket.cached_input, cache_write_input: bucket.cache_write_input, output: bucket.output }, last_observed_at: bucket.last_observed_at };
|
|
4131
|
+
}
|
|
4132
|
+
function toUsage(uuid, observedAt, usage) {
|
|
4133
|
+
const input = usageNumber(usage.input_tokens);
|
|
4134
|
+
const cached = usageNumber(usage.cache_read_input_tokens ?? 0);
|
|
4135
|
+
const created = usageNumber(usage.cache_creation_input_tokens ?? 0);
|
|
4136
|
+
const output = usageNumber(usage.output_tokens);
|
|
4137
|
+
if (input === null || cached === null || created === null || output === null) throw new ClaudeHostError("payload_rejected", "Claude transcript usage is malformed.");
|
|
4138
|
+
const total = input + cached + created;
|
|
4139
|
+
if (![total, output].every((value) => Number.isSafeInteger(value) && value >= 0)) throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
4140
|
+
return { uuid, observed_at: observedAt, input: total, cached_input: cached, cache_write_input: created, output };
|
|
4141
|
+
}
|
|
4142
|
+
function usageNumber(value) {
|
|
4143
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
4144
|
+
}
|
|
4145
|
+
function parseTranscriptLine(line) {
|
|
4146
|
+
try {
|
|
4147
|
+
return JSON.parse(line);
|
|
4148
|
+
} catch {
|
|
4149
|
+
return null;
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
function lineIdentity(record5) {
|
|
4153
|
+
if (typeof record5.uuid !== "string" || record5.uuid.length === 0 || record5.uuid.length > 256) return null;
|
|
4154
|
+
if (typeof record5.timestamp !== "string" || !Number.isFinite(Date.parse(record5.timestamp))) return null;
|
|
4155
|
+
return { uuid: record5.uuid, timestamp: record5.timestamp };
|
|
4156
|
+
}
|
|
4157
|
+
function isUserTurnLine(record5) {
|
|
4158
|
+
if (!isTranscriptRecord(record5) || record5.type !== "user") return false;
|
|
4159
|
+
if (!isTranscriptRecord(record5.message) || typeof record5.message.content !== "string") return false;
|
|
4160
|
+
if (record5.isMeta === true && (!isTranscriptRecord(record5.origin) || typeof record5.origin.kind !== "string")) return false;
|
|
4161
|
+
const identity = lineIdentity(record5);
|
|
4162
|
+
if (identity === null) throw new ClaudeHostError("payload_rejected", "Claude transcript user line is malformed.");
|
|
4163
|
+
return true;
|
|
4164
|
+
}
|
|
4165
|
+
function claudeTranscriptScanFailureKind(error) {
|
|
4166
|
+
if (isSymlinkTranscript(error)) return "symlink";
|
|
4167
|
+
if (error instanceof ClaudeHostError) {
|
|
4168
|
+
if (error.diagnostic_kind !== void 0) return error.diagnostic_kind;
|
|
4169
|
+
if (error.code === "payload_rejected") return "payload_rejected";
|
|
4170
|
+
if (error.code === "storage") return "read_error";
|
|
4171
|
+
}
|
|
4172
|
+
const code2 = transcriptErrorCode(error);
|
|
4173
|
+
if (code2 === "EFBIG") return "size_budget";
|
|
4174
|
+
if (["EBADF", "EACCES", "EIO", "EMFILE", "ENFILE", "ENOENT", "ENOTDIR", "EPERM"].includes(code2 ?? "")) return "read_error";
|
|
4175
|
+
return "other";
|
|
4176
|
+
}
|
|
4177
|
+
function isAssistantLine(record5) {
|
|
4178
|
+
return isTranscriptRecord(record5) && record5.type === "assistant" && isTranscriptRecord(record5.message);
|
|
4179
|
+
}
|
|
4180
|
+
function isAssistantUsageLine(record5) {
|
|
4181
|
+
if (!isTranscriptRecord(record5)) return false;
|
|
4182
|
+
if (!isAssistantLine(record5) || record5.message.usage === void 0) return false;
|
|
4183
|
+
const identity = lineIdentity(record5);
|
|
4184
|
+
if (identity === null || !isTranscriptRecord(record5.message.usage)) throw new ClaudeHostError("payload_rejected", "Claude transcript usage line is malformed.");
|
|
4185
|
+
return true;
|
|
4186
|
+
}
|
|
4187
|
+
function subagentsDirectory(transcriptPath) {
|
|
4188
|
+
const stem = transcriptPath.endsWith(".jsonl") ? transcriptPath.slice(0, -".jsonl".length) : transcriptPath;
|
|
4189
|
+
return join4(stem, "subagents");
|
|
4190
|
+
}
|
|
4191
|
+
function observationRef(event, identity, value) {
|
|
4192
|
+
return cealHostObservationRef("claude", transcriptObjectSha256({ event, identity, value }));
|
|
4193
|
+
}
|
|
4194
|
+
function submissionRef(kind, turn, digest2, prompt) {
|
|
4195
|
+
return `claude-submission:${transcriptObjectSha256({ kind, turn, digest: digest2, prompt })}`;
|
|
4196
|
+
}
|
|
4197
|
+
async function ownedClaudeTranscriptFile(handle) {
|
|
4198
|
+
const stats = await handle.stat();
|
|
4199
|
+
const uid = typeof process.geteuid === "function" ? process.geteuid() : stats.uid;
|
|
4200
|
+
if (!stats.isFile()) throw new ClaudeHostError("payload_rejected", "Claude transcript source is not a regular file.");
|
|
4201
|
+
if (stats.uid !== uid) throw new ClaudeHostError("payload_rejected", "Claude transcript source is owned by another user.");
|
|
4202
|
+
return { size: stats.size, device: String(stats.dev), inode: String(stats.ino) };
|
|
4203
|
+
}
|
|
4204
|
+
function isNotDirectory(error) {
|
|
4205
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOTDIR";
|
|
2786
4206
|
}
|
|
2787
|
-
function
|
|
2788
|
-
return
|
|
4207
|
+
function isSymlinkTranscript(error) {
|
|
4208
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ELOOP";
|
|
4209
|
+
}
|
|
4210
|
+
function transcriptErrorCode(error) {
|
|
4211
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
2789
4212
|
}
|
|
2790
4213
|
|
|
2791
4214
|
// host/claude/src/claude-producer-runtime.ts
|
|
2792
4215
|
async function drainClaudeProducerOnce(options) {
|
|
2793
|
-
|
|
4216
|
+
const promptPort = new HostRelayCurrentPromptPort(options.relay_endpoint, options.timeout_ms);
|
|
4217
|
+
const scan = await new ClaudeTranscriptWatcher({
|
|
4218
|
+
outbox_dir: options.outbox_dir,
|
|
4219
|
+
producer_installation_ref: options.producer_installation_ref,
|
|
4220
|
+
register_current_prompt: (input) => promptPort.register(input),
|
|
4221
|
+
diagnostic: (event, failure_kind) => writeHostServiceDiagnostic("claude", event, failure_kind)
|
|
4222
|
+
}).scanOnce().catch((error) => {
|
|
4223
|
+
writeHostServiceDiagnostic("claude", "watcher_scan_failed", claudeTranscriptScanFailureKind(error));
|
|
4224
|
+
throw error;
|
|
4225
|
+
});
|
|
4226
|
+
const producer = new ClaudeProducer({ outbox_dir: options.outbox_dir, producer_installation_ref: options.producer_installation_ref, relay: new HostRelayIpcAdmissionPort(options.relay_endpoint, options.timeout_ms), ...options.producer_epoch === void 0 ? {} : { producer_epoch: options.producer_epoch }, ...options.now === void 0 ? {} : { now: options.now } });
|
|
4227
|
+
return { scanned: scan.enqueued, ...await producer.drainOutbox() };
|
|
2794
4228
|
}
|
|
2795
4229
|
|
|
2796
4230
|
// host/claude/src/claude-producer-service.ts
|
|
2797
4231
|
var CLAUDE_PRODUCER_CONFIG_SCHEMA = "ceal.claude_producer_config.v1";
|
|
4232
|
+
var CLAUDE_TRANSCRIPTS_ROOT = "claude_transcripts_root";
|
|
2798
4233
|
var CLAUDE_PRODUCER_DESCRIPTOR = {
|
|
2799
4234
|
schema: CLAUDE_PRODUCER_CONFIG_SCHEMA,
|
|
2800
4235
|
producer_id: "claude",
|
|
2801
|
-
|
|
4236
|
+
extra_paths: [CLAUDE_TRANSCRIPTS_ROOT],
|
|
4237
|
+
config: (shared, extras) => ({ ...shared, claude_transcripts_root: hostProducerExtraPath(extras, CLAUDE_TRANSCRIPTS_ROOT) }),
|
|
2802
4238
|
runtime_options: (config) => Object.freeze({
|
|
2803
4239
|
outbox_dir: config.outbox_dir,
|
|
2804
4240
|
producer_installation_ref: config.producer_installation_ref,
|
|
@@ -2809,30 +4245,70 @@ var CLAUDE_PRODUCER_DESCRIPTOR = {
|
|
|
2809
4245
|
drain: drainClaudeProducerOnce
|
|
2810
4246
|
};
|
|
2811
4247
|
|
|
4248
|
+
// host/claude/src/claude-visible-output-budget.ts
|
|
4249
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
4250
|
+
import { homedir } from "node:os";
|
|
4251
|
+
import { join as join5 } from "node:path";
|
|
4252
|
+
var CLAUDE_DEFAULT_BASH_OUTPUT_MAX_CHARS = 3e4;
|
|
4253
|
+
var CLAUDE_MAX_INLINE_OUTPUT_MAX_CHARS = 128e3;
|
|
4254
|
+
async function resolveClaudeVisibleOutputMaxBytes(environment = process.env, workingDirectory = process.cwd()) {
|
|
4255
|
+
const configured = await projectOrUserInlineLimit(environment, workingDirectory);
|
|
4256
|
+
if (configured !== void 0) return configured;
|
|
4257
|
+
const bashReadback = positiveInteger(environment.BASH_MAX_OUTPUT_LENGTH);
|
|
4258
|
+
return Math.min(bashReadback ?? CLAUDE_DEFAULT_BASH_OUTPUT_MAX_CHARS, CLAUDE_DEFAULT_BASH_OUTPUT_MAX_CHARS);
|
|
4259
|
+
}
|
|
4260
|
+
async function projectOrUserInlineLimit(environment, workingDirectory) {
|
|
4261
|
+
const configHome = environment.CLAUDE_CONFIG_DIR ?? join5(homedir(), ".claude");
|
|
4262
|
+
const userLimit = await settingsInlineLimit(join5(configHome, "settings.json"));
|
|
4263
|
+
const projectLimit = await settingsInlineLimit(join5(workingDirectory, ".claude", "settings.json"));
|
|
4264
|
+
const localLimit = await settingsInlineLimit(join5(workingDirectory, ".claude", "settings.local.json"));
|
|
4265
|
+
const candidate = localLimit ?? projectLimit ?? userLimit;
|
|
4266
|
+
return candidate === void 0 ? void 0 : Math.min(candidate, CLAUDE_MAX_INLINE_OUTPUT_MAX_CHARS);
|
|
4267
|
+
}
|
|
4268
|
+
async function settingsInlineLimit(path) {
|
|
4269
|
+
try {
|
|
4270
|
+
const parsed = JSON.parse(await readFile3(path, "utf8"));
|
|
4271
|
+
if (!isRecord4(parsed)) return void 0;
|
|
4272
|
+
return positiveInteger(parsed.bashOutputMaxChars);
|
|
4273
|
+
} catch {
|
|
4274
|
+
return void 0;
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
function positiveInteger(value) {
|
|
4278
|
+
const normalized = typeof value === "string" ? value.trim().replaceAll("_", "") : value;
|
|
4279
|
+
const parsed = typeof normalized === "string" && normalized.length > 0 ? Number(normalized) : normalized;
|
|
4280
|
+
return typeof parsed === "number" && Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
4281
|
+
}
|
|
4282
|
+
function isRecord4(value) {
|
|
4283
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4284
|
+
}
|
|
4285
|
+
|
|
2812
4286
|
// host/claude/src/claude-invocation-authentication-command.ts
|
|
2813
4287
|
var CLAUDE_INVOCATION_AUTHENTICATION_DESCRIPTOR = {
|
|
2814
4288
|
producer_id: "claude",
|
|
4289
|
+
context_activation_wait_max_ms: CLAUDE_CONTEXT_ACTIVATION_WAIT_MAX_MS,
|
|
2815
4290
|
create_store: (directory) => new ClaudeOutboxStore(directory),
|
|
2816
4291
|
session_id_env: CLAUDE_SESSION_ID_ENVIRONMENT,
|
|
2817
4292
|
session_ref: (_hostSourceRef, sessionId) => claudeLocalSessionRef(sessionId),
|
|
2818
4293
|
config_flag: HOST_PRODUCER_CONFIG_FLAG,
|
|
2819
4294
|
parse_config_flag: parseHostProducerConfigFlag,
|
|
2820
|
-
read_producer_config: (configPath) => readHostProducerConfig(CLAUDE_PRODUCER_DESCRIPTOR, configPath)
|
|
4295
|
+
read_producer_config: (configPath) => readHostProducerConfig(CLAUDE_PRODUCER_DESCRIPTOR, configPath),
|
|
4296
|
+
resolve_visible_output_max_bytes: () => resolveClaudeVisibleOutputMaxBytes()
|
|
2821
4297
|
};
|
|
2822
4298
|
function runClaudeInvocationAuthenticationCommand(args, options) {
|
|
2823
4299
|
return runHostInvocationAuthenticationCommand(CLAUDE_INVOCATION_AUTHENTICATION_DESCRIPTOR, args, options);
|
|
2824
4300
|
}
|
|
2825
4301
|
|
|
2826
4302
|
// host/client-doctor.ts
|
|
2827
|
-
import { createHash as
|
|
4303
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
2828
4304
|
import { execFile } from "node:child_process";
|
|
2829
|
-
import { access, open as
|
|
2830
|
-
import { delimiter, isAbsolute, join as
|
|
4305
|
+
import { access, open as open5, readFile as readFile4, realpath, stat } from "node:fs/promises";
|
|
4306
|
+
import { delimiter, isAbsolute as isAbsolute2, join as join6 } from "node:path";
|
|
2831
4307
|
import { promisify } from "node:util";
|
|
2832
4308
|
var execFileAsync = promisify(execFile);
|
|
2833
4309
|
var GIT_OID = /^[a-f0-9]{40}$/u;
|
|
2834
4310
|
var SHA2562 = /^[a-f0-9]{64}$/u;
|
|
2835
|
-
var
|
|
4311
|
+
var SAFE_REF5 = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u;
|
|
2836
4312
|
var CEAL_ENVIRONMENT_OVERRIDES = ["CEAL_GATEWAY_ORIGIN", "CEAL_GATEWAY_TRANSPORT_ORIGIN", "CEAL_ACCESS_TOKEN", "CEAL_CONTEXT_STORE_PATH"];
|
|
2837
4313
|
async function doctorClient(options = {}, component) {
|
|
2838
4314
|
const environment = canonicalClientEnvironment(options.environment ?? process.env);
|
|
@@ -2882,8 +4358,8 @@ function probeFailure(probe, command, component, options) {
|
|
|
2882
4358
|
const refusal = hostClientProbeRefusal(probe, { component, command, executable_identity: executableIdentity(options) });
|
|
2883
4359
|
return inspectedFailure(probeCode(refusal.code), refusal.message, refusal.next_action);
|
|
2884
4360
|
}
|
|
2885
|
-
function probeCode(
|
|
2886
|
-
if (
|
|
4361
|
+
function probeCode(code2) {
|
|
4362
|
+
if (code2 === "ceal_execution_failed" || code2 === "ceal_command_refused") return code2;
|
|
2887
4363
|
return "ceal_output_unreadable";
|
|
2888
4364
|
}
|
|
2889
4365
|
async function inspectCanonicalContext(options, executable, environment, run, component) {
|
|
@@ -2892,8 +4368,8 @@ async function inspectCanonicalContext(options, executable, environment, run, co
|
|
|
2892
4368
|
if (probe.kind !== "value" || options.expected_instance_ref !== void 0 && probe.value.instance_ref !== options.expected_instance_ref) return inspectedFailure("canonical_context_unavailable", `The ${component} Host cannot select the intended authenticated context from Ceal's canonical user configuration (${refusalFacts([{ name: "probe_kind", value: probe.kind, expected: "value" }, { name: "instance_ref", value: probe.kind === "value" ? probe.value.instance_ref : "unavailable", expected: options.expected_instance_ref ?? "a selected context" }])}).`, `Run \`ceal session adopt --email <email>\` without CEAL context overrides, then restart ${component}.`);
|
|
2893
4369
|
return { ok: true, value: probe.value };
|
|
2894
4370
|
}
|
|
2895
|
-
function inspectedFailure(
|
|
2896
|
-
return { ok: false, failure: failure(
|
|
4371
|
+
function inspectedFailure(code2, message, nextAction) {
|
|
4372
|
+
return { ok: false, failure: failure(code2, message, nextAction) };
|
|
2897
4373
|
}
|
|
2898
4374
|
function canonicalClientEnvironment(input) {
|
|
2899
4375
|
const environment = { ...input };
|
|
@@ -2901,7 +4377,7 @@ function canonicalClientEnvironment(input) {
|
|
|
2901
4377
|
return environment;
|
|
2902
4378
|
}
|
|
2903
4379
|
async function resolveCealExecutable(command, environment) {
|
|
2904
|
-
const candidates =
|
|
4380
|
+
const candidates = isAbsolute2(command) ? [command] : (environment.PATH ?? "").split(delimiter).filter((entry) => entry.length > 0).map((entry) => join6(entry, command));
|
|
2905
4381
|
for (const candidate of candidates) {
|
|
2906
4382
|
try {
|
|
2907
4383
|
await access(candidate);
|
|
@@ -2909,7 +4385,7 @@ async function resolveCealExecutable(command, environment) {
|
|
|
2909
4385
|
const metadata = await stat(path);
|
|
2910
4386
|
if (!metadata.isFile()) continue;
|
|
2911
4387
|
if (await isLauncher(path)) return resolveLauncherBinary(path, environment);
|
|
2912
|
-
return { path, sha256:
|
|
4388
|
+
return { path, sha256: createHash5("sha256").update(await readFile4(path)).digest("hex") };
|
|
2913
4389
|
} catch {
|
|
2914
4390
|
}
|
|
2915
4391
|
}
|
|
@@ -2917,7 +4393,7 @@ async function resolveCealExecutable(command, environment) {
|
|
|
2917
4393
|
}
|
|
2918
4394
|
async function isLauncher(path) {
|
|
2919
4395
|
if (/\.(?:bat|cjs|cmd|js|mjs|ps1)$/u.test(path)) return true;
|
|
2920
|
-
const handle = await
|
|
4396
|
+
const handle = await open5(path, "r");
|
|
2921
4397
|
try {
|
|
2922
4398
|
const buffer = Buffer.alloc(128);
|
|
2923
4399
|
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, 0);
|
|
@@ -2935,18 +4411,18 @@ async function resolveLauncherBinary(launcherPath, environment) {
|
|
|
2935
4411
|
} catch {
|
|
2936
4412
|
throw new Error("launcher doctor returned unreadable output");
|
|
2937
4413
|
}
|
|
2938
|
-
if (!
|
|
4414
|
+
if (!isRecord5(parsed) || parsed.schema_version !== "ceal.doctor.v1" || parsed.ok !== true || typeof parsed.binary_path !== "string" || parsed.binary_path.length === 0) throw new Error("launcher doctor did not report a binary path");
|
|
2939
4415
|
const binaryPath = await realpath(parsed.binary_path);
|
|
2940
4416
|
const metadata = await stat(binaryPath);
|
|
2941
4417
|
if (!metadata.isFile()) throw new Error("launcher doctor reported a non-file binary path");
|
|
2942
|
-
return { path: binaryPath, sha256:
|
|
4418
|
+
return { path: binaryPath, sha256: createHash5("sha256").update(await readFile4(binaryPath)).digest("hex") };
|
|
2943
4419
|
}
|
|
2944
4420
|
async function executeCealCommand(executable, args, environment) {
|
|
2945
4421
|
try {
|
|
2946
4422
|
const result = await execFileAsync(executable, [...args], { env: environment, timeout: 15e3, encoding: "utf8" });
|
|
2947
4423
|
return { exit_code: 0, stdout: result.stdout, stderr: result.stderr };
|
|
2948
4424
|
} catch (error) {
|
|
2949
|
-
if (
|
|
4425
|
+
if (isRecord5(error)) return { exit_code: typeof error.code === "number" ? error.code : 1, stdout: typeof error.stdout === "string" ? error.stdout : "", stderr: typeof error.stderr === "string" ? error.stderr : "" };
|
|
2950
4426
|
throw error;
|
|
2951
4427
|
}
|
|
2952
4428
|
}
|
|
@@ -2957,20 +4433,20 @@ function commandDocument(result) {
|
|
|
2957
4433
|
if (result === null) return null;
|
|
2958
4434
|
try {
|
|
2959
4435
|
const value = JSON.parse(result.stdout);
|
|
2960
|
-
return
|
|
4436
|
+
return isRecord5(value) ? value : null;
|
|
2961
4437
|
} catch {
|
|
2962
4438
|
return null;
|
|
2963
4439
|
}
|
|
2964
4440
|
}
|
|
2965
4441
|
function selectedContext(value) {
|
|
2966
4442
|
if (value?.schema_version !== "ceal.access-group.v1" || value.ok !== true || !Array.isArray(value.contexts)) return null;
|
|
2967
|
-
const selected = value.contexts.filter((item) =>
|
|
4443
|
+
const selected = value.contexts.filter((item) => isRecord5(item) && item.selected === true);
|
|
2968
4444
|
if (selected.length !== 1) return null;
|
|
2969
4445
|
return selectedContextRecord(selected[0]);
|
|
2970
4446
|
}
|
|
2971
4447
|
function selectedContextRecord(item) {
|
|
2972
|
-
if (!
|
|
2973
|
-
if (!
|
|
4448
|
+
if (!isRecord5(item) || typeof item.name !== "string" || typeof item.instance_ref !== "string") return null;
|
|
4449
|
+
if (!SAFE_REF5.test(item.instance_ref)) return null;
|
|
2974
4450
|
return { name: item.name, instance_ref: item.instance_ref };
|
|
2975
4451
|
}
|
|
2976
4452
|
function versionIdentity(value) {
|
|
@@ -2979,12 +4455,12 @@ function versionIdentity(value) {
|
|
|
2979
4455
|
return { version: value.version, source_commit: value.source_commit, source_tree: value.source_tree };
|
|
2980
4456
|
}
|
|
2981
4457
|
function validExpectations(options) {
|
|
2982
|
-
return (options.expected_sha256 === void 0 || SHA2562.test(options.expected_sha256)) && (options.expected_source_commit === void 0 || GIT_OID.test(options.expected_source_commit)) && (options.expected_instance_ref === void 0 ||
|
|
4458
|
+
return (options.expected_sha256 === void 0 || SHA2562.test(options.expected_sha256)) && (options.expected_source_commit === void 0 || GIT_OID.test(options.expected_source_commit)) && (options.expected_instance_ref === void 0 || SAFE_REF5.test(options.expected_instance_ref));
|
|
2983
4459
|
}
|
|
2984
|
-
function failure(
|
|
2985
|
-
return { ok: false, status: "failure", error: { code:
|
|
4460
|
+
function failure(code2, message, next_action) {
|
|
4461
|
+
return { ok: false, status: "failure", error: { code: code2, message, next_action } };
|
|
2986
4462
|
}
|
|
2987
|
-
function
|
|
4463
|
+
function isRecord5(value) {
|
|
2988
4464
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2989
4465
|
}
|
|
2990
4466
|
|