@corca-org/ceal-host-linux-arm64 1.0.17 → 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 +0 -0
- package/libexec/ceal-host/claude-hook.mjs +1944 -526
- package/libexec/ceal-host/claude-invocation-authentication.mjs +1827 -331
- package/libexec/ceal-host/claude-producer.mjs +1794 -413
- package/libexec/ceal-host/codex-hook.mjs +1407 -1332
- package/libexec/ceal-host/codex-invocation-authentication.mjs +3075 -856
- package/libexec/ceal-host/codex-producer.mjs +957 -918
- package/libexec/ceal-host/control.mjs +2289 -1452
- package/libexec/ceal-host/relay.mjs +822 -137
- package/package.json +1 -1
- package/share/ceal-host/bundle-manifest.json +12 -12
- package/share/ceal-host/bundle-manifest.sigstore.json +1 -1
|
@@ -153,6 +153,9 @@ var client_wire_contract_default = {
|
|
|
153
153
|
"document"
|
|
154
154
|
],
|
|
155
155
|
optional_fields: [
|
|
156
|
+
"operation_id",
|
|
157
|
+
"visible_output_max_bytes",
|
|
158
|
+
"local_delivery",
|
|
156
159
|
"verified_artifact"
|
|
157
160
|
],
|
|
158
161
|
bare_document_fields: [
|
|
@@ -266,6 +269,7 @@ var CEAL_WRITE_RECEIPT_PROVIDER_STATES = vocabulary(["outcome_unknown", "verifie
|
|
|
266
269
|
var CEAL_WRITE_RECEIPT_PROVIDER_READBACKS = vocabulary(["outcome_unknown", "verified", "not_applied"]);
|
|
267
270
|
var CEAL_PROTOCOL_VERSION = "1.4.0";
|
|
268
271
|
var CEAL_OPERATION_ACCESS_SCHEMA_VERSION = "ceal.operation_access.v1";
|
|
272
|
+
var CEAL_GATEWAY_CONTINUATION_CURSOR_PHASES = vocabulary(["scan", "delivery", "terminal"]);
|
|
269
273
|
var CEAL_GATEWAY_ACTIVE_CALL_PHASES = vocabulary([
|
|
270
274
|
"audited_call_pending",
|
|
271
275
|
"continuation_read_pending",
|
|
@@ -289,6 +293,7 @@ var CEAL_GATEWAY_ORGANIZATION_ROUTE_FACT_CALL_KEYS = Object.freeze([
|
|
|
289
293
|
// packages/ceal-client-protocol/src/operation-next-actions.ts
|
|
290
294
|
var CEAL_REPEAT_REQUEST_NEXT_ACTION = "repeat_request";
|
|
291
295
|
var CEAL_INSPECT_RECEIPT_NEXT_ACTION = "inspect_receipt";
|
|
296
|
+
var CEAL_ANSWER_WITH_GAP_NEXT_ACTION = "answer_with_gap";
|
|
292
297
|
var CALLER_RECOVERABLE_NEXT_ACTION_VALUES = [
|
|
293
298
|
"select_did_you_mean",
|
|
294
299
|
"choose_new_output_path",
|
|
@@ -342,10 +347,20 @@ var CALLER_RECOVERABLE_NEXT_ACTION_VALUES = [
|
|
|
342
347
|
"retry_after",
|
|
343
348
|
// The ref names a container of rows (a Notion database or data source), which
|
|
344
349
|
// has no body of its own; the caller searches its rows instead of reading it.
|
|
345
|
-
"search_container_rows"
|
|
350
|
+
"search_container_rows",
|
|
351
|
+
// A settled unknown write names the exact receipt Operation that can inspect
|
|
352
|
+
// its outcome without issuing another provider mutation.
|
|
353
|
+
"write_receipt.get",
|
|
354
|
+
// Command resolution carries the exact required Operation sequence beside
|
|
355
|
+
// this action, so the caller can execute each declared input in order.
|
|
356
|
+
"follow_required_action_sequence",
|
|
357
|
+
// The command needs caller-owned local materialization (for example an
|
|
358
|
+
// @path file) before the Gateway can receive the argument.
|
|
359
|
+
"materialize_local_argument"
|
|
346
360
|
];
|
|
347
361
|
var CEAL_CALLER_RECOVERABLE_NEXT_ACTIONS = Object.freeze(CALLER_RECOVERABLE_NEXT_ACTION_VALUES);
|
|
348
362
|
var NON_REMEDY_NEXT_ACTION_VALUES = [
|
|
363
|
+
CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
349
364
|
// Only an administrator can take the named step -- approving a place for an
|
|
350
365
|
// Operation, granting a withheld capability -- so a non-administrator asks
|
|
351
366
|
// one. The refusal names the administrator step; this word is the ask.
|
|
@@ -393,7 +408,58 @@ var NON_REMEDY_NEXT_ACTION_VALUES = [
|
|
|
393
408
|
"select_exact_approval"
|
|
394
409
|
];
|
|
395
410
|
var CEAL_NON_REMEDY_NEXT_ACTIONS = Object.freeze(NON_REMEDY_NEXT_ACTION_VALUES);
|
|
396
|
-
var
|
|
411
|
+
var CEAL_DECLARED_NEXT_ACTIONS = Object.freeze([...CEAL_CALLER_RECOVERABLE_NEXT_ACTIONS, ...CEAL_NON_REMEDY_NEXT_ACTIONS]);
|
|
412
|
+
var DECLARED = new Set(CEAL_DECLARED_NEXT_ACTIONS);
|
|
413
|
+
function defineCealNonModelNextActions(actions) {
|
|
414
|
+
return actions;
|
|
415
|
+
}
|
|
416
|
+
var NON_MODEL_NEXT_ACTION_VALUES = defineCealNonModelNextActions([
|
|
417
|
+
CEAL_INSPECT_RECEIPT_NEXT_ACTION,
|
|
418
|
+
"await_pending_approval",
|
|
419
|
+
"inspect_gateway",
|
|
420
|
+
"operator_action",
|
|
421
|
+
"none",
|
|
422
|
+
"check_gateway",
|
|
423
|
+
"inspect_context_store",
|
|
424
|
+
"inspect_local_cache",
|
|
425
|
+
"inspect_local_client",
|
|
426
|
+
"inspect_local_command",
|
|
427
|
+
"inspect_local_output",
|
|
428
|
+
"inspect_local_route",
|
|
429
|
+
"inspect_request",
|
|
430
|
+
"reconcile",
|
|
431
|
+
"reconcile_occurrence",
|
|
432
|
+
"retry_after_owner_recovery",
|
|
433
|
+
"select_exact_approval"
|
|
434
|
+
]);
|
|
435
|
+
var NON_MODEL_NEXT_ACTIONS = new Set(NON_MODEL_NEXT_ACTION_VALUES);
|
|
436
|
+
function isCealModelActionableNonRemedyNextAction(action) {
|
|
437
|
+
return !NON_MODEL_NEXT_ACTIONS.has(action);
|
|
438
|
+
}
|
|
439
|
+
var CEAL_MODEL_ACTIONABLE_NEXT_ACTIONS = Object.freeze([
|
|
440
|
+
...CEAL_CALLER_RECOVERABLE_NEXT_ACTIONS,
|
|
441
|
+
...CEAL_NON_REMEDY_NEXT_ACTIONS.filter(isCealModelActionableNonRemedyNextAction)
|
|
442
|
+
]);
|
|
443
|
+
var MODEL_ACTIONABLE = new Set(CEAL_MODEL_ACTIONABLE_NEXT_ACTIONS);
|
|
444
|
+
var NON_MODEL_ACTION_TO_MODEL_ACTION = Object.freeze({
|
|
445
|
+
[CEAL_INSPECT_RECEIPT_NEXT_ACTION]: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
446
|
+
await_pending_approval: "ask_admin",
|
|
447
|
+
inspect_gateway: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
448
|
+
operator_action: "ask_admin",
|
|
449
|
+
none: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
450
|
+
check_gateway: "ask_admin",
|
|
451
|
+
inspect_context_store: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
452
|
+
inspect_local_cache: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
453
|
+
inspect_local_client: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
454
|
+
inspect_local_command: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
455
|
+
inspect_local_output: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
456
|
+
inspect_local_route: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
457
|
+
inspect_request: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
458
|
+
reconcile: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
459
|
+
reconcile_occurrence: "ask_admin",
|
|
460
|
+
retry_after_owner_recovery: "ask_admin",
|
|
461
|
+
select_exact_approval: "ask_admin"
|
|
462
|
+
});
|
|
397
463
|
var RETRY_PROMISE_NEXT_ACTIONS = ["retry", "retry_after"];
|
|
398
464
|
var RETRY_PROMISE = new Set(RETRY_PROMISE_NEXT_ACTIONS);
|
|
399
465
|
var RECOVERABLE = new Set(CEAL_CALLER_RECOVERABLE_NEXT_ACTIONS);
|
|
@@ -450,9 +516,9 @@ var CEAL_OPERATION_APPROVAL_DISPOSITIONS = vocabulary([
|
|
|
450
516
|
var CealProtocolValidationError = class extends Error {
|
|
451
517
|
name = "CealProtocolValidationError";
|
|
452
518
|
code;
|
|
453
|
-
constructor(
|
|
454
|
-
super(
|
|
455
|
-
this.code =
|
|
519
|
+
constructor(code2) {
|
|
520
|
+
super(code2 === "invalid_gateway_request" ? "Ceal Gateway request is invalid." : "Ceal client response is invalid.");
|
|
521
|
+
this.code = code2;
|
|
456
522
|
}
|
|
457
523
|
};
|
|
458
524
|
var SAFE_REF = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
@@ -485,10 +551,10 @@ function assertSafeJsonArray(value, options, depth, count) {
|
|
|
485
551
|
if (value.length > CEAL_WIRE_SAFE_ARRAY_MAX_ITEMS) invalidByContext(options);
|
|
486
552
|
for (const item of value) assertSafeJsonValue(item, options, depth + 1, count);
|
|
487
553
|
}
|
|
488
|
-
function assertSafeJsonRecord(
|
|
489
|
-
const entries = Object.entries(
|
|
554
|
+
function assertSafeJsonRecord(record5, options, depth, count) {
|
|
555
|
+
const entries = Object.entries(record5);
|
|
490
556
|
if (entries.length > CEAL_WIRE_SAFE_ARRAY_MAX_ITEMS) invalidByContext(options);
|
|
491
|
-
const sourceUrlColumn = options.allowHttpsUrl ? compactSourceUrlColumn(
|
|
557
|
+
const sourceUrlColumn = options.allowHttpsUrl ? compactSourceUrlColumn(record5.fields) : null;
|
|
492
558
|
for (const [key, child] of entries) {
|
|
493
559
|
if (key === "credential_material_included" && child !== false) invalidByContext(options);
|
|
494
560
|
if (!isSafeNegativeMaterialAssertion(key, child)) assertSafeJsonKey(key, options);
|
|
@@ -663,21 +729,15 @@ var CEAL_OPERATION_ACCESS_CLASSES = vocabulary(["read", "write"]);
|
|
|
663
729
|
// packages/ceal-client-protocol/src/domain-vocabulary.ts
|
|
664
730
|
var CEAL_CONNECTOR_STATUSES = vocabulary(["active", "disabled", "disconnected"]);
|
|
665
731
|
var CEAL_CONNECTOR_CUSTODIES = vocabulary(["organization", "personal"]);
|
|
666
|
-
var CEAL_PROVIDER_BOUNDARY_KINDS = vocabulary([
|
|
667
|
-
"github_account",
|
|
668
|
-
"github_organization",
|
|
669
|
-
"google_workspace_domain",
|
|
670
|
-
"notion_workspace",
|
|
671
|
-
"slack_workspace"
|
|
672
|
-
]);
|
|
673
|
-
var CEAL_PROVIDER_BOUNDARY_EVIDENCE = vocabulary(["provider_readback", "operator_stated"]);
|
|
674
732
|
var CEAL_ACCESS_LEVELS = vocabulary(["none", "read", "read_write"]);
|
|
675
733
|
var CEAL_PERSONAL_ACCESS_CEILINGS = vocabulary(["disabled", "read", "read_write"]);
|
|
676
734
|
var CEAL_TARGET_REF_PATTERN = "^[^\\p{Cc}]+$";
|
|
677
735
|
var TARGET_REF = new RegExp(CEAL_TARGET_REF_PATTERN, "u");
|
|
678
736
|
var CEAL_TARGET_VISIBILITIES = vocabulary(["public", "private"]);
|
|
679
|
-
var CEAL_PRINCIPAL_KINDS = vocabulary(["human", "
|
|
737
|
+
var CEAL_PRINCIPAL_KINDS = vocabulary(["human", "runtime"]);
|
|
738
|
+
var CEAL_PERSISTED_PRINCIPAL_KINDS = vocabulary(["human", "service", "runtime"]);
|
|
680
739
|
var CEAL_ORGANIZATION_ROLES = vocabulary(["member", "admin"]);
|
|
740
|
+
var CEAL_CALENDAR_PERIODS = vocabulary(["day", "week", "month"]);
|
|
681
741
|
var LABEL_EXPECTATION = `public-safe display text of at most ${CEAL_PUBLIC_DISPLAY_NAME_MAX_BYTES} bytes`;
|
|
682
742
|
|
|
683
743
|
// packages/ceal-client-protocol/src/safe-json-budget.ts
|
|
@@ -853,7 +913,7 @@ var CEAL_SCHEMA_LITERALS = [
|
|
|
853
913
|
"ceal.operation_receipt.v1",
|
|
854
914
|
"ceal.operation_result.v1",
|
|
855
915
|
"ceal.operation_result.compact.v1",
|
|
856
|
-
"ceal.
|
|
916
|
+
"ceal.local_result_file.v1",
|
|
857
917
|
"ceal.local_result_root.v1",
|
|
858
918
|
"ceal.owner_bootstrap_exchange_request.v1",
|
|
859
919
|
"ceal.owner_bootstrap_exchange_result.v1",
|
|
@@ -947,41 +1007,6 @@ var CEAL_VERIFIED_ARTIFACT_DESCRIPTOR_JSON_SCHEMA = Object.freeze({
|
|
|
947
1007
|
}
|
|
948
1008
|
});
|
|
949
1009
|
|
|
950
|
-
// packages/ceal-client-protocol/src/provider-operation-vocabulary.ts
|
|
951
|
-
var CEAL_FILE_TYPE_FAMILIES = vocabulary(["all", "image", "pdf"]);
|
|
952
|
-
var CEAL_GITHUB_ISSUE_STATES = vocabulary(["open", "closed", "all"]);
|
|
953
|
-
var CEAL_NOTION_UPDATABLE_BLOCK_TYPES = vocabulary([
|
|
954
|
-
"paragraph",
|
|
955
|
-
"heading_1",
|
|
956
|
-
"heading_2",
|
|
957
|
-
"heading_3",
|
|
958
|
-
"bulleted_list_item",
|
|
959
|
-
"numbered_list_item",
|
|
960
|
-
"to_do",
|
|
961
|
-
"code"
|
|
962
|
-
]);
|
|
963
|
-
var CEAL_NOTION_NEUTRAL_TYPES = vocabulary([
|
|
964
|
-
"text",
|
|
965
|
-
"number",
|
|
966
|
-
"boolean",
|
|
967
|
-
"date",
|
|
968
|
-
"select",
|
|
969
|
-
"multi_select",
|
|
970
|
-
"people",
|
|
971
|
-
"url",
|
|
972
|
-
"email",
|
|
973
|
-
"phone",
|
|
974
|
-
"unsupported"
|
|
975
|
-
]);
|
|
976
|
-
var CEAL_NOTION_IDENTITY_GAP_REASONS = vocabulary([
|
|
977
|
-
"identity_unavailable",
|
|
978
|
-
"identity_stale",
|
|
979
|
-
"identity_ambiguous",
|
|
980
|
-
"identity_revoked",
|
|
981
|
-
"identity_unlinked"
|
|
982
|
-
]);
|
|
983
|
-
var CEAL_NOTION_STRING_VALUE_TYPES = vocabulary(["text", "select", "url", "email", "phone"]);
|
|
984
|
-
|
|
985
1010
|
// packages/ceal-client-protocol/src/opaque-reference.ts
|
|
986
1011
|
var CEAL_OPAQUE_HANDLE_KINDS = vocabulary(["target", "message", "thread", "artifact", "document", "block", "object"]);
|
|
987
1012
|
var CEAL_OPAQUE_REF_KINDS = vocabulary([...CEAL_OPAQUE_HANDLE_KINDS, "result", "operation-receipt", "reply-receipt", "field", "notion-data-source"]);
|
|
@@ -993,7 +1018,7 @@ var OPTIONAL_FIELDS = CEAL_CLIENT_WIRE_CONTRACT.command_response.bare_document_o
|
|
|
993
1018
|
// packages/ceal-client-protocol/src/resource-row.ts
|
|
994
1019
|
var CEAL_RESOURCE_SEARCH_ORDERS = vocabulary(["relevance", "modified", "viewed", "created"]);
|
|
995
1020
|
var CEAL_RESOURCE_SEARCH_DIRECTIONS = vocabulary(["ascending", "descending"]);
|
|
996
|
-
var CEAL_RESOURCE_SEARCH_FILTERS = vocabulary(["query", "modified_since", "since", "until", "authored_by_me", "include_replies", "kinds", "container_ref", "container_refs", "container_names", "addressed_to_me", "unread"]);
|
|
1021
|
+
var CEAL_RESOURCE_SEARCH_FILTERS = vocabulary(["query", "modified_since", "since", "until", "authored_by_me", "authored_by", "include_replies", "kinds", "container_ref", "container_refs", "container_names", "addressed_to_me", "unread"]);
|
|
997
1022
|
var CEAL_RESOURCE_TIME_BASES = vocabulary(["created", "modified", "viewed"]);
|
|
998
1023
|
var CEAL_REPOSITORY_VISIBILITIES = vocabulary(["public", "private", "internal"]);
|
|
999
1024
|
var CEAL_ACTIVITY_V3_KINDS = vocabulary(["authored", "edited", "messaged", "responded"]);
|
|
@@ -1014,6 +1039,7 @@ var CEAL_ACTIVITY_TIMESTAMP_BASES = vocabulary([
|
|
|
1014
1039
|
"resource_modified"
|
|
1015
1040
|
]);
|
|
1016
1041
|
var CEAL_ACTIVITY_CONNECTOR_KINDS = vocabulary(["slack", "github", "google-calendar", "google-drive", "notion"]);
|
|
1042
|
+
var CEAL_ACTIVITY_UNSUPPORTED_EVENT_KIND_CODE = "unsupported_event_kind";
|
|
1017
1043
|
var CEAL_ACTIVITY_GAP_CODES = vocabulary([
|
|
1018
1044
|
"provider_page_budget",
|
|
1019
1045
|
"call_deadline",
|
|
@@ -1021,7 +1047,7 @@ var CEAL_ACTIVITY_GAP_CODES = vocabulary([
|
|
|
1021
1047
|
"provider_cursor_unavailable",
|
|
1022
1048
|
"malformed_provider_page",
|
|
1023
1049
|
"visibility_incomplete",
|
|
1024
|
-
|
|
1050
|
+
CEAL_ACTIVITY_UNSUPPORTED_EVENT_KIND_CODE,
|
|
1025
1051
|
"direct_messages_excluded"
|
|
1026
1052
|
]);
|
|
1027
1053
|
var CEAL_ACTIVITY_EVENT_KINDS = vocabulary([
|
|
@@ -1072,6 +1098,7 @@ var RESOURCE_ROW_KEY_RULES = Object.freeze({
|
|
|
1072
1098
|
ref: { required: true, timestamp: false },
|
|
1073
1099
|
next: { required: true, timestamp: false },
|
|
1074
1100
|
kind: { required: true, timestamp: false },
|
|
1101
|
+
matched_kinds: { required: false, timestamp: false },
|
|
1075
1102
|
title: { required: true, timestamp: false },
|
|
1076
1103
|
container_label: { required: true, timestamp: false },
|
|
1077
1104
|
container_ref: { required: false, timestamp: false },
|
|
@@ -1085,6 +1112,8 @@ var RESOURCE_ROW_KEY_RULES = Object.freeze({
|
|
|
1085
1112
|
modified_at: { required: false, timestamp: true },
|
|
1086
1113
|
modified_at_local: { required: false, timestamp: true },
|
|
1087
1114
|
viewed_by_me_at: { required: false, timestamp: true },
|
|
1115
|
+
own_ceal_write: { required: false, timestamp: false },
|
|
1116
|
+
owned_by_me: { required: false, timestamp: false },
|
|
1088
1117
|
visibility: { required: false, timestamp: false },
|
|
1089
1118
|
start: { required: false, timestamp: false },
|
|
1090
1119
|
end: { required: false, timestamp: false },
|
|
@@ -1118,6 +1147,15 @@ function cealExcludedTargetCoverageJsonSchema() {
|
|
|
1118
1147
|
};
|
|
1119
1148
|
}
|
|
1120
1149
|
|
|
1150
|
+
// packages/ceal-client-protocol/src/gateway-write-identity.ts
|
|
1151
|
+
var CEAL_GATEWAY_WRITE_IDENTITY_ROLES = Object.freeze(["replay_identity", "lookup_handle", "collision_evidence"]);
|
|
1152
|
+
var CEAL_GATEWAY_WRITE_REQUEST_REF_PATTERN = /^gateway-write-request:[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/u;
|
|
1153
|
+
var CEAL_GATEWAY_WRITE_IDENTITY_FIELDS = Object.freeze({
|
|
1154
|
+
replay_identity: "idempotency_claim_sha256",
|
|
1155
|
+
lookup_handle: "write_request_sha256",
|
|
1156
|
+
collision_evidence: "normalized_mutation_sha256"
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1121
1159
|
// packages/ceal-client-protocol/src/write-safety-finding.ts
|
|
1122
1160
|
var CEAL_WRITE_SAFETY_FINDING_STATUSES = vocabulary(["warning", "unchecked"]);
|
|
1123
1161
|
var CEAL_WRITE_SAFETY_AUDIENCES = vocabulary(["workspace", "person", "members", "unknown"]);
|
|
@@ -1145,7 +1183,11 @@ var CEAL_REFRESH_FAILURE_CODES = vocabulary([
|
|
|
1145
1183
|
"refresh_revoked",
|
|
1146
1184
|
"authority_replaced",
|
|
1147
1185
|
"refresh_recovery_unavailable",
|
|
1148
|
-
"refresh_recovery_expired"
|
|
1186
|
+
"refresh_recovery_expired",
|
|
1187
|
+
// The serving gate answers this while the Gateway is still starting and the
|
|
1188
|
+
// refresh was never attempted; the same attempt stays retryable. It is never
|
|
1189
|
+
// stored by the session owner, only answered at the serving edge.
|
|
1190
|
+
"gateway_starting"
|
|
1149
1191
|
]);
|
|
1150
1192
|
|
|
1151
1193
|
// packages/ceal-client-protocol/src/request-validation-primitives.ts
|
|
@@ -1178,7 +1220,7 @@ var CANCELLATION_STATES = vocabulary(["requested", "cancelled"]);
|
|
|
1178
1220
|
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"]);
|
|
1179
1221
|
var CEAL_REQUEST_JSON_SCHEMA = {
|
|
1180
1222
|
type: "object",
|
|
1181
|
-
maxProperties:
|
|
1223
|
+
maxProperties: 11,
|
|
1182
1224
|
additionalProperties: false,
|
|
1183
1225
|
properties: {
|
|
1184
1226
|
schema_version: { type: "string", const: CEAL_REQUEST_SCHEMA_VERSION, maxLength: 32 },
|
|
@@ -1202,7 +1244,8 @@ var CEAL_REQUEST_JSON_SCHEMA = {
|
|
|
1202
1244
|
host_turn_ref: { type: "string", minLength: 1, maxLength: 128, pattern: SAFE_REF.source },
|
|
1203
1245
|
host_input_ref: { type: "string", minLength: 1, maxLength: 128, pattern: SAFE_REF.source },
|
|
1204
1246
|
host_source_ref: { type: "string", minLength: 1, maxLength: 128, pattern: SAFE_REF.source },
|
|
1205
|
-
requester_timezone: { type: "string", minLength: 1, maxLength: 128, description: "IANA timezone observed for the requester on this Request." }
|
|
1247
|
+
requester_timezone: { type: "string", minLength: 1, maxLength: 128, description: "IANA timezone observed for the requester on this Request." },
|
|
1248
|
+
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." }
|
|
1206
1249
|
},
|
|
1207
1250
|
required: CEAL_REQUEST_KEYS
|
|
1208
1251
|
};
|
|
@@ -1261,6 +1304,7 @@ var REQUEST_EXECUTION_PREPARE_REFUSAL_PROPERTIES = {
|
|
|
1261
1304
|
retryable: { type: "boolean" },
|
|
1262
1305
|
source: { type: "string", enum: ["provider", "gateway"] },
|
|
1263
1306
|
retry_after_ms: { type: "integer", minimum: 0, maximum: CEAL_RETRY_AFTER_MAX_MS },
|
|
1307
|
+
write_request_ref: { type: "string", pattern: CEAL_GATEWAY_WRITE_REQUEST_REF_PATTERN.source, maxLength: 128 },
|
|
1264
1308
|
excluded_targets: cealExcludedTargetCoverageJsonSchema(),
|
|
1265
1309
|
repaired_input: {},
|
|
1266
1310
|
unrepaired_pointers: { type: "array", maxItems: 128, items: { type: "string", minLength: 1, maxLength: 256 } },
|
|
@@ -1319,12 +1363,55 @@ function isCealHostInputKind(value) {
|
|
|
1319
1363
|
return typeof value === "string" && CEAL_HOST_INPUT_KINDS.some((kind) => kind === value);
|
|
1320
1364
|
}
|
|
1321
1365
|
var CEAL_HOST_OBSERVATION_REF_PREFIX = "host-observation:";
|
|
1366
|
+
var HOST_OBSERVATION_PRODUCER = /^[a-z][a-z0-9]{0,31}$/u;
|
|
1367
|
+
var HOST_OBSERVATION_DIGEST = /^[a-f0-9]{8,64}$/u;
|
|
1368
|
+
function cealHostObservationRef(producer, digest2) {
|
|
1369
|
+
if (!HOST_OBSERVATION_PRODUCER.test(producer) || !HOST_OBSERVATION_DIGEST.test(digest2)) invalid("Ceal Host observation reference parts are invalid.");
|
|
1370
|
+
return `${CEAL_HOST_OBSERVATION_REF_PREFIX}${producer}-${digest2}`;
|
|
1371
|
+
}
|
|
1372
|
+
var HOST_TOKEN_OBSERVATION_BREAKDOWN_KEYS = vocabulary(["cached_input", "cache_write_input", "reasoning_output"]);
|
|
1373
|
+
function createHostTokenObservationValue(counts, source) {
|
|
1374
|
+
const core = validTokenCore(counts);
|
|
1375
|
+
if (typeof source !== "string" || source.length === 0 || source.length > 256) invalid("Ceal Host token observation source is invalid.");
|
|
1376
|
+
const value = { status: "available", unit: "tokens", input: core.input, output: core.output, total: core.total, source };
|
|
1377
|
+
for (const breakdown of validTokenBreakdowns(counts)) value[breakdown.key] = breakdown.tokens;
|
|
1378
|
+
assertHostObservationValue(value, "token");
|
|
1379
|
+
return value;
|
|
1380
|
+
}
|
|
1381
|
+
function validTokenCore(counts) {
|
|
1382
|
+
if (!isOperationRecord(counts) || !safeCount(counts.input) || !safeCount(counts.output) || !safeCount(counts.total)) invalid("Ceal Host token observation counts are invalid.");
|
|
1383
|
+
if (counts.total !== counts.input + counts.output) invalid("Ceal Host token observation counts are invalid.");
|
|
1384
|
+
return { input: counts.input, output: counts.output, total: counts.total };
|
|
1385
|
+
}
|
|
1386
|
+
function validTokenBreakdowns(counts) {
|
|
1387
|
+
const breakdowns = [
|
|
1388
|
+
{ key: "cached_input", tokens: counts.cached_input },
|
|
1389
|
+
{ key: "cache_write_input", tokens: counts.cache_write_input },
|
|
1390
|
+
{ key: "reasoning_output", tokens: counts.reasoning_output }
|
|
1391
|
+
];
|
|
1392
|
+
const stated = breakdowns.filter((breakdown) => breakdown.tokens !== void 0).map((breakdown) => ({ key: breakdown.key, tokens: Number(breakdown.tokens) }));
|
|
1393
|
+
for (const breakdown of stated) {
|
|
1394
|
+
if (!safeCount(breakdown.tokens)) invalid("Ceal Host token observation counts are invalid.");
|
|
1395
|
+
}
|
|
1396
|
+
const cached = counts.cached_input ?? 0;
|
|
1397
|
+
const created = counts.cache_write_input ?? 0;
|
|
1398
|
+
const reasoning = counts.reasoning_output ?? 0;
|
|
1399
|
+
if (cached + created > counts.input || reasoning > counts.output) invalid("Ceal Host token observation counts are invalid.");
|
|
1400
|
+
return stated;
|
|
1401
|
+
}
|
|
1402
|
+
function hostObservationNotRecorded(reason) {
|
|
1403
|
+
if (typeof reason !== "string" || reason.length === 0 || reason.length > 256) invalid("Ceal Host observation gap reason is invalid.");
|
|
1404
|
+
return { status: "not_recorded", reason };
|
|
1405
|
+
}
|
|
1406
|
+
function safeCount(value) {
|
|
1407
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
1408
|
+
}
|
|
1322
1409
|
function validateCealHostObservationDraft(value) {
|
|
1323
|
-
const
|
|
1324
|
-
requirePrefixedRef(
|
|
1325
|
-
requireClosedValue(
|
|
1326
|
-
requireIsoTimestamp(
|
|
1327
|
-
assertHostObservationValue(
|
|
1410
|
+
const record5 = operationRecord(value, "Ceal Host observation draft is invalid.");
|
|
1411
|
+
requirePrefixedRef(record5.observation_ref, CEAL_HOST_OBSERVATION_REF_PREFIX, "Ceal Host observation reference is invalid.");
|
|
1412
|
+
requireClosedValue(record5.observation_kind, CEAL_HOST_OBSERVATION_KINDS, "Ceal Host observation kind is invalid.");
|
|
1413
|
+
requireIsoTimestamp(record5.observed_at, "Ceal Host observation time is invalid.");
|
|
1414
|
+
assertHostObservationValue(record5.value, record5.observation_kind);
|
|
1328
1415
|
}
|
|
1329
1416
|
var CEAL_REQUEST_MAX_OBSERVATION_VALUE_BYTES = 64 * 1024;
|
|
1330
1417
|
var CEAL_HOST_OBSERVATION_KINDS = vocabulary(["prompt", "session", "turn", "model", "token", "cost", "timing"]);
|
|
@@ -1354,6 +1441,10 @@ function inspectHostKeys(value) {
|
|
|
1354
1441
|
// packages/ceal-client-protocol/src/host-prompt-ref.ts
|
|
1355
1442
|
var CEAL_HOST_PROMPT_DIGEST_PATTERN = "^[a-f0-9]{64}$";
|
|
1356
1443
|
var PROMPT_DIGEST = new RegExp(CEAL_HOST_PROMPT_DIGEST_PATTERN, "u");
|
|
1444
|
+
function hostProducerPromptRef(promptSha256) {
|
|
1445
|
+
if (!PROMPT_DIGEST.test(promptSha256)) throw new TypeError("Canonical Host prompt digest is invalid.");
|
|
1446
|
+
return `ceal-host-prompt:${promptSha256}`;
|
|
1447
|
+
}
|
|
1357
1448
|
|
|
1358
1449
|
// packages/ceal-client-protocol/src/request-effect-approval.ts
|
|
1359
1450
|
var CEAL_EFFECT_APPROVAL_DECISION_CHECK_PHRASES = Object.freeze([
|
|
@@ -1456,12 +1547,94 @@ var CEAL_HOST_INVOCATION_AUTHENTICATION_RESULT_MAX_BYTES = new TextEncoder().enc
|
|
|
1456
1547
|
expires_at: (/* @__PURE__ */ new Date("+010000-01-01T00:00:00.000Z")).toISOString()
|
|
1457
1548
|
})}
|
|
1458
1549
|
`).byteLength;
|
|
1550
|
+
var CEAL_HOST_MINT_REQUEST_CONTEXT_REFUSAL_REASONS = vocabulary([
|
|
1551
|
+
"request_authority_unavailable",
|
|
1552
|
+
"request_missing",
|
|
1553
|
+
"authority_mismatch",
|
|
1554
|
+
"host_source_mismatch",
|
|
1555
|
+
"host_session_mismatch",
|
|
1556
|
+
"host_turn_mismatch",
|
|
1557
|
+
"host_input_mismatch",
|
|
1558
|
+
"host_prompt_mismatch"
|
|
1559
|
+
]);
|
|
1560
|
+
var CEAL_HOST_AUTHENTICATION_CAUSES = vocabulary([
|
|
1561
|
+
"invalid_arguments",
|
|
1562
|
+
"options_invalid",
|
|
1563
|
+
"session_unavailable",
|
|
1564
|
+
"context_unavailable",
|
|
1565
|
+
"context_replaced",
|
|
1566
|
+
"no_binding",
|
|
1567
|
+
"context_stale",
|
|
1568
|
+
"context_expired",
|
|
1569
|
+
"host_source_refused",
|
|
1570
|
+
"host_credential_refused",
|
|
1571
|
+
"gateway_unavailable",
|
|
1572
|
+
"gateway_rate_limited",
|
|
1573
|
+
"gateway_refused",
|
|
1574
|
+
"gateway_response_invalid",
|
|
1575
|
+
"producer_frame_invalid",
|
|
1576
|
+
"relay_permission_denied",
|
|
1577
|
+
"relay_unavailable",
|
|
1578
|
+
"transient_retryable",
|
|
1579
|
+
"mint_failed",
|
|
1580
|
+
"relay_failed",
|
|
1581
|
+
"host_refusal_invalid"
|
|
1582
|
+
]);
|
|
1583
|
+
var CEAL_HOST_AUTHENTICATION_POLICY = Object.freeze({
|
|
1584
|
+
invalid_arguments: Object.freeze({ classification: "host_arguments_invalid", next_action: "answer_with_gap" }),
|
|
1585
|
+
options_invalid: Object.freeze({ classification: "host_configuration_invalid", next_action: "answer_with_gap" }),
|
|
1586
|
+
session_unavailable: Object.freeze({ classification: "host_session_unavailable", next_action: "answer_with_gap" }),
|
|
1587
|
+
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" }),
|
|
1588
|
+
context_replaced: Object.freeze({ classification: "host_invocation_context_stale", next_action: "refresh_host_invocation_context" }),
|
|
1589
|
+
no_binding: Object.freeze({ classification: "host_binding_unavailable", next_action: "refresh_host_invocation_context" }),
|
|
1590
|
+
context_stale: Object.freeze({ classification: "host_invocation_context_stale", next_action: "refresh_host_invocation_context" }),
|
|
1591
|
+
context_expired: Object.freeze({ classification: "host_invocation_context_expired", next_action: "refresh_host_invocation_context" }),
|
|
1592
|
+
host_source_refused: Object.freeze({ classification: "host_source_refused", next_action: "answer_with_gap" }),
|
|
1593
|
+
host_credential_refused: Object.freeze({ classification: "host_registration_credential_refused", next_action: "ask_admin" }),
|
|
1594
|
+
gateway_unavailable: Object.freeze({ classification: "host_mint_gateway_unavailable", next_action: "ask_admin" }),
|
|
1595
|
+
gateway_rate_limited: Object.freeze({ classification: "host_mint_gateway_rate_limited", next_action: "answer_with_gap" }),
|
|
1596
|
+
gateway_refused: Object.freeze({ classification: "host_mint_gateway_refused", next_action: "answer_with_gap" }),
|
|
1597
|
+
gateway_response_invalid: Object.freeze({ classification: "host_mint_gateway_response_invalid", next_action: "answer_with_gap" }),
|
|
1598
|
+
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" }),
|
|
1599
|
+
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" }),
|
|
1600
|
+
relay_unavailable: Object.freeze({ classification: "host_relay_unavailable", next_action: "answer_with_gap" }),
|
|
1601
|
+
transient_retryable: Object.freeze({ classification: "host_mint_transient_retryable", next_action: "retry" }),
|
|
1602
|
+
mint_failed: Object.freeze({ classification: "host_mint_failed", next_action: "answer_with_gap" }),
|
|
1603
|
+
relay_failed: Object.freeze({ classification: "host_relay_failed", next_action: "answer_with_gap" }),
|
|
1604
|
+
host_refusal_invalid: Object.freeze({ classification: "host_refusal_invalid", next_action: "answer_with_gap" })
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
// packages/ceal-client-protocol/src/host-service-diagnostic.ts
|
|
1608
|
+
var CEAL_HOST_SERVICE_DIAGNOSTIC_SCHEMA = "ceal.host_service_diagnostic.v1";
|
|
1609
|
+
var CEAL_HOST_SERVICE_DIAGNOSTIC_PRODUCERS = vocabulary(["claude", "codex"]);
|
|
1610
|
+
var CEAL_HOST_SERVICE_DIAGNOSTIC_EVENTS = vocabulary([
|
|
1611
|
+
"hook_invoked",
|
|
1612
|
+
"registration_written",
|
|
1613
|
+
"hook_failed",
|
|
1614
|
+
"watcher_turn_observed",
|
|
1615
|
+
"current_prompt_registered",
|
|
1616
|
+
"current_prompt_registration_failed",
|
|
1617
|
+
"activation_recorded",
|
|
1618
|
+
"activation_failed",
|
|
1619
|
+
"watcher_scan_failed",
|
|
1620
|
+
"activation_wait_skipped",
|
|
1621
|
+
"activation_wait_engaged",
|
|
1622
|
+
"activation_wait_completed",
|
|
1623
|
+
"activation_wait_exhausted"
|
|
1624
|
+
]);
|
|
1625
|
+
var CEAL_HOST_SERVICE_DIAGNOSTIC_FAILURE_KINDS = vocabulary([
|
|
1626
|
+
"malformed_line",
|
|
1627
|
+
"other",
|
|
1628
|
+
"payload_rejected",
|
|
1629
|
+
"read_error",
|
|
1630
|
+
"size_budget",
|
|
1631
|
+
"symlink"
|
|
1632
|
+
]);
|
|
1459
1633
|
|
|
1460
1634
|
// packages/ceal-client-protocol/src/client-result-materialization.ts
|
|
1461
1635
|
var CEAL_UNREAD_REASONS = vocabulary(["blocked", "unavailable", "too_large", "unsupported", "download_failed", "permission_denied"]);
|
|
1462
|
-
var
|
|
1463
|
-
var
|
|
1464
|
-
var FIELDS = /* @__PURE__ */ new Set([...REQUIRED_FIELDS2, "read_argv", "local_result_root", "continue_argv", "model_disclosure", "item_read_argv"]);
|
|
1636
|
+
var REQUIRED_FIELDS2 = Object.freeze(["schema_version", "operation_id", "path", "sha256"]);
|
|
1637
|
+
var FIELDS = /* @__PURE__ */ new Set([...REQUIRED_FIELDS2, "items_path", "row_count", "result_complete", "coverage", "local_result_root", "model_disclosure", "continue_argv", "how_to_read"]);
|
|
1465
1638
|
|
|
1466
1639
|
// packages/ceal-client-protocol/src/gateway-cache-origin-validation.ts
|
|
1467
1640
|
var CEAL_MAX_CACHE_ORIGIN_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
@@ -1491,14 +1664,6 @@ var CEAL_GATEWAY_ANNOUNCEMENT_POLICY_NON_CLAIMS = vocabulary([
|
|
|
1491
1664
|
"target_specific_scope_not_declared"
|
|
1492
1665
|
]);
|
|
1493
1666
|
|
|
1494
|
-
// packages/ceal-client-protocol/src/gateway-write-identity.ts
|
|
1495
|
-
var CEAL_GATEWAY_WRITE_IDENTITY_ROLES = Object.freeze(["replay_identity", "lookup_handle", "collision_evidence"]);
|
|
1496
|
-
var CEAL_GATEWAY_WRITE_IDENTITY_FIELDS = Object.freeze({
|
|
1497
|
-
replay_identity: "idempotency_claim_sha256",
|
|
1498
|
-
lookup_handle: "write_request_sha256",
|
|
1499
|
-
collision_evidence: "normalized_mutation_sha256"
|
|
1500
|
-
});
|
|
1501
|
-
|
|
1502
1667
|
// packages/ceal-client-protocol/src/gateway-scoped-identity-projection-validation.ts
|
|
1503
1668
|
var PROJECTION_REVISION_KEYS = Object.freeze(["graph_revision", "subject_key_revision", "projection_revision"]);
|
|
1504
1669
|
|
|
@@ -1511,6 +1676,18 @@ var CEAL_SUPPORTED_GATEWAY_PROTOCOL_RANGE = Object.freeze({
|
|
|
1511
1676
|
// packages/ceal-client-protocol/src/admin-projection-vocabulary.ts
|
|
1512
1677
|
var ADMIN_PROJECTION_STATES = vocabulary(["ready", "empty", "partial", "unknown"]);
|
|
1513
1678
|
var ADMIN_VIEW_KINDS = vocabulary(["request_detail", "resource_access_timeline", "weekly_usage"]);
|
|
1679
|
+
var ADMIN_RECORD_PROJECTION_KINDS = vocabulary([
|
|
1680
|
+
"request_list",
|
|
1681
|
+
"usage_series",
|
|
1682
|
+
"connector_health",
|
|
1683
|
+
"person_connections",
|
|
1684
|
+
"client_sessions",
|
|
1685
|
+
"knowledge_base",
|
|
1686
|
+
"change_history",
|
|
1687
|
+
"approval_queue",
|
|
1688
|
+
"access_group_list",
|
|
1689
|
+
"capability_catalogue"
|
|
1690
|
+
]);
|
|
1514
1691
|
var ADMIN_GAP_KINDS = vocabulary(["unknown", "unavailable", "partial"]);
|
|
1515
1692
|
var ADMIN_UNAVAILABLE_REASONS = vocabulary([
|
|
1516
1693
|
"not_recorded",
|
|
@@ -1534,7 +1711,11 @@ var ADMIN_SOURCE_OWNERS = vocabulary([
|
|
|
1534
1711
|
"person",
|
|
1535
1712
|
"resource",
|
|
1536
1713
|
"prompt",
|
|
1537
|
-
"usage"
|
|
1714
|
+
"usage",
|
|
1715
|
+
"connector",
|
|
1716
|
+
"session",
|
|
1717
|
+
"knowledge",
|
|
1718
|
+
"policy"
|
|
1538
1719
|
]);
|
|
1539
1720
|
var ADMIN_COMPLETENESS_STATES = vocabulary(["complete", "partial", "unknown"]);
|
|
1540
1721
|
var ADMIN_OPERATION_OUTCOMES = vocabulary(["completed", "failed", "denied", "cancelled", "unknown"]);
|
|
@@ -1578,14 +1759,19 @@ var SAFE_REF2 = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
|
1578
1759
|
var HOST_PRODUCER_SERVE_COMMAND_PREFIX = ["serve", "--config"];
|
|
1579
1760
|
var HostProducerServiceError = class extends Error {
|
|
1580
1761
|
code;
|
|
1581
|
-
constructor(
|
|
1582
|
-
super(message ===
|
|
1762
|
+
constructor(code2, message = code2) {
|
|
1763
|
+
super(message === code2 ? code2 : `${code2}: ${message}`);
|
|
1583
1764
|
this.name = "HostProducerServiceError";
|
|
1584
|
-
this.code =
|
|
1765
|
+
this.code = code2;
|
|
1585
1766
|
}
|
|
1586
1767
|
};
|
|
1587
1768
|
var HOST_PRODUCER_CONFIG_KEYS = ["schema_version", "host_source_ref", "producer_installation_ref", "producer_epoch", "outbox_dir", "relay_endpoint"];
|
|
1588
1769
|
var HOST_PRODUCER_TIMING_DEFAULTS = Object.freeze({ reconcile_interval_ms: 250, retry_backoff_ms: 1e3, ipc_timeout_ms: 5e3 });
|
|
1770
|
+
function hostProducerExtraPath(extras, key) {
|
|
1771
|
+
const value = extras[key];
|
|
1772
|
+
if (value === void 0) throw hostProducerError("invalid_config");
|
|
1773
|
+
return value;
|
|
1774
|
+
}
|
|
1589
1775
|
function parseHostProducerServeArgs(args, platform = process.platform) {
|
|
1590
1776
|
return parseProducerServeArgs(args, platform, hostProducerError);
|
|
1591
1777
|
}
|
|
@@ -1612,8 +1798,8 @@ async function serveHostProducer(descriptor, config, signal, dependencies = {})
|
|
|
1612
1798
|
async function runHostProducerCli(descriptor, args, signal, dependencies = {}) {
|
|
1613
1799
|
await serveHostProducer(descriptor, await readHostProducerConfig(descriptor, parseHostProducerServeArgs(args)), signal, dependencies);
|
|
1614
1800
|
}
|
|
1615
|
-
function hostProducerError(
|
|
1616
|
-
return new HostProducerServiceError(
|
|
1801
|
+
function hostProducerError(code2, message = code2) {
|
|
1802
|
+
return new HostProducerServiceError(code2, message);
|
|
1617
1803
|
}
|
|
1618
1804
|
function parseProducerServeArgs(args, platform, error) {
|
|
1619
1805
|
if (args.length !== HOST_PRODUCER_SERVE_COMMAND_PREFIX.length + 1 || args[0] !== HOST_PRODUCER_SERVE_COMMAND_PREFIX[0] || args[1] !== HOST_PRODUCER_SERVE_COMMAND_PREFIX[1]) throw error("invalid_arguments", refusalFact("args", args, "serve --config <absolute path>"));
|
|
@@ -1764,11 +1950,20 @@ var FRAME_KINDS = new Set(HOST_PRODUCER_FRAME_KINDS);
|
|
|
1764
1950
|
function isHostProducerFrameKind(value) {
|
|
1765
1951
|
return typeof value === "string" && FRAME_KINDS.has(value);
|
|
1766
1952
|
}
|
|
1953
|
+
var HOST_PRODUCER_FRAME_ERROR_CODES = [
|
|
1954
|
+
"invalid_frame_encoding",
|
|
1955
|
+
"invalid_frame_length",
|
|
1956
|
+
"frame_truncated",
|
|
1957
|
+
"unsupported_frame_schema",
|
|
1958
|
+
"invalid_frame_shape",
|
|
1959
|
+
"invalid_producer_identity",
|
|
1960
|
+
"frame_too_large"
|
|
1961
|
+
];
|
|
1767
1962
|
var HostProducerFrameError = class extends Error {
|
|
1768
1963
|
code;
|
|
1769
|
-
constructor(
|
|
1964
|
+
constructor(code2, message = code2) {
|
|
1770
1965
|
super(message);
|
|
1771
|
-
this.code =
|
|
1966
|
+
this.code = code2;
|
|
1772
1967
|
}
|
|
1773
1968
|
};
|
|
1774
1969
|
function encodeHostProducerFrame(frame) {
|
|
@@ -1798,6 +1993,9 @@ function decodeHostProducerFrame(input) {
|
|
|
1798
1993
|
validateFrame(value);
|
|
1799
1994
|
return value;
|
|
1800
1995
|
}
|
|
1996
|
+
function isHostSafeRef(value) {
|
|
1997
|
+
return safeRef(value);
|
|
1998
|
+
}
|
|
1801
1999
|
function validateFrame(value) {
|
|
1802
2000
|
if (!record(value)) throw new HostProducerFrameError("invalid_frame_shape");
|
|
1803
2001
|
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)}`);
|
|
@@ -1885,6 +2083,15 @@ function exactOptional(value, required, optional) {
|
|
|
1885
2083
|
return required.every((key) => keys.includes(key)) && keys.every((key) => required.includes(key) || optional.includes(key));
|
|
1886
2084
|
}
|
|
1887
2085
|
|
|
2086
|
+
// host/relay/src/host-producer-core.ts
|
|
2087
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2088
|
+
import { O_NOFOLLOW, O_RDONLY as O_RDONLY2 } from "node:constants";
|
|
2089
|
+
import { mkdir as mkdir2, open as open3, readFile, rename as rename2, writeFile } from "node:fs/promises";
|
|
2090
|
+
import { join as join2 } from "node:path";
|
|
2091
|
+
|
|
2092
|
+
// host/relay/src/relay-ipc.ts
|
|
2093
|
+
import { createConnection, createServer } from "node:net";
|
|
2094
|
+
|
|
1888
2095
|
// host/relay/src/durable-atomic-replacement.ts
|
|
1889
2096
|
import { O_RDONLY } from "node:constants";
|
|
1890
2097
|
import { lstat as lstat2, mkdir, open as open2, rename, unlink } from "node:fs/promises";
|
|
@@ -1973,17 +2180,6 @@ function errorCode(error) {
|
|
|
1973
2180
|
// host/relay/src/relay-request-binding-store.ts
|
|
1974
2181
|
var MAX_STATE_BYTES = 8 * 1024 * 1024;
|
|
1975
2182
|
|
|
1976
|
-
// host/relay/src/relay-quarantine-store.ts
|
|
1977
|
-
var RELAY_QUARANTINE_MAX_BYTES = 16 * 1024 * 1024;
|
|
1978
|
-
|
|
1979
|
-
// host/relay/src/relay-admission-store.ts
|
|
1980
|
-
var DEFAULT_LIMITS = { global_count: 8192, global_bytes: 64 * 1024 * 1024, producer_count: 4096, producer_bytes: 32 * 1024 * 1024 };
|
|
1981
|
-
var RETRY_SECONDS = [1, 2, 4, 8, 16, 32, 60];
|
|
1982
|
-
var MAX_RETRYABLE_ATTEMPTS = RETRY_SECONDS.length + 10;
|
|
1983
|
-
|
|
1984
|
-
// host/relay/src/relay-ipc.ts
|
|
1985
|
-
import { createConnection, createServer } from "node:net";
|
|
1986
|
-
|
|
1987
2183
|
// host/relay/src/current-prompt-custody.ts
|
|
1988
2184
|
var HOST_CURRENT_PROMPT_MAX_BYTES = 256 * 1024;
|
|
1989
2185
|
|
|
@@ -2004,6 +2200,8 @@ var HOST_RELAY_REQUEST_BINDING_KEYS = Object.freeze([
|
|
|
2004
2200
|
"host_input_ref"
|
|
2005
2201
|
]);
|
|
2006
2202
|
var HOST_RELAY_IPC_RESPONSE_SCHEMA = "ceal.host_relay_admission.v1";
|
|
2203
|
+
var HOST_RELAY_CURRENT_PROMPT_REGISTER_SCHEMA = "ceal.host_relay_current_prompt_register.v1";
|
|
2204
|
+
var HOST_RELAY_CURRENT_PROMPT_RESULT_SCHEMA = "ceal.host_relay_current_prompt_result.v1";
|
|
2007
2205
|
var HOST_RELAY_IPC_LENGTH_BYTES = Uint32Array.BYTES_PER_ELEMENT;
|
|
2008
2206
|
var HOST_RELAY_ADMISSION_RESPONSE_MAX_BYTES = 4096;
|
|
2009
2207
|
var HOST_RELAY_INVOCATION_MINT_REFUSAL_REASONS = vocabulary([
|
|
@@ -2014,10 +2212,31 @@ var HOST_RELAY_INVOCATION_MINT_REFUSAL_REASONS = vocabulary([
|
|
|
2014
2212
|
"gateway_unavailable",
|
|
2015
2213
|
"gateway_refused",
|
|
2016
2214
|
"gateway_response_invalid",
|
|
2215
|
+
"gateway_rate_limited",
|
|
2216
|
+
"producer_frame_invalid",
|
|
2017
2217
|
"deadline",
|
|
2018
2218
|
"cancelled",
|
|
2019
2219
|
"mint_failed",
|
|
2020
|
-
"relay_failed"
|
|
2220
|
+
"relay_failed",
|
|
2221
|
+
"request_authority_unavailable",
|
|
2222
|
+
"request_missing",
|
|
2223
|
+
"authority_mismatch",
|
|
2224
|
+
"host_source_mismatch",
|
|
2225
|
+
"host_session_mismatch",
|
|
2226
|
+
"host_turn_mismatch",
|
|
2227
|
+
"host_input_mismatch",
|
|
2228
|
+
"host_prompt_mismatch"
|
|
2229
|
+
]);
|
|
2230
|
+
var HOST_RELAY_DIRECT_WIRE_MINT_REASONS = vocabulary([
|
|
2231
|
+
"context_stale",
|
|
2232
|
+
"context_expired",
|
|
2233
|
+
"host_source_refused",
|
|
2234
|
+
"host_credential_refused",
|
|
2235
|
+
"gateway_unavailable",
|
|
2236
|
+
"gateway_rate_limited",
|
|
2237
|
+
"gateway_refused",
|
|
2238
|
+
"gateway_response_invalid",
|
|
2239
|
+
"producer_frame_invalid"
|
|
2021
2240
|
]);
|
|
2022
2241
|
var HOST_RELAY_INVOCATION_MINT_OUTCOMES = vocabulary(["completed", "caller_cancelled", "deadline", "failed", "no_binding"]);
|
|
2023
2242
|
var HOST_RELAY_EXCHANGE_FAILURES = vocabulary([
|
|
@@ -2034,12 +2253,15 @@ var HostRelayIpcError = class extends Error {
|
|
|
2034
2253
|
code;
|
|
2035
2254
|
/** Set only when the client-side socket exchange itself failed. */
|
|
2036
2255
|
failure;
|
|
2037
|
-
constructor(
|
|
2256
|
+
constructor(code2, message = code2, failure) {
|
|
2038
2257
|
super(message);
|
|
2039
|
-
this.code =
|
|
2258
|
+
this.code = code2;
|
|
2040
2259
|
this.failure = failure;
|
|
2041
2260
|
}
|
|
2042
2261
|
};
|
|
2262
|
+
function hostRelayExchangeFailure(error) {
|
|
2263
|
+
return error instanceof HostRelayIpcError ? error.failure : void 0;
|
|
2264
|
+
}
|
|
2043
2265
|
function socketErrorFailure(error) {
|
|
2044
2266
|
switch (errorCode2(error)) {
|
|
2045
2267
|
case "EACCES":
|
|
@@ -2066,6 +2288,19 @@ var HostRelayIpcAdmissionPort = class {
|
|
|
2066
2288
|
return { code: response2.code, admitted_through_sequence: response2.admitted_through_sequence, delivered_through_sequence: response2.delivered_through_sequence };
|
|
2067
2289
|
}
|
|
2068
2290
|
};
|
|
2291
|
+
var HostRelayCurrentPromptPort = class {
|
|
2292
|
+
endpoint;
|
|
2293
|
+
timeout_ms;
|
|
2294
|
+
constructor(endpoint, timeoutMs = 5e3) {
|
|
2295
|
+
this.endpoint = endpoint;
|
|
2296
|
+
this.timeout_ms = timeoutMs;
|
|
2297
|
+
}
|
|
2298
|
+
async register(input) {
|
|
2299
|
+
const request2 = { schema_version: HOST_RELAY_CURRENT_PROMPT_REGISTER_SCHEMA, ...input };
|
|
2300
|
+
const response2 = await exchange(this.endpoint, encodeLengthPrefixed(request2), this.timeout_ms, decodeCurrentPromptResponseBuffer);
|
|
2301
|
+
if (response2.status !== "registered") throw new HostRelayIpcError("relay_unavailable", response2.status);
|
|
2302
|
+
}
|
|
2303
|
+
};
|
|
2069
2304
|
function submitHostProducerFrame(endpoint, frame, timeoutMs = 5e3) {
|
|
2070
2305
|
return exchange(endpoint, encodeHostProducerFrame(frame), timeoutMs, decodeResponseBuffer);
|
|
2071
2306
|
}
|
|
@@ -2144,276 +2379,235 @@ function decodeResponseBuffer(buffer) {
|
|
|
2144
2379
|
if (buffer.length !== length + HOST_RELAY_IPC_LENGTH_BYTES) throw new HostRelayIpcError("relay_unavailable", `Host Relay IPC returned trailing bytes: ${refusalFacts([{ name: "declared_bytes", value: length, expected: `the received message length minus ${HOST_RELAY_IPC_LENGTH_BYTES}` }, { name: "received_bytes", value: buffer.byteLength, expected: `exactly ${length + HOST_RELAY_IPC_LENGTH_BYTES}` }])}.`);
|
|
2145
2380
|
return decodeHostRelayIpcResponse(buffer);
|
|
2146
2381
|
}
|
|
2147
|
-
function
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
import { mkdir as mkdir3, readFile as readFile2, rename as rename2, writeFile } from "node:fs/promises";
|
|
2153
|
-
import { join as join3 } from "node:path";
|
|
2154
|
-
|
|
2155
|
-
// host/claude/src/claude-outbox.ts
|
|
2156
|
-
import { open as open3, mkdir as mkdir2, readdir, readFile, unlink as unlink2 } from "node:fs/promises";
|
|
2157
|
-
import { join as join2 } from "node:path";
|
|
2158
|
-
|
|
2159
|
-
// host/claude/src/claude-types.ts
|
|
2160
|
-
var CLAUDE_HOST_ERROR_CODES = ["invalid_input", "storage", "configuration"];
|
|
2161
|
-
function isClaudeHostErrorCode(value) {
|
|
2162
|
-
return typeof value === "string" && CLAUDE_HOST_ERROR_CODES.some((code3) => code3 === value);
|
|
2382
|
+
function decodeCurrentPromptResponseBuffer(buffer) {
|
|
2383
|
+
const value = decodeLengthPrefixedJson(buffer, HOST_RELAY_ADMISSION_RESPONSE_MAX_BYTES);
|
|
2384
|
+
if (value === null) return null;
|
|
2385
|
+
if (!record2(value) || !exactKeys2(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");
|
|
2386
|
+
return { schema_version: HOST_RELAY_CURRENT_PROMPT_RESULT_SCHEMA, status: value.status };
|
|
2163
2387
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2388
|
+
function decodeLengthPrefixedJson(input, maximum) {
|
|
2389
|
+
if (input.length < HOST_RELAY_IPC_LENGTH_BYTES) return null;
|
|
2390
|
+
const length = input.readUInt32BE(0);
|
|
2391
|
+
if (length === 0 || length > maximum) throw new HostRelayIpcError("relay_unavailable", `Host Relay IPC message length is invalid: ${refusalFact("declared_bytes", length, `between 1 and ${maximum}`)}.`);
|
|
2392
|
+
if (input.length < length + HOST_RELAY_IPC_LENGTH_BYTES) return null;
|
|
2393
|
+
if (input.length !== length + HOST_RELAY_IPC_LENGTH_BYTES) throw new HostRelayIpcError("relay_unavailable", `Host Relay IPC message has trailing bytes: ${refusalFacts([{ name: "declared_bytes", value: length, expected: `the received message length minus ${HOST_RELAY_IPC_LENGTH_BYTES}` }, { name: "received_bytes", value: input.byteLength, expected: `exactly ${length + HOST_RELAY_IPC_LENGTH_BYTES}` }])}.`);
|
|
2394
|
+
try {
|
|
2395
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.subarray(HOST_RELAY_IPC_LENGTH_BYTES)));
|
|
2396
|
+
} catch {
|
|
2397
|
+
throw new HostRelayIpcError("relay_unavailable", "Host Relay IPC message is invalid.");
|
|
2173
2398
|
}
|
|
2174
|
-
};
|
|
2175
|
-
|
|
2176
|
-
// host/invocation-context.ts
|
|
2177
|
-
var HOST_INVOCATION_IDENTITY_KEYS = ["host_session_ref", "host_turn_ref", "host_source_ref"];
|
|
2178
|
-
var HOST_INVOCATION_CONTEXT_REQUIRED_KEYS = ["schema_version", "identity", "prompt_ref", "expires_at"];
|
|
2179
|
-
function invocationContextKey(identity) {
|
|
2180
|
-
return cealRequestSha256({ host_source_ref: identity.host_source_ref, host_session_ref: identity.host_session_ref });
|
|
2181
|
-
}
|
|
2182
|
-
function hasHostInvocationIdentityKeys(value) {
|
|
2183
|
-
return exactKeys2(value, HOST_INVOCATION_IDENTITY_KEYS);
|
|
2184
|
-
}
|
|
2185
|
-
function hasHostInvocationContextKeys(value, optional = []) {
|
|
2186
|
-
return requiredKeys(value, HOST_INVOCATION_CONTEXT_REQUIRED_KEYS, optional);
|
|
2187
2399
|
}
|
|
2188
|
-
function
|
|
2189
|
-
const
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
...descriptor.working_directory === void 0 ? {} : { working_directory: descriptor.working_directory }
|
|
2195
|
-
};
|
|
2196
|
-
descriptor.validate(context);
|
|
2197
|
-
return Object.freeze(context);
|
|
2400
|
+
function encodeLengthPrefixed(value) {
|
|
2401
|
+
const body = Buffer.from(JSON.stringify(value), "utf8");
|
|
2402
|
+
const output = Buffer.allocUnsafe(body.length + HOST_RELAY_IPC_LENGTH_BYTES);
|
|
2403
|
+
output.writeUInt32BE(body.length, 0);
|
|
2404
|
+
body.copy(output, HOST_RELAY_IPC_LENGTH_BYTES);
|
|
2405
|
+
return output;
|
|
2198
2406
|
}
|
|
2199
|
-
function exactKeys2(value,
|
|
2200
|
-
const
|
|
2201
|
-
return
|
|
2407
|
+
function exactKeys2(value, required, optional = []) {
|
|
2408
|
+
const keys = Object.keys(value);
|
|
2409
|
+
return required.every((key) => Object.hasOwn(value, key)) && keys.every((key) => required.includes(key) || optional.includes(key));
|
|
2202
2410
|
}
|
|
2203
|
-
function
|
|
2204
|
-
|
|
2205
|
-
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
2206
|
-
return actual.length >= required.length && actual.length <= allowed.size && required.every((key) => Object.hasOwn(value, key)) && actual.every((key) => allowed.has(key));
|
|
2411
|
+
function errorCode2(error) {
|
|
2412
|
+
return record2(error) && typeof error.code === "string" ? error.code : null;
|
|
2207
2413
|
}
|
|
2208
2414
|
|
|
2209
|
-
// host/
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2415
|
+
// host/relay/src/host-producer-core.ts
|
|
2416
|
+
var HOST_TRANSCRIPT_CURSOR_REASONS = vocabulary(["transcript_replaced", "transcript_truncated", "transcript_rewritten", "transcript_malformed"]);
|
|
2417
|
+
function isHostTranscriptCursorReason(value) {
|
|
2418
|
+
return typeof value === "string" && HOST_TRANSCRIPT_CURSOR_REASONS.some((reason) => reason === value);
|
|
2419
|
+
}
|
|
2420
|
+
var TRANSCRIPT_MISSING_REASON = "transcript_missing";
|
|
2421
|
+
var HOST_TRANSCRIPT_READ_CHUNK_BYTES = 256 * 1024;
|
|
2422
|
+
async function readTranscriptCompleteChunk(handle, offset, size) {
|
|
2423
|
+
const available = Math.max(0, size - offset);
|
|
2424
|
+
if (available === 0) return "empty";
|
|
2425
|
+
const chunks = [];
|
|
2426
|
+
let position = offset;
|
|
2427
|
+
let total = 0;
|
|
2428
|
+
let lastNewline = -1;
|
|
2429
|
+
while (position < size) {
|
|
2430
|
+
const buffer = Buffer.alloc(Math.min(HOST_TRANSCRIPT_READ_CHUNK_BYTES, size - position));
|
|
2431
|
+
const read = await handle.read(buffer, 0, buffer.length, position);
|
|
2432
|
+
if (read.bytesRead === 0) break;
|
|
2433
|
+
const chunk = buffer.subarray(0, read.bytesRead);
|
|
2434
|
+
chunks.push(chunk);
|
|
2435
|
+
const newline = chunk.lastIndexOf(10);
|
|
2436
|
+
if (newline >= 0) lastNewline = total + newline;
|
|
2437
|
+
total += chunk.length;
|
|
2438
|
+
position += chunk.length;
|
|
2439
|
+
if (lastNewline >= 0) break;
|
|
2440
|
+
}
|
|
2441
|
+
if (lastNewline < 0) return "partial";
|
|
2442
|
+
return Buffer.concat(chunks, total).subarray(0, lastNewline + 1);
|
|
2443
|
+
}
|
|
2444
|
+
async function readTranscriptAnchor(handle, offset, anchorBytes) {
|
|
2445
|
+
const start = Math.max(0, offset - anchorBytes);
|
|
2446
|
+
const anchor = Buffer.alloc(offset - start);
|
|
2447
|
+
if (anchor.length > 0) await handle.read(anchor, 0, anchor.length, start);
|
|
2448
|
+
return { start, sha256: transcriptSha256(anchor) };
|
|
2449
|
+
}
|
|
2450
|
+
async function transcriptAnchorMatches(handle, cursor) {
|
|
2451
|
+
const buffer = Buffer.alloc(cursor.offset - cursor.anchor_start);
|
|
2452
|
+
if (buffer.length > 0) await handle.read(buffer, 0, buffer.length, cursor.anchor_start);
|
|
2453
|
+
return transcriptSha256(buffer) === cursor.anchor_sha256;
|
|
2454
|
+
}
|
|
2455
|
+
function transcriptCursorIdentityReason(cursor, transcriptPath, device, inode) {
|
|
2456
|
+
if (cursor.device !== device || cursor.inode !== inode || cursor.transcript_path !== transcriptPath) return "transcript_replaced";
|
|
2457
|
+
return null;
|
|
2227
2458
|
}
|
|
2228
|
-
function
|
|
2229
|
-
if (
|
|
2230
|
-
if (
|
|
2459
|
+
async function transcriptCursorBoundsReason(handle, cursor, size) {
|
|
2460
|
+
if (size < cursor.offset) return "transcript_truncated";
|
|
2461
|
+
if (cursor.offset > 0 && !await transcriptAnchorMatches(handle, cursor)) return "transcript_rewritten";
|
|
2462
|
+
return null;
|
|
2231
2463
|
}
|
|
2232
|
-
function
|
|
2233
|
-
return
|
|
2464
|
+
function transcriptSha256(value) {
|
|
2465
|
+
return createHash3("sha256").update(value).digest("hex");
|
|
2234
2466
|
}
|
|
2235
|
-
function
|
|
2236
|
-
return value
|
|
2467
|
+
function transcriptObjectSha256(value) {
|
|
2468
|
+
return createHash3("sha256").update(JSON.stringify(value), "utf8").digest("hex");
|
|
2237
2469
|
}
|
|
2238
|
-
function
|
|
2239
|
-
return value ===
|
|
2470
|
+
function isTranscriptRecord(value) {
|
|
2471
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2240
2472
|
}
|
|
2241
|
-
function
|
|
2242
|
-
return
|
|
2473
|
+
function isMissingTranscriptError(error) {
|
|
2474
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2243
2475
|
}
|
|
2244
|
-
function
|
|
2245
|
-
|
|
2476
|
+
function transcriptTimestamp(now, fail) {
|
|
2477
|
+
const value = now();
|
|
2478
|
+
if (!Number.isFinite(value.getTime())) throw fail();
|
|
2479
|
+
return value.toISOString();
|
|
2246
2480
|
}
|
|
2247
|
-
function
|
|
2248
|
-
return
|
|
2481
|
+
function transcriptExpiration(now, retentionMs, fail) {
|
|
2482
|
+
return new Date(Date.parse(transcriptTimestamp(now, fail)) + retentionMs).toISOString();
|
|
2249
2483
|
}
|
|
2250
|
-
function
|
|
2251
|
-
|
|
2484
|
+
function createTranscriptWatcherBase(options, owner) {
|
|
2485
|
+
if (typeof options.outbox_dir !== "string" || options.outbox_dir.length === 0) throw owner.invalid_outbox();
|
|
2486
|
+
return {
|
|
2487
|
+
outboxRetentionMs: options.retention_ms ?? owner.default_retention_ms,
|
|
2488
|
+
now: options.now ?? (() => /* @__PURE__ */ new Date()),
|
|
2489
|
+
log: options.log ?? ((line) => {
|
|
2490
|
+
process.stderr.write(`${line}
|
|
2491
|
+
`);
|
|
2492
|
+
}),
|
|
2493
|
+
producerInstallationRef: options.producer_installation_ref
|
|
2494
|
+
};
|
|
2252
2495
|
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
if (
|
|
2262
|
-
this.directory = directory;
|
|
2263
|
-
this.entries = join2(directory, "entries");
|
|
2264
|
-
this.activeContexts = join2(directory, "active-contexts");
|
|
2265
|
-
}
|
|
2266
|
-
async prepare() {
|
|
2267
|
-
await mkdir2(this.entries, { recursive: true, mode: 448 });
|
|
2268
|
-
await mkdir2(this.activeContexts, { recursive: true, mode: 448 });
|
|
2496
|
+
async function scanTranscriptRegistrations(registrations, scanOne) {
|
|
2497
|
+
let records = 0;
|
|
2498
|
+
let enqueued = 0;
|
|
2499
|
+
let quarantined = 0;
|
|
2500
|
+
for (const registration of registrations) {
|
|
2501
|
+
const result = await scanOne(registration);
|
|
2502
|
+
records += result.records;
|
|
2503
|
+
enqueued += result.enqueued;
|
|
2504
|
+
if (result.quarantined) quarantined += 1;
|
|
2269
2505
|
}
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2506
|
+
return { registrations: registrations.length, records, enqueued, quarantined };
|
|
2507
|
+
}
|
|
2508
|
+
function formatRetiredTranscriptLog(producerLabel, transcriptPath) {
|
|
2509
|
+
return `${producerLabel} retired a transcript registration: transcript_path=${JSON.stringify(transcriptPath)}, reason=${TRANSCRIPT_MISSING_REASON}.`;
|
|
2510
|
+
}
|
|
2511
|
+
var HostTranscriptWatcher = class {
|
|
2512
|
+
outboxRetentionMs;
|
|
2513
|
+
now;
|
|
2514
|
+
log;
|
|
2515
|
+
producerInstallationRef;
|
|
2516
|
+
clockFailure;
|
|
2517
|
+
constructor(options, owner) {
|
|
2518
|
+
const base = createTranscriptWatcherBase(options, owner);
|
|
2519
|
+
this.outboxRetentionMs = base.outboxRetentionMs;
|
|
2520
|
+
this.now = base.now;
|
|
2521
|
+
this.log = base.log;
|
|
2522
|
+
this.producerInstallationRef = base.producerInstallationRef;
|
|
2523
|
+
this.clockFailure = owner.clock_failure;
|
|
2274
2524
|
}
|
|
2275
|
-
async
|
|
2276
|
-
|
|
2277
|
-
await this.prepare();
|
|
2278
|
-
return readInvocationContextFile(this.activeContextPath({ host_source_ref: hostSourceRef, host_session_ref: hostSessionRef }), hostSourceRef, hostSessionRef, now);
|
|
2525
|
+
async scanOnce() {
|
|
2526
|
+
return scanTranscriptRegistrations(await this.listRegistrations(), (registration) => this.scanRegistration(registration));
|
|
2279
2527
|
}
|
|
2280
|
-
async
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
try {
|
|
2286
|
-
const value = JSON.parse(await readFile(join2(this.activeContexts, name), "utf8"));
|
|
2287
|
-
validateClaudeHostInvocationContext(value);
|
|
2288
|
-
if (Date.parse(value.expires_at) > now.getTime()) contexts.push(value);
|
|
2289
|
-
} catch {
|
|
2290
|
-
}
|
|
2291
|
-
}
|
|
2292
|
-
return contexts;
|
|
2528
|
+
async scanRegistration(registration) {
|
|
2529
|
+
return this.openTranscript(registration, (handle) => this.scanOpened(handle, registration), {
|
|
2530
|
+
missing: () => this.retireMissing(registration),
|
|
2531
|
+
failed: (handle, error) => this.scanFailed(handle, registration, error)
|
|
2532
|
+
});
|
|
2293
2533
|
}
|
|
2294
|
-
async
|
|
2295
|
-
|
|
2296
|
-
await this.prepare();
|
|
2297
|
-
const path = this.path(batch.batch_ref);
|
|
2534
|
+
async openTranscript(registration, scan, handlers) {
|
|
2535
|
+
let handle;
|
|
2298
2536
|
try {
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
await handle.writeFile(JSON.stringify(batch));
|
|
2302
|
-
await handle.sync();
|
|
2303
|
-
} finally {
|
|
2304
|
-
await handle.close();
|
|
2305
|
-
}
|
|
2537
|
+
handle = await open3(this.transcriptPathOf(registration), O_RDONLY2 | O_NOFOLLOW);
|
|
2538
|
+
return await scan(handle);
|
|
2306
2539
|
} catch (error) {
|
|
2307
|
-
if (
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2540
|
+
if (handle === void 0 && isMissingTranscriptError(error)) return handlers.missing();
|
|
2541
|
+
if (hostRelayExchangeFailure(error) !== void 0) throw error;
|
|
2542
|
+
return handlers.failed(handle, error);
|
|
2543
|
+
} finally {
|
|
2544
|
+
await handle?.close();
|
|
2311
2545
|
}
|
|
2312
2546
|
}
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
malformed += 1;
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
2327
|
-
batches.sort(compareBatch);
|
|
2328
|
-
return { entries: batches, malformed };
|
|
2547
|
+
/**
|
|
2548
|
+
* A transcript the Host deleted never comes back, so retrying its
|
|
2549
|
+
* registration once per reconcile interval is a failure with no end.
|
|
2550
|
+
* Retire it at the scan that finds it missing and say so once. Only
|
|
2551
|
+
* ENOENT means gone; a permission or busy failure is about this moment,
|
|
2552
|
+
* not about the file, and keeps the registration for the next scan.
|
|
2553
|
+
*/
|
|
2554
|
+
async retireMissingTranscript(transcriptPath, producerLabel, retire) {
|
|
2555
|
+
await retire();
|
|
2556
|
+
this.log(formatRetiredTranscriptLog(producerLabel, transcriptPath));
|
|
2557
|
+
return { records: 0, enqueued: 0, quarantined: false };
|
|
2329
2558
|
}
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
await unlink2(this.path(batch.batch_ref));
|
|
2333
|
-
} catch (error) {
|
|
2334
|
-
if (code(error) !== "ENOENT") throw error;
|
|
2335
|
-
}
|
|
2559
|
+
stamp() {
|
|
2560
|
+
return transcriptTimestamp(this.now, this.clockFailure);
|
|
2336
2561
|
}
|
|
2337
|
-
|
|
2338
|
-
return
|
|
2562
|
+
expire(retentionMs) {
|
|
2563
|
+
return transcriptExpiration(this.now, retentionMs, this.clockFailure);
|
|
2339
2564
|
}
|
|
2340
|
-
|
|
2341
|
-
return
|
|
2565
|
+
sha(value) {
|
|
2566
|
+
return transcriptSha256(value);
|
|
2342
2567
|
}
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
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.");
|
|
2346
|
-
}
|
|
2347
|
-
async function readInvocationContextFile(file, hostSourceRef, hostSessionRef, now) {
|
|
2348
|
-
try {
|
|
2349
|
-
const context = JSON.parse(await readFile(file, "utf8"));
|
|
2350
|
-
validateClaudeHostInvocationContext(context);
|
|
2351
|
-
return contextMatches(context, hostSourceRef, hostSessionRef) && Date.parse(context.expires_at) > now.getTime() ? context : null;
|
|
2352
|
-
} catch (error) {
|
|
2353
|
-
if (code(error) === "ENOENT") return null;
|
|
2354
|
-
if (error instanceof ClaudeHostError) throw error;
|
|
2355
|
-
throw new ClaudeHostError("storage", "Unable to read the Claude Host invocation context.");
|
|
2568
|
+
objectSha(value) {
|
|
2569
|
+
return transcriptObjectSha256(value);
|
|
2356
2570
|
}
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
return context.identity.host_source_ref === hostSourceRef && context.identity.host_session_ref === hostSessionRef;
|
|
2360
|
-
}
|
|
2361
|
-
function compareBatch(left, right) {
|
|
2362
|
-
return rank(left.hook_event) - rank(right.hook_event) || left.occurred_at.localeCompare(right.occurred_at) || left.batch_ref.localeCompare(right.batch_ref);
|
|
2363
|
-
}
|
|
2364
|
-
function rank(event) {
|
|
2365
|
-
return event === "SessionStart" ? 0 : event === "UserPromptSubmit" ? 1 : 2;
|
|
2366
|
-
}
|
|
2367
|
-
function validateBatch(value) {
|
|
2368
|
-
if (!validBatchHeader(value)) throw new ClaudeHostError("storage", "Claude hook batch is malformed.");
|
|
2369
|
-
for (const [index, draft] of value.frames.entries()) {
|
|
2370
|
-
if (!record4(draft) || !hostFrameKind(draft.frame_kind) || !record4(draft.payload)) throw new ClaudeHostError("storage", "Claude frame draft is malformed.");
|
|
2371
|
-
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 });
|
|
2571
|
+
isRecord(value) {
|
|
2572
|
+
return isTranscriptRecord(value);
|
|
2372
2573
|
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
}
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2394
|
-
}
|
|
2395
|
-
function code(error) {
|
|
2396
|
-
return record4(error) && typeof error.code === "string" ? error.code : null;
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
// host/claude/src/claude-producer.ts
|
|
2400
|
-
var STATE_SCHEMA = "ceal.claude_producer_state.v1";
|
|
2401
|
-
var SAFE_REF5 = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
2402
|
-
var ClaudeProducer = class {
|
|
2403
|
-
store;
|
|
2574
|
+
readChunk(handle, offset, size) {
|
|
2575
|
+
return readTranscriptCompleteChunk(handle, offset, size);
|
|
2576
|
+
}
|
|
2577
|
+
readAnchor(handle, offset, anchorBytes) {
|
|
2578
|
+
return readTranscriptAnchor(handle, offset, anchorBytes);
|
|
2579
|
+
}
|
|
2580
|
+
cursorIdentityReason(cursor, transcriptPath, device, inode) {
|
|
2581
|
+
return transcriptCursorIdentityReason(cursor, transcriptPath, device, inode);
|
|
2582
|
+
}
|
|
2583
|
+
cursorBoundsReason(handle, cursor, size) {
|
|
2584
|
+
return transcriptCursorBoundsReason(handle, cursor, size);
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
var HOST_PRODUCER_STATE_IDENTITY = {
|
|
2588
|
+
codex: { file_name: "codex-producer-state.json", schema: "ceal.codex_producer_state.v1" },
|
|
2589
|
+
claude: { file_name: "claude-producer-state.json", schema: "ceal.claude_producer_state.v1" }
|
|
2590
|
+
};
|
|
2591
|
+
var HostProducerDrainCore = class {
|
|
2592
|
+
host;
|
|
2593
|
+
relay;
|
|
2404
2594
|
statePath;
|
|
2405
|
-
|
|
2595
|
+
stateSchema;
|
|
2596
|
+
installationRef;
|
|
2406
2597
|
epoch;
|
|
2407
|
-
|
|
2598
|
+
now;
|
|
2408
2599
|
tail = Promise.resolve();
|
|
2409
|
-
constructor(
|
|
2410
|
-
if (!
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
this.
|
|
2415
|
-
this.statePath =
|
|
2416
|
-
this.
|
|
2600
|
+
constructor(host, runtime) {
|
|
2601
|
+
if (!isHostSafeRef(runtime.producer_installation_ref)) throw host.fail("configuration", `${host.producer_label} producer installation identity is invalid.`);
|
|
2602
|
+
const epoch = runtime.producer_epoch ?? 1;
|
|
2603
|
+
if (!Number.isSafeInteger(epoch) || epoch <= 0) throw host.fail("configuration", `${host.producer_label} producer epoch is invalid.`);
|
|
2604
|
+
this.host = host;
|
|
2605
|
+
this.relay = runtime.relay;
|
|
2606
|
+
this.statePath = join2(host.outbox_directory, HOST_PRODUCER_STATE_IDENTITY[host.producer_id].file_name);
|
|
2607
|
+
this.stateSchema = HOST_PRODUCER_STATE_IDENTITY[host.producer_id].schema;
|
|
2608
|
+
this.installationRef = runtime.producer_installation_ref;
|
|
2609
|
+
this.epoch = epoch;
|
|
2610
|
+
this.now = runtime.now ?? (() => /* @__PURE__ */ new Date());
|
|
2417
2611
|
}
|
|
2418
2612
|
drainOutbox() {
|
|
2419
2613
|
const run = this.tail.then(() => this.drainLocked());
|
|
@@ -2421,14 +2615,21 @@ var ClaudeProducer = class {
|
|
|
2421
2615
|
return run;
|
|
2422
2616
|
}
|
|
2423
2617
|
async drainLocked() {
|
|
2424
|
-
const read = await this.
|
|
2618
|
+
const read = await this.host.listEnvelopes();
|
|
2425
2619
|
let admitted = 0;
|
|
2620
|
+
let expired = 0;
|
|
2426
2621
|
let retained = 0;
|
|
2427
|
-
for (const
|
|
2428
|
-
const
|
|
2622
|
+
for (const envelope of read.entries) {
|
|
2623
|
+
const reserved = await this.hasReservation(this.host.submissionRefOf(envelope));
|
|
2624
|
+
if (!reserved && Date.parse(this.host.expiresAtOf(envelope)) <= this.now().getTime()) {
|
|
2625
|
+
await this.host.removeEnvelope(envelope);
|
|
2626
|
+
expired += 1;
|
|
2627
|
+
continue;
|
|
2628
|
+
}
|
|
2629
|
+
const frames = await this.reserve(envelope);
|
|
2429
2630
|
let complete = true;
|
|
2430
2631
|
for (const frame of frames) {
|
|
2431
|
-
const outcome = await this.
|
|
2632
|
+
const outcome = await this.relay.admit(frame);
|
|
2432
2633
|
if (outcome.code !== "admitted" && outcome.code !== "already_admitted") {
|
|
2433
2634
|
complete = false;
|
|
2434
2635
|
break;
|
|
@@ -2438,105 +2639,117 @@ var ClaudeProducer = class {
|
|
|
2438
2639
|
retained += 1;
|
|
2439
2640
|
break;
|
|
2440
2641
|
}
|
|
2441
|
-
await this.
|
|
2442
|
-
await this.release(
|
|
2642
|
+
await this.host.removeEnvelope(envelope);
|
|
2643
|
+
await this.release(this.host.submissionRefOf(envelope));
|
|
2443
2644
|
admitted += 1;
|
|
2444
2645
|
}
|
|
2445
|
-
return { admitted, retained: retained + Math.max(0, read.entries.length - admitted - retained), malformed: read.malformed };
|
|
2646
|
+
return { admitted, retained: retained + Math.max(0, read.entries.length - admitted - expired - retained), expired, malformed: read.malformed };
|
|
2446
2647
|
}
|
|
2447
|
-
async
|
|
2648
|
+
async hasReservation(submissionRef2) {
|
|
2649
|
+
return (await this.readState()).pending[submissionRef2] !== void 0;
|
|
2650
|
+
}
|
|
2651
|
+
async reserve(envelope) {
|
|
2448
2652
|
const state = await this.readState();
|
|
2449
|
-
const
|
|
2653
|
+
const submissionRef2 = this.host.submissionRefOf(envelope);
|
|
2654
|
+
const existing = state.pending[submissionRef2];
|
|
2450
2655
|
if (existing !== void 0) return existing;
|
|
2451
|
-
const
|
|
2452
|
-
|
|
2656
|
+
const drafts = await this.host.mapEnvelope(envelope);
|
|
2657
|
+
const frames = drafts.map((draft, offset) => ({
|
|
2658
|
+
schema_version: "ceal.host_producer_frame.v1",
|
|
2659
|
+
producer_id: this.host.producer_id,
|
|
2660
|
+
producer_installation_ref: this.installationRef,
|
|
2661
|
+
producer_epoch: this.epoch,
|
|
2662
|
+
sequence: state.next_sequence + offset,
|
|
2663
|
+
frame_kind: draft.frame_kind,
|
|
2664
|
+
occurred_at: this.host.createdAtOf(envelope),
|
|
2665
|
+
payload: draft.payload
|
|
2666
|
+
}));
|
|
2667
|
+
await this.writeState({ ...state, next_sequence: state.next_sequence + frames.length, pending: { ...state.pending, [submissionRef2]: frames } });
|
|
2453
2668
|
return frames;
|
|
2454
2669
|
}
|
|
2455
|
-
async release(
|
|
2670
|
+
async release(submissionRef2) {
|
|
2456
2671
|
const state = await this.readState();
|
|
2457
|
-
if (state.pending[
|
|
2672
|
+
if (state.pending[submissionRef2] === void 0) return;
|
|
2458
2673
|
const pending = { ...state.pending };
|
|
2459
|
-
delete pending[
|
|
2674
|
+
delete pending[submissionRef2];
|
|
2460
2675
|
await this.writeState({ ...state, pending });
|
|
2461
2676
|
}
|
|
2462
2677
|
async readState() {
|
|
2463
2678
|
try {
|
|
2464
|
-
|
|
2679
|
+
const value = JSON.parse(await readFile(this.statePath, "utf8"));
|
|
2680
|
+
return this.decodeState(value);
|
|
2465
2681
|
} catch (error) {
|
|
2466
|
-
if (
|
|
2467
|
-
return { schema_version:
|
|
2682
|
+
if (errorCode3(error) !== "ENOENT") throw error;
|
|
2683
|
+
return { schema_version: this.stateSchema, producer_installation_ref: this.installationRef, producer_epoch: this.epoch, next_sequence: 1, pending: {} };
|
|
2468
2684
|
}
|
|
2469
2685
|
}
|
|
2470
2686
|
async writeState(state) {
|
|
2471
|
-
await
|
|
2472
|
-
const
|
|
2473
|
-
await writeFile(
|
|
2474
|
-
await rename2(
|
|
2687
|
+
await mkdir2(this.host.outbox_directory, { recursive: true, mode: 448 });
|
|
2688
|
+
const temporary = `${this.statePath}.${process.pid}.new`;
|
|
2689
|
+
await writeFile(temporary, JSON.stringify(state), { mode: 384 });
|
|
2690
|
+
await rename2(temporary, this.statePath);
|
|
2691
|
+
}
|
|
2692
|
+
decodeState(value) {
|
|
2693
|
+
const label = this.host.producer_label;
|
|
2694
|
+
const fail = (code2, message) => this.host.fail(code2, message);
|
|
2695
|
+
if (!isDrainStateHeader(value, this.stateSchema, this.installationRef)) {
|
|
2696
|
+
throw fail("storage", `${label} producer state header is malformed or belongs to another installation (${refusalFacts([
|
|
2697
|
+
{ name: "schema_version", value: isTranscriptRecord(value) ? value.schema_version : void 0, expected: this.stateSchema },
|
|
2698
|
+
{ name: "producer_installation_ref", value: isTranscriptRecord(value) ? value.producer_installation_ref : void 0, expected: this.installationRef }
|
|
2699
|
+
])}).`);
|
|
2700
|
+
}
|
|
2701
|
+
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}`)}).`);
|
|
2702
|
+
const pending = {};
|
|
2703
|
+
const storedEpoch = Number(value.producer_epoch);
|
|
2704
|
+
for (const [submissionRef2, frames] of Object.entries(value.pending)) {
|
|
2705
|
+
if (!isHostSafeRef(submissionRef2) || !Array.isArray(frames) || frames.length === 0) throw fail("storage", `${label} producer pending reservation is malformed.`);
|
|
2706
|
+
pending[submissionRef2] = frames.map((frame) => this.decodeStoredFrame(frame, storedEpoch));
|
|
2707
|
+
}
|
|
2708
|
+
return { schema_version: this.stateSchema, producer_installation_ref: this.installationRef, producer_epoch: this.epoch, next_sequence: Number(value.next_sequence), pending };
|
|
2709
|
+
}
|
|
2710
|
+
decodeStoredFrame(value, storedEpoch) {
|
|
2711
|
+
const label = this.host.producer_label;
|
|
2712
|
+
const json = Buffer.from(JSON.stringify(value), "utf8");
|
|
2713
|
+
const encoded = Buffer.allocUnsafe(json.length + 4);
|
|
2714
|
+
encoded.writeUInt32BE(json.length, 0);
|
|
2715
|
+
json.copy(encoded, 4);
|
|
2716
|
+
const frame = decodeHostProducerFrame(encoded);
|
|
2717
|
+
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([
|
|
2718
|
+
{ name: "producer_id", value: frame.producer_id, expected: this.host.producer_id },
|
|
2719
|
+
{ name: "producer_installation_ref", value: frame.producer_installation_ref, expected: this.installationRef },
|
|
2720
|
+
{ name: "producer_epoch", value: frame.producer_epoch, expected: `at most ${storedEpoch}` }
|
|
2721
|
+
])}).`);
|
|
2722
|
+
return frame;
|
|
2475
2723
|
}
|
|
2476
2724
|
};
|
|
2477
|
-
function
|
|
2478
|
-
|
|
2479
|
-
{ name: "schema_version", value: record5(value) ? value.schema_version : void 0, expected: STATE_SCHEMA },
|
|
2480
|
-
{ name: "producer_installation_ref", value: record5(value) ? value.producer_installation_ref : void 0, expected: installation }
|
|
2481
|
-
])}).`);
|
|
2482
|
-
if (Number(value.producer_epoch) > epoch) throw new ClaudeHostError("storage", `Claude producer state epoch is ahead of this installation (${refusalFact("producer_epoch", value.producer_epoch, `at most ${epoch}`)}).`);
|
|
2483
|
-
const pending = {};
|
|
2484
|
-
for (const [ref, frames] of Object.entries(value.pending)) {
|
|
2485
|
-
if (!SAFE_REF5.test(ref) || !Array.isArray(frames) || frames.length === 0) throw new ClaudeHostError("storage", "Claude producer reservation is malformed.");
|
|
2486
|
-
pending[ref] = frames.map((frame) => decodeFrame(frame, installation, Number(value.producer_epoch)));
|
|
2487
|
-
}
|
|
2488
|
-
return { schema_version: STATE_SCHEMA, producer_installation_ref: installation, producer_epoch: epoch, next_sequence: Number(value.next_sequence), pending };
|
|
2489
|
-
}
|
|
2490
|
-
function validStateHeader(value, installation) {
|
|
2491
|
-
return record5(value) && value.schema_version === STATE_SCHEMA && value.producer_installation_ref === installation && positiveInteger(value.producer_epoch) && positiveInteger(value.next_sequence) && record5(value.pending);
|
|
2492
|
-
}
|
|
2493
|
-
function positiveInteger(value) {
|
|
2494
|
-
return Number.isSafeInteger(value) && Number(value) > 0;
|
|
2495
|
-
}
|
|
2496
|
-
function decodeFrame(value, installation, epoch) {
|
|
2497
|
-
const json = Buffer.from(JSON.stringify(value));
|
|
2498
|
-
const encoded = Buffer.allocUnsafe(json.length + 4);
|
|
2499
|
-
encoded.writeUInt32BE(json.length, 0);
|
|
2500
|
-
json.copy(encoded, 4);
|
|
2501
|
-
const frame = decodeHostProducerFrame(encoded);
|
|
2502
|
-
if (frame.producer_id !== "claude" || frame.producer_installation_ref !== installation || frame.producer_epoch > epoch) throw new ClaudeHostError("storage", `Claude producer frame identity is malformed (${refusalFacts([{ name: "producer_id", value: frame.producer_id, expected: "claude" }, { name: "producer_installation_ref", value: frame.producer_installation_ref, expected: installation }, { name: "producer_epoch", value: frame.producer_epoch, expected: `at most ${epoch}` }])}).`);
|
|
2503
|
-
return frame;
|
|
2725
|
+
function isDrainStateHeader(value, schema, installationRef) {
|
|
2726
|
+
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);
|
|
2504
2727
|
}
|
|
2505
|
-
function
|
|
2506
|
-
return
|
|
2507
|
-
}
|
|
2508
|
-
function code2(error) {
|
|
2509
|
-
return record5(error) && typeof error.code === "string" ? error.code : null;
|
|
2728
|
+
function errorCode3(error) {
|
|
2729
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : null;
|
|
2510
2730
|
}
|
|
2511
2731
|
|
|
2512
|
-
// host/
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2732
|
+
// host/relay/src/relay-quarantine-store.ts
|
|
2733
|
+
var RELAY_QUARANTINE_MAX_BYTES = 16 * 1024 * 1024;
|
|
2734
|
+
var RELAY_QUARANTINE_FRAME_REASONS = ["frame_identity_conflict", "sequence_gap", "gateway_refused"];
|
|
2735
|
+
var RELAY_QUARANTINE_REASONS = [...RELAY_QUARANTINE_FRAME_REASONS, ...HOST_PRODUCER_FRAME_ERROR_CODES];
|
|
2516
2736
|
|
|
2517
|
-
// host/
|
|
2518
|
-
var
|
|
2519
|
-
var
|
|
2520
|
-
|
|
2521
|
-
producer_id: "claude",
|
|
2522
|
-
config: (shared) => shared,
|
|
2523
|
-
runtime_options: (config) => Object.freeze({
|
|
2524
|
-
outbox_dir: config.outbox_dir,
|
|
2525
|
-
producer_installation_ref: config.producer_installation_ref,
|
|
2526
|
-
producer_epoch: config.producer_epoch,
|
|
2527
|
-
relay_endpoint: config.relay_endpoint,
|
|
2528
|
-
timeout_ms: config.ipc_timeout_ms
|
|
2529
|
-
}),
|
|
2530
|
-
drain: drainClaudeProducerOnce
|
|
2531
|
-
};
|
|
2532
|
-
function runClaudeProducerCli(args, signal, dependencies = {}) {
|
|
2533
|
-
return runHostProducerCli(CLAUDE_PRODUCER_DESCRIPTOR, args, signal, dependencies);
|
|
2534
|
-
}
|
|
2737
|
+
// host/relay/src/relay-admission-store.ts
|
|
2738
|
+
var DEFAULT_LIMITS = { global_count: 8192, global_bytes: 64 * 1024 * 1024, producer_count: 4096, producer_bytes: 32 * 1024 * 1024 };
|
|
2739
|
+
var RETRY_SECONDS = [1, 2, 4, 8, 16, 32, 60];
|
|
2740
|
+
var MAX_RETRYABLE_ATTEMPTS = RETRY_SECONDS.length + 10;
|
|
2535
2741
|
|
|
2536
2742
|
// host/service-log.ts
|
|
2537
2743
|
import { copyFile, open as open4, stat, truncate } from "node:fs/promises";
|
|
2538
2744
|
import { isAbsolute } from "node:path";
|
|
2539
2745
|
var HOST_SERVICE_LOG_ENVIRONMENT = "CEAL_HOST_SERVICE_LOG";
|
|
2746
|
+
function writeHostServiceDiagnostic(producer_id, event, failure_kind) {
|
|
2747
|
+
try {
|
|
2748
|
+
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 } : {} })}
|
|
2749
|
+
`);
|
|
2750
|
+
} catch {
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2540
2753
|
var HOST_SERVICE_LOG_MAX_BYTES = 1024 * 1024;
|
|
2541
2754
|
var HOST_SERVICE_LOG_CHECK_INTERVAL_MS = 6e4;
|
|
2542
2755
|
function hostServiceLogPreviousPath(path) {
|
|
@@ -2563,7 +2776,7 @@ function boundHostServiceLog(environment = process.env) {
|
|
|
2563
2776
|
rotateHostServiceLog(path).catch((error) => {
|
|
2564
2777
|
if (reported) return;
|
|
2565
2778
|
reported = true;
|
|
2566
|
-
process.stderr.write(`[host-service-log] rotation_failed code=${
|
|
2779
|
+
process.stderr.write(`[host-service-log] rotation_failed code=${errorCode4(error)}
|
|
2567
2780
|
`);
|
|
2568
2781
|
});
|
|
2569
2782
|
};
|
|
@@ -2574,12 +2787,1180 @@ function boundHostServiceLog(environment = process.env) {
|
|
|
2574
2787
|
}
|
|
2575
2788
|
var HOST_SERVICE_LOG_TAIL_BYTES = 64 * 1024;
|
|
2576
2789
|
function isNotFound(error) {
|
|
2577
|
-
return
|
|
2790
|
+
return errorCode4(error) === "ENOENT";
|
|
2578
2791
|
}
|
|
2579
|
-
function
|
|
2792
|
+
function errorCode4(error) {
|
|
2580
2793
|
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "unknown";
|
|
2581
2794
|
}
|
|
2582
2795
|
|
|
2796
|
+
// host/claude/src/claude-decoder.ts
|
|
2797
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2798
|
+
|
|
2799
|
+
// host/claude/src/claude-types.ts
|
|
2800
|
+
var CLAUDE_HOST_ERROR_CODES = ["invalid_input", "payload_rejected", "storage", "configuration"];
|
|
2801
|
+
function isClaudeHostErrorCode(value) {
|
|
2802
|
+
return typeof value === "string" && CLAUDE_HOST_ERROR_CODES.some((code2) => code2 === value);
|
|
2803
|
+
}
|
|
2804
|
+
var CLAUDE_INVOCATION_CONTEXT_SCHEMA_VERSION = "ceal.claude_invocation_context.v1";
|
|
2805
|
+
var CLAUDE_OUTBOX_SCHEMA_VERSION = "ceal.claude_observation_outbox.v1";
|
|
2806
|
+
var CLAUDE_SESSION_SCHEMA_VERSION = "ceal.claude_session.v1";
|
|
2807
|
+
var CLAUDE_TRANSCRIPT_REGISTRATION_SCHEMA_VERSION = "ceal.claude_transcript_registration.v1";
|
|
2808
|
+
var CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION = "ceal.claude_transcript_cursor.v1";
|
|
2809
|
+
var CLAUDE_DEFAULT_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
2810
|
+
var CLAUDE_TRANSCRIPT_MAX_TURN_MESSAGES = 1024;
|
|
2811
|
+
var CLAUDE_TRANSCRIPT_MAX_SUBAGENT_FILES = 64;
|
|
2812
|
+
var CLAUDE_TRANSCRIPT_ANCHOR_BYTES = 256;
|
|
2813
|
+
var ClaudeHostError = class extends Error {
|
|
2814
|
+
name = "ClaudeHostError";
|
|
2815
|
+
code;
|
|
2816
|
+
diagnostic_kind;
|
|
2817
|
+
constructor(code2, message, diagnostic_kind) {
|
|
2818
|
+
super(message);
|
|
2819
|
+
if (!isClaudeHostErrorCode(code2)) throw new TypeError("Claude Host error code is invalid.");
|
|
2820
|
+
this.code = code2;
|
|
2821
|
+
this.diagnostic_kind = diagnostic_kind;
|
|
2822
|
+
}
|
|
2823
|
+
};
|
|
2824
|
+
|
|
2825
|
+
// host/claude/src/claude-decoder.ts
|
|
2826
|
+
var MAX_HOOK_BYTES = 256 * 1024;
|
|
2827
|
+
function claudeLocalTurnRef(sessionId, turnId) {
|
|
2828
|
+
return `claude-turn:${cealRequestSha256({ session_id: sessionId, prompt_id: turnId })}`;
|
|
2829
|
+
}
|
|
2830
|
+
function claudeLocalInputRef(sessionId, turnId) {
|
|
2831
|
+
return `claude-input:${cealRequestSha256({ session_id: sessionId, prompt_id: turnId })}`;
|
|
2832
|
+
}
|
|
2833
|
+
function claudeWorkspaceRef(cwd) {
|
|
2834
|
+
return `workspace:${digest(cwd)}`;
|
|
2835
|
+
}
|
|
2836
|
+
function digest(value) {
|
|
2837
|
+
return createHash4("sha256").update(value, "utf8").digest("hex");
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
// host/claude/src/claude-outbox.ts
|
|
2841
|
+
import { mkdir as mkdir3, readdir, readFile as readFile2, unlink as unlink2 } from "node:fs/promises";
|
|
2842
|
+
import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
|
|
2843
|
+
|
|
2844
|
+
// host/invocation-context.ts
|
|
2845
|
+
var HOST_INVOCATION_IDENTITY_KEYS = ["host_session_ref", "host_turn_ref", "host_source_ref"];
|
|
2846
|
+
var HOST_INVOCATION_CONTEXT_REQUIRED_KEYS = ["schema_version", "identity", "prompt_ref", "expires_at"];
|
|
2847
|
+
function invocationContextKey(identity) {
|
|
2848
|
+
return cealRequestSha256({ host_source_ref: identity.host_source_ref, host_session_ref: identity.host_session_ref });
|
|
2849
|
+
}
|
|
2850
|
+
function hasHostInvocationIdentityKeys(value) {
|
|
2851
|
+
return exactKeys3(value, HOST_INVOCATION_IDENTITY_KEYS);
|
|
2852
|
+
}
|
|
2853
|
+
function hasHostInvocationContextKeys(value, optional = []) {
|
|
2854
|
+
return requiredKeys(value, HOST_INVOCATION_CONTEXT_REQUIRED_KEYS, optional);
|
|
2855
|
+
}
|
|
2856
|
+
async function readLatestSessionProjection(file, hostSourceRef, hostSessionRef, readProjection) {
|
|
2857
|
+
const context = await readProjection(file);
|
|
2858
|
+
if (context === null || context.identity.host_source_ref !== hostSourceRef || context.identity.host_session_ref !== hostSessionRef) return null;
|
|
2859
|
+
return context;
|
|
2860
|
+
}
|
|
2861
|
+
function isLiveSessionProjection(context, now) {
|
|
2862
|
+
return context !== null && Date.parse(context.expires_at) > now.getTime();
|
|
2863
|
+
}
|
|
2864
|
+
function invocationContextFromTurn(identity, promptRef, expiresAt, descriptor) {
|
|
2865
|
+
const context = {
|
|
2866
|
+
schema_version: descriptor.schema_version,
|
|
2867
|
+
identity,
|
|
2868
|
+
prompt_ref: promptRef,
|
|
2869
|
+
expires_at: expiresAt,
|
|
2870
|
+
...descriptor.turn_generation_ref === void 0 ? {} : { turn_generation_ref: descriptor.turn_generation_ref },
|
|
2871
|
+
...descriptor.working_directory === void 0 ? {} : { working_directory: descriptor.working_directory }
|
|
2872
|
+
};
|
|
2873
|
+
descriptor.validate(context);
|
|
2874
|
+
return Object.freeze(context);
|
|
2875
|
+
}
|
|
2876
|
+
function isHostInvocationTurnSelection(value) {
|
|
2877
|
+
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) => safeRef2(value[key]));
|
|
2878
|
+
}
|
|
2879
|
+
function invocationTurnSelectionMatches(context, selection) {
|
|
2880
|
+
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;
|
|
2881
|
+
}
|
|
2882
|
+
function exactKeys3(value, keys) {
|
|
2883
|
+
const actual = Object.keys(value);
|
|
2884
|
+
return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key)) && actual.every((key) => keys.includes(key));
|
|
2885
|
+
}
|
|
2886
|
+
function requiredKeys(value, required, optional) {
|
|
2887
|
+
const actual = Object.keys(value);
|
|
2888
|
+
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
2889
|
+
return actual.length >= required.length && actual.length <= allowed.size && required.every((key) => Object.hasOwn(value, key)) && actual.every((key) => allowed.has(key));
|
|
2890
|
+
}
|
|
2891
|
+
function isRecord2(value) {
|
|
2892
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2893
|
+
}
|
|
2894
|
+
function safeRef2(value) {
|
|
2895
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value);
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
// host/claude/src/claude-invocation-context.ts
|
|
2899
|
+
function claudeInvocationContextFromTurn(identity, promptRef, expiresAt, workingDirectory, turnGenerationRef) {
|
|
2900
|
+
return invocationContextFromTurn(identity, promptRef, expiresAt, { schema_version: CLAUDE_INVOCATION_CONTEXT_SCHEMA_VERSION, validate: validateClaudeHostInvocationContext, working_directory: workingDirectory, turn_generation_ref: turnGenerationRef });
|
|
2901
|
+
}
|
|
2902
|
+
function validateClaudeHostInvocationContext(value) {
|
|
2903
|
+
if (!contextShape(value)) throw new ClaudeHostError("storage", "Claude Host invocation context is malformed.");
|
|
2904
|
+
validateIdentity2(value.identity);
|
|
2905
|
+
validateContextReferences(value);
|
|
2906
|
+
validateContextTimes(value);
|
|
2907
|
+
}
|
|
2908
|
+
function contextShape(value) {
|
|
2909
|
+
return record3(value) && value.schema_version === CLAUDE_INVOCATION_CONTEXT_SCHEMA_VERSION && hasHostInvocationContextKeys(value, ["request_ref", "bound_at", "working_directory", "turn_generation_ref"]);
|
|
2910
|
+
}
|
|
2911
|
+
function validateIdentity2(value) {
|
|
2912
|
+
if (!validIdentity(value)) throw new ClaudeHostError("storage", "Claude Host invocation identity is malformed.");
|
|
2913
|
+
}
|
|
2914
|
+
function validateContextReferences(value) {
|
|
2915
|
+
if (!safeRef3(value.prompt_ref) || !optionalSafeRef(value.request_ref) || !optionalSafeRef(value.turn_generation_ref)) throw new ClaudeHostError("storage", "Claude Host invocation context is malformed.");
|
|
2916
|
+
}
|
|
2917
|
+
function validateContextTimes(value) {
|
|
2918
|
+
if (!timestamp2(value.expires_at) || !optionalTimestamp(value.bound_at) || !optionalDirectory(value.working_directory)) throw new ClaudeHostError("storage", "Claude Host invocation context is malformed.");
|
|
2919
|
+
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)}`)}).`);
|
|
2920
|
+
}
|
|
2921
|
+
function validIdentity(value) {
|
|
2922
|
+
return record3(value) && hasHostInvocationIdentityKeys(value) && [value.host_session_ref, value.host_turn_ref, value.host_source_ref].every(safeRef3);
|
|
2923
|
+
}
|
|
2924
|
+
function optionalSafeRef(value) {
|
|
2925
|
+
return value === void 0 || safeRef3(value);
|
|
2926
|
+
}
|
|
2927
|
+
function optionalTimestamp(value) {
|
|
2928
|
+
return value === void 0 || timestamp2(value);
|
|
2929
|
+
}
|
|
2930
|
+
function optionalDirectory(value) {
|
|
2931
|
+
return value === void 0 || typeof value === "string" && value.startsWith("/") && !value.includes("\0") && value.length <= 4096;
|
|
2932
|
+
}
|
|
2933
|
+
function safeRef3(value) {
|
|
2934
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value);
|
|
2935
|
+
}
|
|
2936
|
+
function timestamp2(value) {
|
|
2937
|
+
return typeof value === "string" && value.length <= 64 && Number.isFinite(Date.parse(value));
|
|
2938
|
+
}
|
|
2939
|
+
function record3(value) {
|
|
2940
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
// host/claude/src/claude-outbox.ts
|
|
2944
|
+
var SAFE_REF4 = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
2945
|
+
var CLAUDE_TRANSCRIPT_PATH_MAX_CHARACTERS = 4096;
|
|
2946
|
+
var CLAUDE_TRANSCRIPT_SUBAGENT_NAME_MAX_CHARACTERS = 256;
|
|
2947
|
+
var ClaudeOutboxStore = class {
|
|
2948
|
+
envelopes;
|
|
2949
|
+
sessions;
|
|
2950
|
+
registrations;
|
|
2951
|
+
cursors;
|
|
2952
|
+
directory;
|
|
2953
|
+
activeContexts;
|
|
2954
|
+
turnSelections;
|
|
2955
|
+
constructor(directory) {
|
|
2956
|
+
if (!directory.startsWith("/")) throw new ClaudeHostError("configuration", "Claude outbox directory must be absolute.");
|
|
2957
|
+
this.directory = directory;
|
|
2958
|
+
this.envelopes = join3(directory, "envelopes");
|
|
2959
|
+
this.sessions = join3(directory, "sessions");
|
|
2960
|
+
this.registrations = join3(directory, "transcript-registrations");
|
|
2961
|
+
this.cursors = join3(directory, "transcript-cursors");
|
|
2962
|
+
this.activeContexts = join3(directory, "active-contexts");
|
|
2963
|
+
this.turnSelections = join3(directory, "turn-selections");
|
|
2964
|
+
}
|
|
2965
|
+
async prepare() {
|
|
2966
|
+
for (const path of [this.envelopes, this.sessions, this.registrations, this.cursors, this.activeContexts, this.turnSelections]) {
|
|
2967
|
+
await mkdir3(path, { recursive: true, mode: 448 });
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
async activateInvocationContext(identity, promptRef, expiresAt, workingDirectory, turnGenerationRef) {
|
|
2971
|
+
const context = claudeInvocationContextFromTurn(identity, promptRef, expiresAt, workingDirectory, turnGenerationRef);
|
|
2972
|
+
await this.prepare();
|
|
2973
|
+
await durableAtomicReplace(this.activeContextPath(identity), JSON.stringify(context));
|
|
2974
|
+
}
|
|
2975
|
+
async readInvocationContext(hostSourceRef, hostSessionRef, now = /* @__PURE__ */ new Date()) {
|
|
2976
|
+
validateInvocationContextReadArguments(hostSourceRef, hostSessionRef, now);
|
|
2977
|
+
const context = await this.readRawInvocationContext(hostSourceRef, hostSessionRef);
|
|
2978
|
+
return isLiveSessionProjection(context, now) ? context : null;
|
|
2979
|
+
}
|
|
2980
|
+
async writeInvocationTurnSelection(selection) {
|
|
2981
|
+
if (!isHostInvocationTurnSelection(selection)) throw new ClaudeHostError("invalid_input", "Claude Host turn selection is invalid.");
|
|
2982
|
+
await this.prepare();
|
|
2983
|
+
await durableAtomicReplace(this.turnSelectionPath(selection.host_source_ref, selection.host_session_ref), JSON.stringify(selection));
|
|
2984
|
+
}
|
|
2985
|
+
async attachInvocationTurnGeneration(identity, promptRef, turnGenerationRef) {
|
|
2986
|
+
const selection = await this.readInvocationTurnSelection(identity.host_source_ref, identity.host_session_ref);
|
|
2987
|
+
if (selection === null || selection.prompt_ref !== promptRef || selection.turn_generation_ref !== turnGenerationRef) return false;
|
|
2988
|
+
const path = this.activeContextPath(identity);
|
|
2989
|
+
const active = await readProjectionFile(path);
|
|
2990
|
+
if (active === null || active.prompt_ref !== promptRef || active.identity.host_turn_ref !== identity.host_turn_ref) return false;
|
|
2991
|
+
const updated = { ...active, turn_generation_ref: turnGenerationRef };
|
|
2992
|
+
validateClaudeHostInvocationContext(updated);
|
|
2993
|
+
await durableAtomicReplace(path, JSON.stringify(updated));
|
|
2994
|
+
return true;
|
|
2995
|
+
}
|
|
2996
|
+
/**
|
|
2997
|
+
* The latest Claude session projection even when past expiry, so the
|
|
2998
|
+
* invocation authenticator can report an expired context distinctly from
|
|
2999
|
+
* a missing one. Every other caller keeps the fail-closed read above.
|
|
3000
|
+
*/
|
|
3001
|
+
async readRawInvocationContext(hostSourceRef, hostSessionRef) {
|
|
3002
|
+
if (!SAFE_REF4.test(hostSourceRef) || !SAFE_REF4.test(hostSessionRef)) throw new ClaudeHostError("invalid_input", "Claude Host context identity or clock is invalid.");
|
|
3003
|
+
await this.prepare();
|
|
3004
|
+
const context = await readLatestSessionProjection(this.activeContextPath({ host_source_ref: hostSourceRef, host_session_ref: hostSessionRef }), hostSourceRef, hostSessionRef, readProjectionFile);
|
|
3005
|
+
return invocationTurnSelectionMatches(context, await this.readInvocationTurnSelection(hostSourceRef, hostSessionRef)) ? context : null;
|
|
3006
|
+
}
|
|
3007
|
+
async listInvocationContexts(now = /* @__PURE__ */ new Date()) {
|
|
3008
|
+
await this.prepare();
|
|
3009
|
+
const contexts = [];
|
|
3010
|
+
for (const name of await readdir(this.activeContexts)) {
|
|
3011
|
+
if (!name.endsWith(".json")) continue;
|
|
3012
|
+
try {
|
|
3013
|
+
const value = JSON.parse(await readFile2(join3(this.activeContexts, name), "utf8"));
|
|
3014
|
+
validateClaudeHostInvocationContext(value);
|
|
3015
|
+
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);
|
|
3016
|
+
} catch {
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
return contexts;
|
|
3020
|
+
}
|
|
3021
|
+
async putEnvelope(entry) {
|
|
3022
|
+
validateEnvelope(entry);
|
|
3023
|
+
await this.prepare();
|
|
3024
|
+
await durableAtomicReplace(this.envelopePath(entry.submission_ref), JSON.stringify(entry));
|
|
3025
|
+
}
|
|
3026
|
+
async listEnvelopes() {
|
|
3027
|
+
await this.prepare();
|
|
3028
|
+
const entries = [];
|
|
3029
|
+
let malformed = 0;
|
|
3030
|
+
for (const name of await readdir(this.envelopes)) {
|
|
3031
|
+
if (!name.endsWith(".json")) continue;
|
|
3032
|
+
try {
|
|
3033
|
+
const value = JSON.parse(await readFile2(join3(this.envelopes, name), "utf8"));
|
|
3034
|
+
validateEnvelope(value);
|
|
3035
|
+
entries.push(value);
|
|
3036
|
+
} catch {
|
|
3037
|
+
malformed += 1;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
entries.sort((left, right) => left.created_at.localeCompare(right.created_at));
|
|
3041
|
+
return { entries, malformed };
|
|
3042
|
+
}
|
|
3043
|
+
async removeEnvelope(entry) {
|
|
3044
|
+
validateEnvelope(entry);
|
|
3045
|
+
try {
|
|
3046
|
+
await unlink2(this.envelopePath(entry.submission_ref));
|
|
3047
|
+
} catch (error) {
|
|
3048
|
+
if (code(error) !== "ENOENT") throw error;
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
async saveSession(snapshot) {
|
|
3052
|
+
validateSession(snapshot);
|
|
3053
|
+
await this.prepare();
|
|
3054
|
+
await durableAtomicReplace(join3(this.sessions, `${cealRequestSha256({ source: snapshot.host_source_ref, session: snapshot.host_session_ref })}.json`), JSON.stringify(snapshot));
|
|
3055
|
+
}
|
|
3056
|
+
async readSession(sourceRef, sessionRef) {
|
|
3057
|
+
if (!SAFE_REF4.test(sourceRef) || !SAFE_REF4.test(sessionRef)) throw new ClaudeHostError("invalid_input", "Claude session identity is invalid.");
|
|
3058
|
+
await this.prepare();
|
|
3059
|
+
try {
|
|
3060
|
+
const value = JSON.parse(await readFile2(join3(this.sessions, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`), "utf8"));
|
|
3061
|
+
validateSession(value);
|
|
3062
|
+
return value;
|
|
3063
|
+
} catch (error) {
|
|
3064
|
+
if (code(error) === "ENOENT") return null;
|
|
3065
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Claude session snapshot is malformed.");
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
async registerTranscript(registration) {
|
|
3069
|
+
validateRegistration(registration);
|
|
3070
|
+
await this.prepare();
|
|
3071
|
+
const path = this.registrationPath(registration.host_source_ref, registration.host_session_ref);
|
|
3072
|
+
const existing = await this.readRegistrationFile(path);
|
|
3073
|
+
if (existing !== null && (existing.claude_session_id !== registration.claude_session_id || existing.transcript_path !== registration.transcript_path)) {
|
|
3074
|
+
throw new ClaudeHostError("storage", "Claude transcript registration conflicts with the source-bound session.");
|
|
3075
|
+
}
|
|
3076
|
+
await durableAtomicReplace(path, JSON.stringify(registration));
|
|
3077
|
+
}
|
|
3078
|
+
/**
|
|
3079
|
+
* End a registration whose transcript the Host has deleted. The cursor is
|
|
3080
|
+
* keyed by the same session and describes byte offsets of a file that no
|
|
3081
|
+
* longer exists, so it leaves with the registration.
|
|
3082
|
+
*/
|
|
3083
|
+
async retireTranscriptRegistration(registration) {
|
|
3084
|
+
validateRegistration(registration);
|
|
3085
|
+
await this.prepare();
|
|
3086
|
+
await this.removeStateFile(this.registrationPath(registration.host_source_ref, registration.host_session_ref));
|
|
3087
|
+
await this.removeStateFile(this.cursorPath(registration.host_source_ref, registration.host_session_ref));
|
|
3088
|
+
}
|
|
3089
|
+
async listTranscriptRegistrations() {
|
|
3090
|
+
await this.prepare();
|
|
3091
|
+
const registrations = [];
|
|
3092
|
+
for (const name of await readdir(this.registrations)) {
|
|
3093
|
+
if (!name.endsWith(".json")) continue;
|
|
3094
|
+
try {
|
|
3095
|
+
const value = JSON.parse(await readFile2(join3(this.registrations, name), "utf8"));
|
|
3096
|
+
validateRegistration(value);
|
|
3097
|
+
registrations.push(value);
|
|
3098
|
+
} catch {
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
return registrations.sort((left, right) => left.registered_at.localeCompare(right.registered_at));
|
|
3102
|
+
}
|
|
3103
|
+
async readInvocationTurnSelection(hostSourceRef, hostSessionRef) {
|
|
3104
|
+
if (!SAFE_REF4.test(hostSourceRef) || !SAFE_REF4.test(hostSessionRef)) throw new ClaudeHostError("invalid_input", "Claude Host turn selection identity is invalid.");
|
|
3105
|
+
await this.prepare();
|
|
3106
|
+
try {
|
|
3107
|
+
const value = JSON.parse(await readFile2(this.turnSelectionPath(hostSourceRef, hostSessionRef), "utf8"));
|
|
3108
|
+
if (!isHostInvocationTurnSelection(value) || value.host_source_ref !== hostSourceRef || value.host_session_ref !== hostSessionRef) throw new ClaudeHostError("storage", "Claude Host turn selection is malformed.");
|
|
3109
|
+
return value;
|
|
3110
|
+
} catch (error) {
|
|
3111
|
+
if (code(error) === "ENOENT") return null;
|
|
3112
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Unable to read the Claude Host turn selection.");
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
async readTranscriptCursor(registration) {
|
|
3116
|
+
validateRegistration(registration);
|
|
3117
|
+
await this.prepare();
|
|
3118
|
+
try {
|
|
3119
|
+
const value = JSON.parse(await readFile2(this.cursorPath(registration.host_source_ref, registration.host_session_ref), "utf8"));
|
|
3120
|
+
validateCursor(value);
|
|
3121
|
+
return value;
|
|
3122
|
+
} catch (error) {
|
|
3123
|
+
if (code(error) === "ENOENT") return null;
|
|
3124
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Claude transcript cursor is malformed.");
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
async writeTranscriptCursor(cursor) {
|
|
3128
|
+
validateCursor(cursor);
|
|
3129
|
+
await this.prepare();
|
|
3130
|
+
await durableAtomicReplace(this.cursorPath(cursor.host_source_ref, cursor.host_session_ref), JSON.stringify(cursor));
|
|
3131
|
+
}
|
|
3132
|
+
envelopePath(ref) {
|
|
3133
|
+
return join3(this.envelopes, `${cealRequestSha256(ref)}.json`);
|
|
3134
|
+
}
|
|
3135
|
+
registrationPath(sourceRef, sessionRef) {
|
|
3136
|
+
return join3(this.registrations, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`);
|
|
3137
|
+
}
|
|
3138
|
+
turnSelectionPath(sourceRef, sessionRef) {
|
|
3139
|
+
return join3(this.turnSelections, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`);
|
|
3140
|
+
}
|
|
3141
|
+
cursorPath(sourceRef, sessionRef) {
|
|
3142
|
+
return join3(this.cursors, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`);
|
|
3143
|
+
}
|
|
3144
|
+
async readRegistrationFile(path) {
|
|
3145
|
+
try {
|
|
3146
|
+
const value = JSON.parse(await readFile2(path, "utf8"));
|
|
3147
|
+
validateRegistration(value);
|
|
3148
|
+
return value;
|
|
3149
|
+
} catch (error) {
|
|
3150
|
+
if (code(error) === "ENOENT") return null;
|
|
3151
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Claude transcript registration is malformed.");
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
async removeStateFile(path) {
|
|
3155
|
+
try {
|
|
3156
|
+
await unlink2(path);
|
|
3157
|
+
} catch (error) {
|
|
3158
|
+
if (code(error) !== "ENOENT") throw error;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
activeContextPath(identity) {
|
|
3162
|
+
return join3(this.activeContexts, `${invocationContextKey(identity)}.json`);
|
|
3163
|
+
}
|
|
3164
|
+
};
|
|
3165
|
+
function validateInvocationContextReadArguments(hostSourceRef, hostSessionRef, now) {
|
|
3166
|
+
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.");
|
|
3167
|
+
}
|
|
3168
|
+
async function readProjectionFile(file) {
|
|
3169
|
+
try {
|
|
3170
|
+
const context = JSON.parse(await readFile2(file, "utf8"));
|
|
3171
|
+
validateClaudeHostInvocationContext(context);
|
|
3172
|
+
return context;
|
|
3173
|
+
} catch (error) {
|
|
3174
|
+
if (code(error) === "ENOENT") return null;
|
|
3175
|
+
if (error instanceof ClaudeHostError) throw error;
|
|
3176
|
+
throw new ClaudeHostError("storage", "Unable to read the Claude Host invocation context.");
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
function safeRef4(value) {
|
|
3180
|
+
return typeof value === "string" && SAFE_REF4.test(value);
|
|
3181
|
+
}
|
|
3182
|
+
function timestamp3(value) {
|
|
3183
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
3184
|
+
}
|
|
3185
|
+
function record4(value) {
|
|
3186
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3187
|
+
}
|
|
3188
|
+
function code(error) {
|
|
3189
|
+
return record4(error) && typeof error.code === "string" ? error.code : null;
|
|
3190
|
+
}
|
|
3191
|
+
function validateEnvelope(value) {
|
|
3192
|
+
if (!record4(value)) throw new ClaudeHostError("storage", "Claude outbox entry is malformed.");
|
|
3193
|
+
if (!sameKeys(value, ["schema_version", "identity", "host_input_ref", "input_kind", "prompt_ref", "observations", "origin_input", "submission_ref", "created_at", "expires_at"], ["request_ref"])) {
|
|
3194
|
+
throw new ClaudeHostError("storage", `Claude outbox entry key set is malformed (${refusalFact("keys", Object.keys(value), "exactly the outbox entry key set")}).`);
|
|
3195
|
+
}
|
|
3196
|
+
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}`)}).`);
|
|
3197
|
+
validateEnvelopeIdentity(value);
|
|
3198
|
+
validateEnvelopeObservations(value.observations);
|
|
3199
|
+
if (typeof value.origin_input !== "boolean") throw new ClaudeHostError("storage", "Claude outbox origin disposition is malformed.");
|
|
3200
|
+
if (!timestamp3(value.created_at) || !timestamp3(value.expires_at)) throw new ClaudeHostError("storage", "Claude outbox timestamps are malformed.");
|
|
3201
|
+
}
|
|
3202
|
+
function validateEnvelopeIdentity(value) {
|
|
3203
|
+
validateIdentity3(value.identity);
|
|
3204
|
+
if (!safeRef4(value.host_input_ref)) throw new ClaudeHostError("storage", "Claude outbox input reference is malformed.");
|
|
3205
|
+
if (!isCealHostInputKind(value.input_kind)) throw new ClaudeHostError("storage", "Claude outbox input provenance is malformed.");
|
|
3206
|
+
if (!safeRef4(value.prompt_ref) || !safeRef4(value.submission_ref)) throw new ClaudeHostError("storage", "Claude outbox references are malformed.");
|
|
3207
|
+
if (value.request_ref !== void 0 && !safeRef4(value.request_ref)) throw new ClaudeHostError("storage", "Claude outbox Request reference is malformed.");
|
|
3208
|
+
}
|
|
3209
|
+
function validateEnvelopeObservations(value) {
|
|
3210
|
+
if (!Array.isArray(value)) throw new ClaudeHostError("storage", "Claude outbox observations are malformed.");
|
|
3211
|
+
try {
|
|
3212
|
+
for (const observation2 of value) validateCealHostObservationDraft(observation2);
|
|
3213
|
+
} catch {
|
|
3214
|
+
throw new ClaudeHostError("storage", "Claude outbox observations are malformed.");
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
function validateSession(value) {
|
|
3218
|
+
if (!record4(value)) throw new ClaudeHostError("storage", "Claude session snapshot is malformed.");
|
|
3219
|
+
if (!sameKeys(value, ["schema_version", "host_session_ref", "host_source_ref", "observed_at", "value", "expires_at"])) {
|
|
3220
|
+
throw new ClaudeHostError("storage", `Claude session snapshot key set is malformed (${refusalFact("keys", Object.keys(value), "exactly the session snapshot key set")}).`);
|
|
3221
|
+
}
|
|
3222
|
+
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}`)}).`);
|
|
3223
|
+
if (!safeRef4(value.host_session_ref) || !safeRef4(value.host_source_ref) || !timestamp3(value.observed_at) || !timestamp3(value.expires_at)) {
|
|
3224
|
+
throw new ClaudeHostError("storage", "Claude session snapshot fields are malformed.");
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
function validateRegistration(value) {
|
|
3228
|
+
if (!record4(value)) throw new ClaudeHostError("storage", "Claude transcript registration is malformed.");
|
|
3229
|
+
if (!sameKeys(value, ["schema_version", "host_source_ref", "host_session_ref", "claude_session_id", "transcript_path", "registered_at", "expires_at"], ["cwd"])) {
|
|
3230
|
+
throw new ClaudeHostError("storage", `Claude transcript registration key set is malformed (${refusalFact("keys", Object.keys(value), "exactly the registration key set")}).`);
|
|
3231
|
+
}
|
|
3232
|
+
if (value.schema_version !== CLAUDE_TRANSCRIPT_REGISTRATION_SCHEMA_VERSION) throw new ClaudeHostError("storage", "Claude transcript registration is malformed.");
|
|
3233
|
+
if (![value.host_source_ref, value.host_session_ref, value.claude_session_id].every((candidate) => typeof candidate === "string" && SAFE_REF4.test(candidate))) {
|
|
3234
|
+
throw new ClaudeHostError("storage", "Claude transcript registration references are malformed.");
|
|
3235
|
+
}
|
|
3236
|
+
validateRegistrationPaths(value.cwd, value.transcript_path);
|
|
3237
|
+
if (!timestamp3(value.registered_at) || !timestamp3(value.expires_at)) throw new ClaudeHostError("storage", "Claude transcript registration timestamps are malformed.");
|
|
3238
|
+
}
|
|
3239
|
+
function validateRegistrationPaths(cwd, transcriptPath) {
|
|
3240
|
+
if (cwd !== void 0 && (typeof cwd !== "string" || !isAbsolute2(cwd) || cwd.length > CLAUDE_TRANSCRIPT_PATH_MAX_CHARACTERS || cwd.includes("\0"))) {
|
|
3241
|
+
throw new ClaudeHostError("storage", "Claude transcript registration working directory is malformed.");
|
|
3242
|
+
}
|
|
3243
|
+
if (typeof transcriptPath !== "string" || transcriptPath.length === 0 || transcriptPath.length > CLAUDE_TRANSCRIPT_PATH_MAX_CHARACTERS) {
|
|
3244
|
+
throw new ClaudeHostError("storage", "Claude transcript registration path is malformed.");
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
function validateCursor(value) {
|
|
3248
|
+
if (!record4(value)) throw new ClaudeHostError("storage", "Claude transcript cursor is malformed.");
|
|
3249
|
+
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"])) {
|
|
3250
|
+
throw new ClaudeHostError("storage", "Claude transcript cursor key set is malformed.");
|
|
3251
|
+
}
|
|
3252
|
+
if (value.schema_version !== CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION) throw new ClaudeHostError("storage", "Claude transcript cursor is malformed.");
|
|
3253
|
+
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.");
|
|
3254
|
+
if (typeof value.transcript_path !== "string" || value.transcript_path.length === 0) throw new ClaudeHostError("storage", "Claude transcript cursor path is malformed.");
|
|
3255
|
+
validateCursorPosition(value);
|
|
3256
|
+
validateCursorTurn(value.open_turn, value.unattributed, value.session_observed);
|
|
3257
|
+
validateSubagentCursors(value.subagents);
|
|
3258
|
+
if (value.reason !== void 0 && !isHostTranscriptCursorReason(value.reason)) throw new ClaudeHostError("storage", "Claude transcript cursor reason is malformed.");
|
|
3259
|
+
}
|
|
3260
|
+
function validateCursorPosition(value) {
|
|
3261
|
+
if (!validCursorPosition(value)) throw new ClaudeHostError("storage", "Claude transcript cursor position is malformed.");
|
|
3262
|
+
if (value.device !== void 0 && typeof value.device !== "string") throw new ClaudeHostError("storage", "Claude transcript cursor device is malformed.");
|
|
3263
|
+
if (value.inode !== void 0 && typeof value.inode !== "string") throw new ClaudeHostError("storage", "Claude transcript cursor inode is malformed.");
|
|
3264
|
+
if (value.device === void 0 && value.offset !== 0) throw new ClaudeHostError("storage", "Claude transcript cursor is unpinned past its start.");
|
|
3265
|
+
if (value.status !== "active" && value.status !== "quarantined" || !timestamp3(value.updated_at)) throw new ClaudeHostError("storage", "Claude transcript cursor status is malformed.");
|
|
3266
|
+
}
|
|
3267
|
+
function validateCursorTurn(openTurn, unattributed, sessionObserved) {
|
|
3268
|
+
if (openTurn !== void 0 && !validOpenTurn(openTurn)) throw new ClaudeHostError("storage", "Claude transcript turn cursor is malformed.");
|
|
3269
|
+
if (unattributed !== void 0 && !validUnattributed(unattributed)) throw new ClaudeHostError("storage", "Claude transcript unattributed cursor is malformed.");
|
|
3270
|
+
if (sessionObserved !== void 0 && typeof sessionObserved !== "boolean") throw new ClaudeHostError("storage", "Claude transcript cursor session marker is malformed.");
|
|
3271
|
+
}
|
|
3272
|
+
function validateSubagentCursors(value) {
|
|
3273
|
+
if (!record4(value)) throw new ClaudeHostError("storage", "Claude transcript subagent cursors are malformed.");
|
|
3274
|
+
for (const [name, entry] of Object.entries(value)) validateSubagentEntry(name, entry);
|
|
3275
|
+
}
|
|
3276
|
+
function validateSubagentEntry(name, entry) {
|
|
3277
|
+
if (name.length === 0 || name.length > CLAUDE_TRANSCRIPT_SUBAGENT_NAME_MAX_CHARACTERS || name.includes("/")) throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3278
|
+
if (name.includes("\0")) throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3279
|
+
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.");
|
|
3280
|
+
validateSubagentIdentity(entry);
|
|
3281
|
+
}
|
|
3282
|
+
function validateSubagentIdentity(entry) {
|
|
3283
|
+
if (entry.device !== void 0 && typeof entry.device !== "string" || entry.inode !== void 0 && typeof entry.inode !== "string") {
|
|
3284
|
+
throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3285
|
+
}
|
|
3286
|
+
if (!nonNegativeInteger(entry.anchor_start) || typeof entry.anchor_sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(entry.anchor_sha256)) {
|
|
3287
|
+
throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3288
|
+
}
|
|
3289
|
+
if (entry.device === void 0 && Number(entry.offset) !== 0) throw new ClaudeHostError("storage", "Claude transcript subagent cursor is unpinned past its start.");
|
|
3290
|
+
}
|
|
3291
|
+
function validCursorPosition(value) {
|
|
3292
|
+
return nonNegativeInteger(value.offset) && nonNegativeInteger(value.anchor_start) && typeof value.anchor_sha256 === "string" && /^[a-f0-9]{64}$/u.test(value.anchor_sha256);
|
|
3293
|
+
}
|
|
3294
|
+
function validOpenTurn(value) {
|
|
3295
|
+
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;
|
|
3296
|
+
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;
|
|
3297
|
+
if (!timestamp3(value.started_at) || !timestamp3(value.last_observed_at)) return false;
|
|
3298
|
+
if (!validMessageIds(value.message_ids)) return false;
|
|
3299
|
+
if (!validTurnCounts(value.counts)) return false;
|
|
3300
|
+
return value.last_terminal_digest === void 0 || typeof value.last_terminal_digest === "string" && /^[a-f0-9]{64}$/u.test(value.last_terminal_digest);
|
|
3301
|
+
}
|
|
3302
|
+
function validMessageIds(value) {
|
|
3303
|
+
if (!Array.isArray(value) || value.length > CLAUDE_TRANSCRIPT_MAX_TURN_MESSAGES) return false;
|
|
3304
|
+
if (!value.every((candidate) => typeof candidate === "string" && candidate.length > 0 && candidate.length <= 256)) return false;
|
|
3305
|
+
return new Set(value).size === value.length;
|
|
3306
|
+
}
|
|
3307
|
+
function validUnattributed(value) {
|
|
3308
|
+
if (!record4(value) || !sameKeys(value, ["input", "cached_input", "cache_write_input", "output", "message_ids", "last_observed_at"])) return false;
|
|
3309
|
+
if (!validMessageIds(value.message_ids) || value.message_ids.length === 0) return false;
|
|
3310
|
+
if (!timestamp3(value.last_observed_at)) return false;
|
|
3311
|
+
return validTurnCounts(value);
|
|
3312
|
+
}
|
|
3313
|
+
function validTurnCounts(value) {
|
|
3314
|
+
if (!record4(value) || !sameKeys(value, ["input", "cached_input", "cache_write_input", "output"])) return false;
|
|
3315
|
+
return [value.input, value.cached_input, value.cache_write_input, value.output].every(nonNegativeInteger);
|
|
3316
|
+
}
|
|
3317
|
+
function validateIdentity3(value) {
|
|
3318
|
+
if (!record4(value) || ![value.host_session_ref, value.host_turn_ref, value.host_source_ref].every((candidate) => typeof candidate === "string" && SAFE_REF4.test(candidate))) {
|
|
3319
|
+
throw new ClaudeHostError("storage", "Claude Host identity is malformed.");
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
function nonNegativeInteger(value) {
|
|
3323
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
3324
|
+
}
|
|
3325
|
+
function sameKeys(value, required, optional = []) {
|
|
3326
|
+
return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
// host/claude/src/claude-producer.ts
|
|
3330
|
+
var ClaudeProducer = class {
|
|
3331
|
+
producer_id = "claude";
|
|
3332
|
+
producer_label = "Claude";
|
|
3333
|
+
outbox_directory;
|
|
3334
|
+
store;
|
|
3335
|
+
core;
|
|
3336
|
+
constructor(options) {
|
|
3337
|
+
this.store = new ClaudeOutboxStore(options.outbox_dir);
|
|
3338
|
+
this.outbox_directory = this.store.directory;
|
|
3339
|
+
this.core = new HostProducerDrainCore(this, options);
|
|
3340
|
+
}
|
|
3341
|
+
drainOutbox() {
|
|
3342
|
+
return this.core.drainOutbox();
|
|
3343
|
+
}
|
|
3344
|
+
fail(code2, message) {
|
|
3345
|
+
return new ClaudeHostError(code2, message);
|
|
3346
|
+
}
|
|
3347
|
+
listEnvelopes() {
|
|
3348
|
+
return this.store.listEnvelopes();
|
|
3349
|
+
}
|
|
3350
|
+
removeEnvelope(envelope) {
|
|
3351
|
+
return this.store.removeEnvelope(envelope);
|
|
3352
|
+
}
|
|
3353
|
+
submissionRefOf(envelope) {
|
|
3354
|
+
return envelope.submission_ref;
|
|
3355
|
+
}
|
|
3356
|
+
expiresAtOf(envelope) {
|
|
3357
|
+
return envelope.expires_at;
|
|
3358
|
+
}
|
|
3359
|
+
createdAtOf(envelope) {
|
|
3360
|
+
return envelope.created_at;
|
|
3361
|
+
}
|
|
3362
|
+
async mapEnvelope(envelope) {
|
|
3363
|
+
const prompt = observation(envelope, "prompt");
|
|
3364
|
+
if (prompt !== void 0) return this.mapInput(envelope, prompt);
|
|
3365
|
+
return [mapTerminal(envelope)];
|
|
3366
|
+
}
|
|
3367
|
+
async mapInput(envelope, prompt) {
|
|
3368
|
+
const drafts = [];
|
|
3369
|
+
const session = observation(envelope, "session");
|
|
3370
|
+
if (session !== void 0) {
|
|
3371
|
+
const registrations = await this.store.listTranscriptRegistrations();
|
|
3372
|
+
const registration = registrations.find((item) => item.host_source_ref === envelope.identity.host_source_ref && item.host_session_ref === envelope.identity.host_session_ref);
|
|
3373
|
+
const directoryIdentity = registration?.cwd ?? envelope.identity.host_session_ref;
|
|
3374
|
+
drafts.push({
|
|
3375
|
+
frame_kind: "session_started",
|
|
3376
|
+
payload: compact({
|
|
3377
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3378
|
+
working_directory_ref: claudeWorkspaceRef(directoryIdentity),
|
|
3379
|
+
model: recordText(session.value, "model"),
|
|
3380
|
+
observations: [session]
|
|
3381
|
+
})
|
|
3382
|
+
});
|
|
3383
|
+
}
|
|
3384
|
+
const turn = observation(envelope, "turn");
|
|
3385
|
+
if (turn !== void 0) drafts.push({
|
|
3386
|
+
frame_kind: "turn_started",
|
|
3387
|
+
payload: compact({
|
|
3388
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3389
|
+
local_turn_ref: localTurnRef(envelope),
|
|
3390
|
+
observations: [turn]
|
|
3391
|
+
})
|
|
3392
|
+
});
|
|
3393
|
+
if (!isCealHostPromptObservationValue(prompt.value)) throw new ClaudeHostError("storage", "Claude prompt observation is malformed.");
|
|
3394
|
+
drafts.push({
|
|
3395
|
+
frame_kind: "user_input",
|
|
3396
|
+
payload: compact({
|
|
3397
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3398
|
+
local_turn_ref: localTurnRef(envelope),
|
|
3399
|
+
local_input_ref: envelope.host_input_ref,
|
|
3400
|
+
input_kind: envelope.input_kind,
|
|
3401
|
+
origin_input: envelope.origin_input,
|
|
3402
|
+
observations: [prompt],
|
|
3403
|
+
context_expires_at: envelope.expires_at
|
|
3404
|
+
})
|
|
3405
|
+
});
|
|
3406
|
+
return drafts;
|
|
3407
|
+
}
|
|
3408
|
+
};
|
|
3409
|
+
function mapTerminal(envelope) {
|
|
3410
|
+
const usage = envelope.observations.filter((item) => ["token", "cost"].includes(item.observation_kind));
|
|
3411
|
+
return {
|
|
3412
|
+
frame_kind: "turn_terminal",
|
|
3413
|
+
payload: compact({
|
|
3414
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3415
|
+
local_turn_ref: localTurnRef(envelope),
|
|
3416
|
+
terminal_state: "completed",
|
|
3417
|
+
usage: usage.length === 0 ? void 0 : Object.fromEntries(usage.map((item) => [item.observation_kind, item.value])),
|
|
3418
|
+
observations: envelope.observations
|
|
3419
|
+
})
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
function observation(envelope, kind) {
|
|
3423
|
+
return envelope.observations.find((item) => item.observation_kind === kind);
|
|
3424
|
+
}
|
|
3425
|
+
function localTurnRef(envelope) {
|
|
3426
|
+
return envelope.identity.host_turn_ref;
|
|
3427
|
+
}
|
|
3428
|
+
function compact(value) {
|
|
3429
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== void 0));
|
|
3430
|
+
}
|
|
3431
|
+
function asRecord(value) {
|
|
3432
|
+
return isRecord3(value) ? value : void 0;
|
|
3433
|
+
}
|
|
3434
|
+
function recordText(value, key) {
|
|
3435
|
+
const record5 = asRecord(value);
|
|
3436
|
+
return typeof record5?.[key] === "string" ? record5[key] : void 0;
|
|
3437
|
+
}
|
|
3438
|
+
function isRecord3(value) {
|
|
3439
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
// host/claude/src/claude-transcript-watcher.ts
|
|
3443
|
+
import { O_NOFOLLOW as O_NOFOLLOW2, O_RDONLY as O_RDONLY3 } from "node:constants";
|
|
3444
|
+
import { open as open5, readdir as readdir2 } from "node:fs/promises";
|
|
3445
|
+
import { join as join4 } from "node:path";
|
|
3446
|
+
var TOKEN_OBSERVATION_SOURCE = "claude_transcript.message.usage";
|
|
3447
|
+
var ClaudeTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
3448
|
+
store;
|
|
3449
|
+
registerCurrentPrompt;
|
|
3450
|
+
diagnostic;
|
|
3451
|
+
constructor(options) {
|
|
3452
|
+
super(options, {
|
|
3453
|
+
default_retention_ms: CLAUDE_DEFAULT_RETENTION_MS,
|
|
3454
|
+
invalid_outbox: () => new ClaudeHostError("configuration", "Claude transcript watcher outbox is invalid."),
|
|
3455
|
+
clock_failure: () => new ClaudeHostError("storage", "Claude watcher clock is invalid.")
|
|
3456
|
+
});
|
|
3457
|
+
this.store = new ClaudeOutboxStore(options.outbox_dir);
|
|
3458
|
+
this.registerCurrentPrompt = options.register_current_prompt;
|
|
3459
|
+
this.diagnostic = options.diagnostic ?? (() => void 0);
|
|
3460
|
+
}
|
|
3461
|
+
transcriptPathOf(registration) {
|
|
3462
|
+
return registration.transcript_path;
|
|
3463
|
+
}
|
|
3464
|
+
async listRegistrations() {
|
|
3465
|
+
return this.store.listTranscriptRegistrations();
|
|
3466
|
+
}
|
|
3467
|
+
async scanOpened(handle, registration) {
|
|
3468
|
+
const { size, device, inode } = await ownedClaudeTranscriptFile(handle);
|
|
3469
|
+
const cursor = await this.preflight(registration, handle, size, device, inode);
|
|
3470
|
+
if (cursor === null) return { records: 0, enqueued: 0, quarantined: true };
|
|
3471
|
+
if (cursor.status === "quarantined") return { records: 0, enqueued: 0, quarantined: true };
|
|
3472
|
+
const draft = mutableCursor(cursor);
|
|
3473
|
+
await this.scanOpenedTranscript(registration, handle, draft, size, device, inode);
|
|
3474
|
+
return { records: draft.consumed, enqueued: draft.enqueued, quarantined: false };
|
|
3475
|
+
}
|
|
3476
|
+
async scanFailed(handle, registration, error) {
|
|
3477
|
+
this.diagnostic("watcher_scan_failed", claudeTranscriptScanFailureKind(error));
|
|
3478
|
+
if (handle === void 0 && isSymlinkTranscript(error)) return await this.quarantineUnopened(registration, "transcript_malformed");
|
|
3479
|
+
if (error instanceof ClaudeHostError && error.code === "payload_rejected") return await this.quarantineDraft(await this.preservedCursor(registration));
|
|
3480
|
+
await this.quarantineFailure(registration, handle);
|
|
3481
|
+
return { records: 0, enqueued: 0, quarantined: true };
|
|
3482
|
+
}
|
|
3483
|
+
async retireMissing(registration) {
|
|
3484
|
+
return this.retireMissingTranscript(registration.transcript_path, "Claude", () => this.store.retireTranscriptRegistration(registration));
|
|
3485
|
+
}
|
|
3486
|
+
initialCursor(registration) {
|
|
3487
|
+
return {
|
|
3488
|
+
schema_version: CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION,
|
|
3489
|
+
host_source_ref: registration.host_source_ref,
|
|
3490
|
+
host_session_ref: registration.host_session_ref,
|
|
3491
|
+
transcript_path: registration.transcript_path,
|
|
3492
|
+
offset: 0,
|
|
3493
|
+
anchor_start: 0,
|
|
3494
|
+
anchor_sha256: this.sha(Buffer.alloc(0)),
|
|
3495
|
+
subagents: {},
|
|
3496
|
+
status: "active",
|
|
3497
|
+
updated_at: this.stamp()
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
async scanOpenedTranscript(registration, handle, draft, size, device, inode) {
|
|
3501
|
+
const main = await this.readChunk(handle, draft.offset, size);
|
|
3502
|
+
if (main !== "empty" && main !== "partial") {
|
|
3503
|
+
await this.consumeMainChunk(registration, main, draft);
|
|
3504
|
+
draft.offset += main.length;
|
|
3505
|
+
}
|
|
3506
|
+
draft.consumed += await this.consumeSubagents(registration, draft);
|
|
3507
|
+
await this.emitOpenTerminal(registration, draft);
|
|
3508
|
+
await this.commitCursor(registration, handle, draft, device, inode);
|
|
3509
|
+
}
|
|
3510
|
+
async preflight(registration, handle, size, device, inode) {
|
|
3511
|
+
const existing = await this.store.readTranscriptCursor(registration);
|
|
3512
|
+
if (existing?.status === "quarantined") return existing;
|
|
3513
|
+
const cursor = existing ?? this.initialCursor(registration);
|
|
3514
|
+
if (cursor.device === void 0) {
|
|
3515
|
+
if (cursor.offset !== 0) return await this.quarantineStored(cursor, device, inode, "transcript_replaced");
|
|
3516
|
+
return { ...cursor, device, inode };
|
|
3517
|
+
}
|
|
3518
|
+
const reason = this.cursorIdentityReason(cursor, registration.transcript_path, device, inode) ?? await this.cursorBoundsReason(handle, cursor, size);
|
|
3519
|
+
if (reason !== null) return await this.quarantineStored(cursor, device, inode, reason);
|
|
3520
|
+
return cursor;
|
|
3521
|
+
}
|
|
3522
|
+
async consumeMainChunk(registration, complete, draft) {
|
|
3523
|
+
for (const line of complete.toString("utf8").split("\n").filter((value) => value.length > 0)) {
|
|
3524
|
+
draft.consumed += 1;
|
|
3525
|
+
const record5 = parseTranscriptLine(line);
|
|
3526
|
+
if (record5 === null) throw new ClaudeHostError("payload_rejected", "Claude transcript line is not valid JSON.", "malformed_line");
|
|
3527
|
+
if (isUserTurnLine(record5)) await this.openTurn(registration, draft, record5);
|
|
3528
|
+
else if (isAssistantUsageLine(record5)) this.accumulateUsage(draft, toUsage(record5.uuid, record5.timestamp, record5.message.usage));
|
|
3529
|
+
else if (isAssistantLine(record5) && record5.message.usage !== void 0) throw new ClaudeHostError("payload_rejected", "Claude transcript usage is malformed.");
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
async openTurn(registration, draft, record5) {
|
|
3533
|
+
this.diagnostic("watcher_turn_observed");
|
|
3534
|
+
await this.closeTurn(registration, draft, record5.timestamp);
|
|
3535
|
+
const sessionRef = registration.host_session_ref;
|
|
3536
|
+
const turnRef = claudeLocalTurnRef(registration.claude_session_id, record5.uuid);
|
|
3537
|
+
const inputRef = claudeLocalInputRef(registration.claude_session_id, record5.uuid);
|
|
3538
|
+
const promptRef = hostProducerPromptRef(this.sha(record5.message.content));
|
|
3539
|
+
const identity = { host_source_ref: registration.host_source_ref, host_session_ref: sessionRef, host_turn_ref: turnRef };
|
|
3540
|
+
const observations = [
|
|
3541
|
+
{ 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) } },
|
|
3542
|
+
{ 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 } }
|
|
3543
|
+
];
|
|
3544
|
+
if (!draft.session_observed) {
|
|
3545
|
+
const session = await this.store.readSession(registration.host_source_ref, sessionRef);
|
|
3546
|
+
if (session !== null) observations.push({ observation_ref: observationRef("transcript_session", sessionRef, session.value), observation_kind: "session", observed_at: record5.timestamp, value: session.value });
|
|
3547
|
+
draft.session_observed = true;
|
|
3548
|
+
}
|
|
3549
|
+
await this.store.putEnvelope({
|
|
3550
|
+
schema_version: "ceal.claude_observation_outbox.v1",
|
|
3551
|
+
identity,
|
|
3552
|
+
host_input_ref: inputRef,
|
|
3553
|
+
input_kind: "human",
|
|
3554
|
+
prompt_ref: promptRef,
|
|
3555
|
+
observations,
|
|
3556
|
+
origin_input: true,
|
|
3557
|
+
submission_ref: submissionRef("input", turnRef, inputRef, promptRef),
|
|
3558
|
+
created_at: record5.timestamp,
|
|
3559
|
+
expires_at: this.expire(this.outboxRetentionMs)
|
|
3560
|
+
});
|
|
3561
|
+
draft.enqueued += 1;
|
|
3562
|
+
await this.registerPrompt(sessionRef, turnRef, promptRef, record5.message.content);
|
|
3563
|
+
const selection = await this.store.readInvocationTurnSelection(identity.host_source_ref, identity.host_session_ref);
|
|
3564
|
+
const turnGenerationRef = selection?.prompt_ref === promptRef ? selection.turn_generation_ref : void 0;
|
|
3565
|
+
await this.activateContext(identity, promptRef, turnGenerationRef);
|
|
3566
|
+
const folded = foldUnattributed(draft.unattributed);
|
|
3567
|
+
draft.open_turn = {
|
|
3568
|
+
turn_key: turnRef,
|
|
3569
|
+
user_uuid: record5.uuid,
|
|
3570
|
+
prompt_ref: promptRef,
|
|
3571
|
+
host_input_ref: inputRef,
|
|
3572
|
+
started_at: record5.timestamp,
|
|
3573
|
+
last_observed_at: folded?.last_observed_at ?? record5.timestamp,
|
|
3574
|
+
message_ids: folded?.message_ids ?? [],
|
|
3575
|
+
counts: folded?.counts ?? zeroCounts()
|
|
3576
|
+
};
|
|
3577
|
+
draft.unattributed = void 0;
|
|
3578
|
+
}
|
|
3579
|
+
async closeTurn(registration, draft, at) {
|
|
3580
|
+
const turn = draft.open_turn;
|
|
3581
|
+
draft.open_turn = void 0;
|
|
3582
|
+
if (turn === void 0) return;
|
|
3583
|
+
const digest2 = terminalDigest(turn);
|
|
3584
|
+
if (digest2 === turn.last_terminal_digest) return;
|
|
3585
|
+
await this.putTerminalEnvelope(registration, turn, digest2, turn.message_ids.length === 0 ? at : void 0);
|
|
3586
|
+
draft.enqueued += 1;
|
|
3587
|
+
}
|
|
3588
|
+
accumulateUsage(draft, usage) {
|
|
3589
|
+
const turn = draft.open_turn;
|
|
3590
|
+
if (turn === void 0) {
|
|
3591
|
+
draft.unattributed = accumulateUnattributed(draft.unattributed, usage);
|
|
3592
|
+
return;
|
|
3593
|
+
}
|
|
3594
|
+
if (turn.message_ids.includes(usage.uuid)) return;
|
|
3595
|
+
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");
|
|
3596
|
+
draft.open_turn = {
|
|
3597
|
+
...turn,
|
|
3598
|
+
last_observed_at: usage.observed_at > turn.last_observed_at ? usage.observed_at : turn.last_observed_at,
|
|
3599
|
+
message_ids: [...turn.message_ids, usage.uuid],
|
|
3600
|
+
counts: addCounts(turn.counts, usage)
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
3603
|
+
/**
|
|
3604
|
+
* Subagent transcripts live beside the session transcript at
|
|
3605
|
+
* `<session-id>/subagents/*.jsonl` with a `.meta.json` sibling the scan
|
|
3606
|
+
* ignores. Their usage belongs to the turn open when the scan runs: a
|
|
3607
|
+
* subagent only runs while its parent turn is in flight. A straggler line
|
|
3608
|
+
* that lands after the next user message therefore attributes to the open
|
|
3609
|
+
* turn, never to a closed one.
|
|
3610
|
+
*/
|
|
3611
|
+
async consumeSubagents(registration, draft) {
|
|
3612
|
+
const directory = subagentsDirectory(registration.transcript_path);
|
|
3613
|
+
let names;
|
|
3614
|
+
try {
|
|
3615
|
+
names = (await readdir2(directory)).filter((name) => name.endsWith(".jsonl") && !name.endsWith(".meta.json")).sort();
|
|
3616
|
+
} catch (error) {
|
|
3617
|
+
if (isMissingTranscriptError(error) || isNotDirectory(error)) return 0;
|
|
3618
|
+
throw new ClaudeHostError("payload_rejected", "Claude subagent transcript directory is unavailable.");
|
|
3619
|
+
}
|
|
3620
|
+
if (names.length > CLAUDE_TRANSCRIPT_MAX_SUBAGENT_FILES) {
|
|
3621
|
+
this.log(`Claude ignored subagent transcripts past its file bound: transcript_path=${JSON.stringify(registration.transcript_path)}, files=${names.length}.`);
|
|
3622
|
+
names = names.slice(0, CLAUDE_TRANSCRIPT_MAX_SUBAGENT_FILES);
|
|
3623
|
+
}
|
|
3624
|
+
let consumed = 0;
|
|
3625
|
+
for (const name of names) {
|
|
3626
|
+
consumed += await this.consumeSubagentFile(draft, directory, name);
|
|
3627
|
+
}
|
|
3628
|
+
return consumed;
|
|
3629
|
+
}
|
|
3630
|
+
async consumeSubagentFile(draft, directory, name) {
|
|
3631
|
+
const path = join4(directory, name);
|
|
3632
|
+
let handle;
|
|
3633
|
+
try {
|
|
3634
|
+
handle = await open5(path, O_RDONLY3 | O_NOFOLLOW2);
|
|
3635
|
+
const { size, device, inode } = await ownedClaudeTranscriptFile(handle);
|
|
3636
|
+
const entry = draft.subagents[name] ?? { offset: 0, anchor_start: 0, anchor_sha256: this.sha(Buffer.alloc(0)) };
|
|
3637
|
+
const pinned = await this.preflightSubagent(entry, size, device, inode);
|
|
3638
|
+
const chunk = await this.readChunk(handle, pinned.offset, size);
|
|
3639
|
+
if (chunk === "empty" || chunk === "partial") {
|
|
3640
|
+
draft.subagents[name] = pinned;
|
|
3641
|
+
return 0;
|
|
3642
|
+
}
|
|
3643
|
+
const consumed = this.consumeSubagentLines(draft, chunk);
|
|
3644
|
+
const offset = pinned.offset + chunk.length;
|
|
3645
|
+
const anchor = await this.readAnchor(handle, offset, CLAUDE_TRANSCRIPT_ANCHOR_BYTES);
|
|
3646
|
+
draft.subagents[name] = { device, inode, offset, anchor_start: anchor.start, anchor_sha256: anchor.sha256 };
|
|
3647
|
+
return consumed;
|
|
3648
|
+
} catch (error) {
|
|
3649
|
+
if (isMissingTranscriptError(error)) {
|
|
3650
|
+
delete draft.subagents[name];
|
|
3651
|
+
return 0;
|
|
3652
|
+
}
|
|
3653
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("payload_rejected", "Claude subagent transcript is unavailable.");
|
|
3654
|
+
} finally {
|
|
3655
|
+
await handle?.close();
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
consumeSubagentLines(draft, chunk) {
|
|
3659
|
+
let consumed = 0;
|
|
3660
|
+
for (const line of chunk.toString("utf8").split("\n").filter((value) => value.length > 0)) {
|
|
3661
|
+
consumed += 1;
|
|
3662
|
+
const record5 = parseTranscriptLine(line);
|
|
3663
|
+
if (record5 === null) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript line is not valid JSON.", "malformed_line");
|
|
3664
|
+
if (isAssistantUsageLine(record5)) this.accumulateUsage(draft, toUsage(record5.uuid, record5.timestamp, record5.message.usage));
|
|
3665
|
+
else if (isAssistantLine(record5) && record5.message.usage !== void 0) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript usage is malformed.");
|
|
3666
|
+
}
|
|
3667
|
+
return consumed;
|
|
3668
|
+
}
|
|
3669
|
+
async preflightSubagent(entry, size, device, inode) {
|
|
3670
|
+
if (entry.device === void 0) {
|
|
3671
|
+
if (entry.offset !== 0) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript cursor is unpinned past its start.");
|
|
3672
|
+
return { device, inode, offset: 0, anchor_start: 0, anchor_sha256: this.sha(Buffer.alloc(0)) };
|
|
3673
|
+
}
|
|
3674
|
+
if (entry.device !== device || entry.inode !== inode) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript was replaced.");
|
|
3675
|
+
if (size < entry.offset) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript was truncated.");
|
|
3676
|
+
return { device, inode, offset: entry.offset, anchor_start: entry.anchor_start, anchor_sha256: entry.anchor_sha256 };
|
|
3677
|
+
}
|
|
3678
|
+
/**
|
|
3679
|
+
* The open turn's terminal is emitted optimistically on every scan whose
|
|
3680
|
+
* new lines changed its totals, so usage is visible before the next user
|
|
3681
|
+
* message closes the turn. The emission digest changes with the content,
|
|
3682
|
+
* so a repeated scan over unchanged state emits nothing.
|
|
3683
|
+
*/
|
|
3684
|
+
async emitOpenTerminal(registration, draft) {
|
|
3685
|
+
const turn = draft.open_turn;
|
|
3686
|
+
if (turn === void 0 || turn.message_ids.length === 0) return;
|
|
3687
|
+
const digest2 = terminalDigest(turn);
|
|
3688
|
+
if (digest2 === turn.last_terminal_digest) return;
|
|
3689
|
+
await this.putTerminalEnvelope(registration, turn, digest2, void 0);
|
|
3690
|
+
draft.enqueued += 1;
|
|
3691
|
+
draft.open_turn = { ...turn, last_terminal_digest: digest2 };
|
|
3692
|
+
}
|
|
3693
|
+
async putTerminalEnvelope(registration, turn, digest2, gapAt) {
|
|
3694
|
+
const observedAt = gapAt ?? turn.last_observed_at;
|
|
3695
|
+
const tokenValue = turn.message_ids.length === 0 ? hostObservationNotRecorded("claude_turn_usage_not_observed") : availableTokenValue(turn.counts);
|
|
3696
|
+
const identity = { host_source_ref: registration.host_source_ref, host_session_ref: registration.host_session_ref, host_turn_ref: turn.turn_key };
|
|
3697
|
+
await this.store.putEnvelope({
|
|
3698
|
+
schema_version: "ceal.claude_observation_outbox.v1",
|
|
3699
|
+
identity,
|
|
3700
|
+
host_input_ref: turn.host_input_ref,
|
|
3701
|
+
input_kind: "host_automatic",
|
|
3702
|
+
prompt_ref: turn.prompt_ref,
|
|
3703
|
+
observations: [
|
|
3704
|
+
{ 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 } },
|
|
3705
|
+
{ observation_ref: observationRef("transcript_token", turn.turn_key, digest2), observation_kind: "token", observed_at: observedAt, value: tokenValue },
|
|
3706
|
+
{ observation_ref: observationRef("transcript_cost", turn.turn_key, digest2), observation_kind: "cost", observed_at: observedAt, value: hostObservationNotRecorded("provider_cost_not_observed") }
|
|
3707
|
+
],
|
|
3708
|
+
origin_input: false,
|
|
3709
|
+
submission_ref: submissionRef("terminal", turn.turn_key, digest2, turn.prompt_ref),
|
|
3710
|
+
created_at: observedAt,
|
|
3711
|
+
expires_at: this.expire(this.outboxRetentionMs)
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3714
|
+
async commitCursor(registration, handle, draft, device, inode) {
|
|
3715
|
+
const anchor = await this.readAnchor(handle, draft.offset, CLAUDE_TRANSCRIPT_ANCHOR_BYTES);
|
|
3716
|
+
await this.store.writeTranscriptCursor({
|
|
3717
|
+
schema_version: CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION,
|
|
3718
|
+
host_source_ref: registration.host_source_ref,
|
|
3719
|
+
host_session_ref: registration.host_session_ref,
|
|
3720
|
+
transcript_path: registration.transcript_path,
|
|
3721
|
+
device,
|
|
3722
|
+
inode,
|
|
3723
|
+
offset: draft.offset,
|
|
3724
|
+
anchor_start: anchor.start,
|
|
3725
|
+
anchor_sha256: anchor.sha256,
|
|
3726
|
+
subagents: draft.subagents,
|
|
3727
|
+
...draft.open_turn === void 0 ? {} : { open_turn: draft.open_turn },
|
|
3728
|
+
...draft.unattributed === void 0 ? {} : { unattributed: draft.unattributed },
|
|
3729
|
+
...draft.session_observed ? { session_observed: true } : {},
|
|
3730
|
+
status: "active",
|
|
3731
|
+
updated_at: this.stamp()
|
|
3732
|
+
});
|
|
3733
|
+
}
|
|
3734
|
+
async registerPrompt(sessionRef, turnRef, promptRef, prompt) {
|
|
3735
|
+
if (this.producerInstallationRef === void 0 || this.registerCurrentPrompt === void 0) return;
|
|
3736
|
+
try {
|
|
3737
|
+
await this.registerCurrentPrompt({
|
|
3738
|
+
producer_id: "claude",
|
|
3739
|
+
producer_installation_ref: this.producerInstallationRef,
|
|
3740
|
+
local_session_ref: sessionRef,
|
|
3741
|
+
local_turn_ref: turnRef,
|
|
3742
|
+
prompt_ref: promptRef,
|
|
3743
|
+
current_prompt: prompt,
|
|
3744
|
+
context_expires_at: this.expire(this.outboxRetentionMs)
|
|
3745
|
+
});
|
|
3746
|
+
this.diagnostic("current_prompt_registered");
|
|
3747
|
+
} catch (error) {
|
|
3748
|
+
this.diagnostic("current_prompt_registration_failed");
|
|
3749
|
+
throw error;
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
async activateContext(identity, promptRef, turnGenerationRef) {
|
|
3753
|
+
try {
|
|
3754
|
+
await this.store.activateInvocationContext(identity, promptRef, this.expire(this.outboxRetentionMs), void 0, turnGenerationRef);
|
|
3755
|
+
this.diagnostic("activation_recorded");
|
|
3756
|
+
} catch (error) {
|
|
3757
|
+
this.diagnostic("activation_failed");
|
|
3758
|
+
throw error;
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
async quarantineStored(cursor, device, inode, reason) {
|
|
3762
|
+
await this.store.writeTranscriptCursor({ ...cursor, device, inode, status: "quarantined", reason, updated_at: this.stamp() });
|
|
3763
|
+
return null;
|
|
3764
|
+
}
|
|
3765
|
+
async quarantineUnopened(registration, reason) {
|
|
3766
|
+
const existing = await this.store.readTranscriptCursor(registration).catch(() => null);
|
|
3767
|
+
await this.store.writeTranscriptCursor({ ...existing ?? this.initialCursor(registration), transcript_path: registration.transcript_path, status: "quarantined", reason, updated_at: this.stamp() });
|
|
3768
|
+
return { records: 0, enqueued: 0, quarantined: true };
|
|
3769
|
+
}
|
|
3770
|
+
async quarantineDraft(cursor) {
|
|
3771
|
+
await this.store.writeTranscriptCursor({ ...cursor, status: "quarantined", reason: "transcript_malformed", updated_at: this.stamp() });
|
|
3772
|
+
return { records: 0, enqueued: 0, quarantined: true };
|
|
3773
|
+
}
|
|
3774
|
+
async quarantineFailure(registration, handle) {
|
|
3775
|
+
if (handle === void 0) return;
|
|
3776
|
+
const stats = await handle.stat().catch(() => null);
|
|
3777
|
+
if (stats === null) return;
|
|
3778
|
+
const existing = await this.store.readTranscriptCursor(registration).catch(() => null);
|
|
3779
|
+
const cursor = existing ?? this.initialCursor(registration);
|
|
3780
|
+
await this.store.writeTranscriptCursor({ ...cursor, device: String(stats.dev), inode: String(stats.ino), status: "quarantined", reason: "transcript_malformed", updated_at: this.stamp() });
|
|
3781
|
+
}
|
|
3782
|
+
async preservedCursor(registration) {
|
|
3783
|
+
return await this.store.readTranscriptCursor(registration).catch(() => null) ?? this.initialCursor(registration);
|
|
3784
|
+
}
|
|
3785
|
+
};
|
|
3786
|
+
function mutableCursor(cursor) {
|
|
3787
|
+
return {
|
|
3788
|
+
offset: cursor.offset,
|
|
3789
|
+
...cursor.open_turn === void 0 ? {} : { open_turn: cursor.open_turn },
|
|
3790
|
+
...cursor.unattributed === void 0 ? {} : { unattributed: cursor.unattributed },
|
|
3791
|
+
subagents: Object.fromEntries(Object.entries(cursor.subagents).map(([name, entry]) => [name, { ...entry }])),
|
|
3792
|
+
session_observed: cursor.session_observed ?? false,
|
|
3793
|
+
consumed: 0,
|
|
3794
|
+
enqueued: 0
|
|
3795
|
+
};
|
|
3796
|
+
}
|
|
3797
|
+
function zeroCounts() {
|
|
3798
|
+
return { input: 0, cached_input: 0, cache_write_input: 0, output: 0 };
|
|
3799
|
+
}
|
|
3800
|
+
function terminalDigest(turn) {
|
|
3801
|
+
return transcriptObjectSha256({ turn: turn.turn_key, messages: turn.message_ids, counts: turn.counts });
|
|
3802
|
+
}
|
|
3803
|
+
function availableTokenValue(counts) {
|
|
3804
|
+
const input = counts.input;
|
|
3805
|
+
const output = counts.output;
|
|
3806
|
+
if (!Number.isSafeInteger(input + output)) throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
3807
|
+
return createHostTokenObservationValue({
|
|
3808
|
+
input,
|
|
3809
|
+
output,
|
|
3810
|
+
total: input + output,
|
|
3811
|
+
cached_input: counts.cached_input,
|
|
3812
|
+
cache_write_input: counts.cache_write_input
|
|
3813
|
+
}, TOKEN_OBSERVATION_SOURCE);
|
|
3814
|
+
}
|
|
3815
|
+
function addCounts(current, next) {
|
|
3816
|
+
const counts = {
|
|
3817
|
+
input: current.input + next.input,
|
|
3818
|
+
cached_input: current.cached_input + next.cached_input,
|
|
3819
|
+
cache_write_input: current.cache_write_input + next.cache_write_input,
|
|
3820
|
+
output: current.output + next.output
|
|
3821
|
+
};
|
|
3822
|
+
if (![counts.input, counts.cached_input, counts.cache_write_input, counts.output].every((value) => Number.isSafeInteger(value) && value >= 0)) {
|
|
3823
|
+
throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
3824
|
+
}
|
|
3825
|
+
return counts;
|
|
3826
|
+
}
|
|
3827
|
+
function accumulateUnattributed(current, usage) {
|
|
3828
|
+
const base = current ?? { input: 0, cached_input: 0, cache_write_input: 0, output: 0, message_ids: [], last_observed_at: usage.observed_at };
|
|
3829
|
+
if (base.message_ids.includes(usage.uuid)) return base;
|
|
3830
|
+
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");
|
|
3831
|
+
const counts = addCounts(base, usage);
|
|
3832
|
+
return {
|
|
3833
|
+
...counts,
|
|
3834
|
+
message_ids: [...base.message_ids, usage.uuid],
|
|
3835
|
+
last_observed_at: usage.observed_at > base.last_observed_at ? usage.observed_at : base.last_observed_at
|
|
3836
|
+
};
|
|
3837
|
+
}
|
|
3838
|
+
function foldUnattributed(bucket) {
|
|
3839
|
+
if (bucket === void 0) return void 0;
|
|
3840
|
+
if (![bucket.input, bucket.cached_input, bucket.cache_write_input, bucket.output].every((value) => Number.isSafeInteger(value) && value >= 0)) {
|
|
3841
|
+
throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
3842
|
+
}
|
|
3843
|
+
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 };
|
|
3844
|
+
}
|
|
3845
|
+
function toUsage(uuid, observedAt, usage) {
|
|
3846
|
+
const input = usageNumber(usage.input_tokens);
|
|
3847
|
+
const cached = usageNumber(usage.cache_read_input_tokens ?? 0);
|
|
3848
|
+
const created = usageNumber(usage.cache_creation_input_tokens ?? 0);
|
|
3849
|
+
const output = usageNumber(usage.output_tokens);
|
|
3850
|
+
if (input === null || cached === null || created === null || output === null) throw new ClaudeHostError("payload_rejected", "Claude transcript usage is malformed.");
|
|
3851
|
+
const total = input + cached + created;
|
|
3852
|
+
if (![total, output].every((value) => Number.isSafeInteger(value) && value >= 0)) throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
3853
|
+
return { uuid, observed_at: observedAt, input: total, cached_input: cached, cache_write_input: created, output };
|
|
3854
|
+
}
|
|
3855
|
+
function usageNumber(value) {
|
|
3856
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
3857
|
+
}
|
|
3858
|
+
function parseTranscriptLine(line) {
|
|
3859
|
+
try {
|
|
3860
|
+
return JSON.parse(line);
|
|
3861
|
+
} catch {
|
|
3862
|
+
return null;
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
function lineIdentity(record5) {
|
|
3866
|
+
if (typeof record5.uuid !== "string" || record5.uuid.length === 0 || record5.uuid.length > 256) return null;
|
|
3867
|
+
if (typeof record5.timestamp !== "string" || !Number.isFinite(Date.parse(record5.timestamp))) return null;
|
|
3868
|
+
return { uuid: record5.uuid, timestamp: record5.timestamp };
|
|
3869
|
+
}
|
|
3870
|
+
function isUserTurnLine(record5) {
|
|
3871
|
+
if (!isTranscriptRecord(record5) || record5.type !== "user") return false;
|
|
3872
|
+
if (!isTranscriptRecord(record5.message) || typeof record5.message.content !== "string") return false;
|
|
3873
|
+
if (record5.isMeta === true && (!isTranscriptRecord(record5.origin) || typeof record5.origin.kind !== "string")) return false;
|
|
3874
|
+
const identity = lineIdentity(record5);
|
|
3875
|
+
if (identity === null) throw new ClaudeHostError("payload_rejected", "Claude transcript user line is malformed.");
|
|
3876
|
+
return true;
|
|
3877
|
+
}
|
|
3878
|
+
function claudeTranscriptScanFailureKind(error) {
|
|
3879
|
+
if (isSymlinkTranscript(error)) return "symlink";
|
|
3880
|
+
if (error instanceof ClaudeHostError) {
|
|
3881
|
+
if (error.diagnostic_kind !== void 0) return error.diagnostic_kind;
|
|
3882
|
+
if (error.code === "payload_rejected") return "payload_rejected";
|
|
3883
|
+
if (error.code === "storage") return "read_error";
|
|
3884
|
+
}
|
|
3885
|
+
const code2 = transcriptErrorCode(error);
|
|
3886
|
+
if (code2 === "EFBIG") return "size_budget";
|
|
3887
|
+
if (["EBADF", "EACCES", "EIO", "EMFILE", "ENFILE", "ENOENT", "ENOTDIR", "EPERM"].includes(code2 ?? "")) return "read_error";
|
|
3888
|
+
return "other";
|
|
3889
|
+
}
|
|
3890
|
+
function isAssistantLine(record5) {
|
|
3891
|
+
return isTranscriptRecord(record5) && record5.type === "assistant" && isTranscriptRecord(record5.message);
|
|
3892
|
+
}
|
|
3893
|
+
function isAssistantUsageLine(record5) {
|
|
3894
|
+
if (!isTranscriptRecord(record5)) return false;
|
|
3895
|
+
if (!isAssistantLine(record5) || record5.message.usage === void 0) return false;
|
|
3896
|
+
const identity = lineIdentity(record5);
|
|
3897
|
+
if (identity === null || !isTranscriptRecord(record5.message.usage)) throw new ClaudeHostError("payload_rejected", "Claude transcript usage line is malformed.");
|
|
3898
|
+
return true;
|
|
3899
|
+
}
|
|
3900
|
+
function subagentsDirectory(transcriptPath) {
|
|
3901
|
+
const stem = transcriptPath.endsWith(".jsonl") ? transcriptPath.slice(0, -".jsonl".length) : transcriptPath;
|
|
3902
|
+
return join4(stem, "subagents");
|
|
3903
|
+
}
|
|
3904
|
+
function observationRef(event, identity, value) {
|
|
3905
|
+
return cealHostObservationRef("claude", transcriptObjectSha256({ event, identity, value }));
|
|
3906
|
+
}
|
|
3907
|
+
function submissionRef(kind, turn, digest2, prompt) {
|
|
3908
|
+
return `claude-submission:${transcriptObjectSha256({ kind, turn, digest: digest2, prompt })}`;
|
|
3909
|
+
}
|
|
3910
|
+
async function ownedClaudeTranscriptFile(handle) {
|
|
3911
|
+
const stats = await handle.stat();
|
|
3912
|
+
const uid = typeof process.geteuid === "function" ? process.geteuid() : stats.uid;
|
|
3913
|
+
if (!stats.isFile()) throw new ClaudeHostError("payload_rejected", "Claude transcript source is not a regular file.");
|
|
3914
|
+
if (stats.uid !== uid) throw new ClaudeHostError("payload_rejected", "Claude transcript source is owned by another user.");
|
|
3915
|
+
return { size: stats.size, device: String(stats.dev), inode: String(stats.ino) };
|
|
3916
|
+
}
|
|
3917
|
+
function isNotDirectory(error) {
|
|
3918
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOTDIR";
|
|
3919
|
+
}
|
|
3920
|
+
function isSymlinkTranscript(error) {
|
|
3921
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ELOOP";
|
|
3922
|
+
}
|
|
3923
|
+
function transcriptErrorCode(error) {
|
|
3924
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
// host/claude/src/claude-producer-runtime.ts
|
|
3928
|
+
async function drainClaudeProducerOnce(options) {
|
|
3929
|
+
const promptPort = new HostRelayCurrentPromptPort(options.relay_endpoint, options.timeout_ms);
|
|
3930
|
+
const scan = await new ClaudeTranscriptWatcher({
|
|
3931
|
+
outbox_dir: options.outbox_dir,
|
|
3932
|
+
producer_installation_ref: options.producer_installation_ref,
|
|
3933
|
+
register_current_prompt: (input) => promptPort.register(input),
|
|
3934
|
+
diagnostic: (event, failure_kind) => writeHostServiceDiagnostic("claude", event, failure_kind)
|
|
3935
|
+
}).scanOnce().catch((error) => {
|
|
3936
|
+
writeHostServiceDiagnostic("claude", "watcher_scan_failed", claudeTranscriptScanFailureKind(error));
|
|
3937
|
+
throw error;
|
|
3938
|
+
});
|
|
3939
|
+
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 } });
|
|
3940
|
+
return { scanned: scan.enqueued, ...await producer.drainOutbox() };
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
// host/claude/src/claude-producer-service.ts
|
|
3944
|
+
var CLAUDE_PRODUCER_CONFIG_SCHEMA = "ceal.claude_producer_config.v1";
|
|
3945
|
+
var CLAUDE_TRANSCRIPTS_ROOT = "claude_transcripts_root";
|
|
3946
|
+
var CLAUDE_PRODUCER_DESCRIPTOR = {
|
|
3947
|
+
schema: CLAUDE_PRODUCER_CONFIG_SCHEMA,
|
|
3948
|
+
producer_id: "claude",
|
|
3949
|
+
extra_paths: [CLAUDE_TRANSCRIPTS_ROOT],
|
|
3950
|
+
config: (shared, extras) => ({ ...shared, claude_transcripts_root: hostProducerExtraPath(extras, CLAUDE_TRANSCRIPTS_ROOT) }),
|
|
3951
|
+
runtime_options: (config) => Object.freeze({
|
|
3952
|
+
outbox_dir: config.outbox_dir,
|
|
3953
|
+
producer_installation_ref: config.producer_installation_ref,
|
|
3954
|
+
producer_epoch: config.producer_epoch,
|
|
3955
|
+
relay_endpoint: config.relay_endpoint,
|
|
3956
|
+
timeout_ms: config.ipc_timeout_ms
|
|
3957
|
+
}),
|
|
3958
|
+
drain: drainClaudeProducerOnce
|
|
3959
|
+
};
|
|
3960
|
+
function runClaudeProducerCli(args, signal, dependencies = {}) {
|
|
3961
|
+
return runHostProducerCli(CLAUDE_PRODUCER_DESCRIPTOR, args, signal, dependencies);
|
|
3962
|
+
}
|
|
3963
|
+
|
|
2583
3964
|
// host/producer-command.ts
|
|
2584
3965
|
async function runProducerCommand(schemaVersion, run) {
|
|
2585
3966
|
const controller = new AbortController();
|
|
@@ -2590,8 +3971,8 @@ async function runProducerCommand(schemaVersion, run) {
|
|
|
2590
3971
|
try {
|
|
2591
3972
|
await run(process.argv.slice(2), controller.signal);
|
|
2592
3973
|
} catch (error) {
|
|
2593
|
-
const
|
|
2594
|
-
process.stderr.write(`${JSON.stringify({ schema_version: schemaVersion, ok: false, code:
|
|
3974
|
+
const code2 = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "producer_failed";
|
|
3975
|
+
process.stderr.write(`${JSON.stringify({ schema_version: schemaVersion, ok: false, code: code2 })}
|
|
2595
3976
|
`);
|
|
2596
3977
|
process.exitCode = 1;
|
|
2597
3978
|
} finally {
|