@corca-org/ceal-host-linux-arm64 1.0.18 → 1.0.20
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 +2041 -514
- package/libexec/ceal-host/claude-invocation-authentication.mjs +1899 -330
- package/libexec/ceal-host/claude-producer.mjs +1860 -406
- package/libexec/ceal-host/codex-hook.mjs +1618 -1013
- package/libexec/ceal-host/codex-invocation-authentication.mjs +3222 -505
- package/libexec/ceal-host/codex-producer.mjs +1126 -581
- package/libexec/ceal-host/control.mjs +2857 -1134
- package/libexec/ceal-host/relay.mjs +559 -116
- 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"]);
|
|
@@ -991,6 +1016,7 @@ var REQUIRED_FIELDS = CEAL_CLIENT_WIRE_CONTRACT.command_response.bare_document_f
|
|
|
991
1016
|
var OPTIONAL_FIELDS = CEAL_CLIENT_WIRE_CONTRACT.command_response.bare_document_optional_fields;
|
|
992
1017
|
|
|
993
1018
|
// packages/ceal-client-protocol/src/resource-row.ts
|
|
1019
|
+
var CEAL_RESOURCE_ROW_TITLE_MATCH_DISPOSITIONS = vocabulary(["exact", "not_exact", "unknown"]);
|
|
994
1020
|
var CEAL_RESOURCE_SEARCH_ORDERS = vocabulary(["relevance", "modified", "viewed", "created"]);
|
|
995
1021
|
var CEAL_RESOURCE_SEARCH_DIRECTIONS = vocabulary(["ascending", "descending"]);
|
|
996
1022
|
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"]);
|
|
@@ -1014,6 +1040,7 @@ var CEAL_ACTIVITY_TIMESTAMP_BASES = vocabulary([
|
|
|
1014
1040
|
"resource_modified"
|
|
1015
1041
|
]);
|
|
1016
1042
|
var CEAL_ACTIVITY_CONNECTOR_KINDS = vocabulary(["slack", "github", "google-calendar", "google-drive", "notion"]);
|
|
1043
|
+
var CEAL_ACTIVITY_UNSUPPORTED_EVENT_KIND_CODE = "unsupported_event_kind";
|
|
1017
1044
|
var CEAL_ACTIVITY_GAP_CODES = vocabulary([
|
|
1018
1045
|
"provider_page_budget",
|
|
1019
1046
|
"call_deadline",
|
|
@@ -1021,7 +1048,7 @@ var CEAL_ACTIVITY_GAP_CODES = vocabulary([
|
|
|
1021
1048
|
"provider_cursor_unavailable",
|
|
1022
1049
|
"malformed_provider_page",
|
|
1023
1050
|
"visibility_incomplete",
|
|
1024
|
-
|
|
1051
|
+
CEAL_ACTIVITY_UNSUPPORTED_EVENT_KIND_CODE,
|
|
1025
1052
|
"direct_messages_excluded"
|
|
1026
1053
|
]);
|
|
1027
1054
|
var CEAL_ACTIVITY_EVENT_KINDS = vocabulary([
|
|
@@ -1073,6 +1100,7 @@ var RESOURCE_ROW_KEY_RULES = Object.freeze({
|
|
|
1073
1100
|
next: { required: true, timestamp: false },
|
|
1074
1101
|
kind: { required: true, timestamp: false },
|
|
1075
1102
|
matched_kinds: { required: false, timestamp: false },
|
|
1103
|
+
title_match: { required: false, timestamp: false },
|
|
1076
1104
|
title: { required: true, timestamp: false },
|
|
1077
1105
|
container_label: { required: true, timestamp: false },
|
|
1078
1106
|
container_ref: { required: false, timestamp: false },
|
|
@@ -1086,6 +1114,8 @@ var RESOURCE_ROW_KEY_RULES = Object.freeze({
|
|
|
1086
1114
|
modified_at: { required: false, timestamp: true },
|
|
1087
1115
|
modified_at_local: { required: false, timestamp: true },
|
|
1088
1116
|
viewed_by_me_at: { required: false, timestamp: true },
|
|
1117
|
+
own_ceal_write: { required: false, timestamp: false },
|
|
1118
|
+
owned_by_me: { required: false, timestamp: false },
|
|
1089
1119
|
visibility: { required: false, timestamp: false },
|
|
1090
1120
|
start: { required: false, timestamp: false },
|
|
1091
1121
|
end: { required: false, timestamp: false },
|
|
@@ -1119,6 +1149,15 @@ function cealExcludedTargetCoverageJsonSchema() {
|
|
|
1119
1149
|
};
|
|
1120
1150
|
}
|
|
1121
1151
|
|
|
1152
|
+
// packages/ceal-client-protocol/src/gateway-write-identity.ts
|
|
1153
|
+
var CEAL_GATEWAY_WRITE_IDENTITY_ROLES = Object.freeze(["replay_identity", "lookup_handle", "collision_evidence"]);
|
|
1154
|
+
var CEAL_GATEWAY_WRITE_REQUEST_REF_PATTERN = /^gateway-write-request:[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/u;
|
|
1155
|
+
var CEAL_GATEWAY_WRITE_IDENTITY_FIELDS = Object.freeze({
|
|
1156
|
+
replay_identity: "idempotency_claim_sha256",
|
|
1157
|
+
lookup_handle: "write_request_sha256",
|
|
1158
|
+
collision_evidence: "normalized_mutation_sha256"
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1122
1161
|
// packages/ceal-client-protocol/src/write-safety-finding.ts
|
|
1123
1162
|
var CEAL_WRITE_SAFETY_FINDING_STATUSES = vocabulary(["warning", "unchecked"]);
|
|
1124
1163
|
var CEAL_WRITE_SAFETY_AUDIENCES = vocabulary(["workspace", "person", "members", "unknown"]);
|
|
@@ -1146,7 +1185,11 @@ var CEAL_REFRESH_FAILURE_CODES = vocabulary([
|
|
|
1146
1185
|
"refresh_revoked",
|
|
1147
1186
|
"authority_replaced",
|
|
1148
1187
|
"refresh_recovery_unavailable",
|
|
1149
|
-
"refresh_recovery_expired"
|
|
1188
|
+
"refresh_recovery_expired",
|
|
1189
|
+
// The serving gate answers this while the Gateway is still starting and the
|
|
1190
|
+
// refresh was never attempted; the same attempt stays retryable. It is never
|
|
1191
|
+
// stored by the session owner, only answered at the serving edge.
|
|
1192
|
+
"gateway_starting"
|
|
1150
1193
|
]);
|
|
1151
1194
|
|
|
1152
1195
|
// packages/ceal-client-protocol/src/request-validation-primitives.ts
|
|
@@ -1179,7 +1222,7 @@ var CANCELLATION_STATES = vocabulary(["requested", "cancelled"]);
|
|
|
1179
1222
|
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"]);
|
|
1180
1223
|
var CEAL_REQUEST_JSON_SCHEMA = {
|
|
1181
1224
|
type: "object",
|
|
1182
|
-
maxProperties:
|
|
1225
|
+
maxProperties: 11,
|
|
1183
1226
|
additionalProperties: false,
|
|
1184
1227
|
properties: {
|
|
1185
1228
|
schema_version: { type: "string", const: CEAL_REQUEST_SCHEMA_VERSION, maxLength: 32 },
|
|
@@ -1203,7 +1246,8 @@ var CEAL_REQUEST_JSON_SCHEMA = {
|
|
|
1203
1246
|
host_turn_ref: { type: "string", minLength: 1, maxLength: 128, pattern: SAFE_REF.source },
|
|
1204
1247
|
host_input_ref: { type: "string", minLength: 1, maxLength: 128, pattern: SAFE_REF.source },
|
|
1205
1248
|
host_source_ref: { type: "string", minLength: 1, maxLength: 128, pattern: SAFE_REF.source },
|
|
1206
|
-
requester_timezone: { type: "string", minLength: 1, maxLength: 128, description: "IANA timezone observed for the requester on this Request." }
|
|
1249
|
+
requester_timezone: { type: "string", minLength: 1, maxLength: 128, description: "IANA timezone observed for the requester on this Request." },
|
|
1250
|
+
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." }
|
|
1207
1251
|
},
|
|
1208
1252
|
required: CEAL_REQUEST_KEYS
|
|
1209
1253
|
};
|
|
@@ -1262,6 +1306,7 @@ var REQUEST_EXECUTION_PREPARE_REFUSAL_PROPERTIES = {
|
|
|
1262
1306
|
retryable: { type: "boolean" },
|
|
1263
1307
|
source: { type: "string", enum: ["provider", "gateway"] },
|
|
1264
1308
|
retry_after_ms: { type: "integer", minimum: 0, maximum: CEAL_RETRY_AFTER_MAX_MS },
|
|
1309
|
+
write_request_ref: { type: "string", pattern: CEAL_GATEWAY_WRITE_REQUEST_REF_PATTERN.source, maxLength: 128 },
|
|
1265
1310
|
excluded_targets: cealExcludedTargetCoverageJsonSchema(),
|
|
1266
1311
|
repaired_input: {},
|
|
1267
1312
|
unrepaired_pointers: { type: "array", maxItems: 128, items: { type: "string", minLength: 1, maxLength: 256 } },
|
|
@@ -1320,12 +1365,55 @@ function isCealHostInputKind(value) {
|
|
|
1320
1365
|
return typeof value === "string" && CEAL_HOST_INPUT_KINDS.some((kind) => kind === value);
|
|
1321
1366
|
}
|
|
1322
1367
|
var CEAL_HOST_OBSERVATION_REF_PREFIX = "host-observation:";
|
|
1368
|
+
var HOST_OBSERVATION_PRODUCER = /^[a-z][a-z0-9]{0,31}$/u;
|
|
1369
|
+
var HOST_OBSERVATION_DIGEST = /^[a-f0-9]{8,64}$/u;
|
|
1370
|
+
function cealHostObservationRef(producer, digest2) {
|
|
1371
|
+
if (!HOST_OBSERVATION_PRODUCER.test(producer) || !HOST_OBSERVATION_DIGEST.test(digest2)) invalid("Ceal Host observation reference parts are invalid.");
|
|
1372
|
+
return `${CEAL_HOST_OBSERVATION_REF_PREFIX}${producer}-${digest2}`;
|
|
1373
|
+
}
|
|
1374
|
+
var HOST_TOKEN_OBSERVATION_BREAKDOWN_KEYS = vocabulary(["cached_input", "cache_write_input", "reasoning_output"]);
|
|
1375
|
+
function createHostTokenObservationValue(counts, source) {
|
|
1376
|
+
const core = validTokenCore(counts);
|
|
1377
|
+
if (typeof source !== "string" || source.length === 0 || source.length > 256) invalid("Ceal Host token observation source is invalid.");
|
|
1378
|
+
const value = { status: "available", unit: "tokens", input: core.input, output: core.output, total: core.total, source };
|
|
1379
|
+
for (const breakdown of validTokenBreakdowns(counts)) value[breakdown.key] = breakdown.tokens;
|
|
1380
|
+
assertHostObservationValue(value, "token");
|
|
1381
|
+
return value;
|
|
1382
|
+
}
|
|
1383
|
+
function validTokenCore(counts) {
|
|
1384
|
+
if (!isOperationRecord(counts) || !safeCount(counts.input) || !safeCount(counts.output) || !safeCount(counts.total)) invalid("Ceal Host token observation counts are invalid.");
|
|
1385
|
+
if (counts.total !== counts.input + counts.output) invalid("Ceal Host token observation counts are invalid.");
|
|
1386
|
+
return { input: counts.input, output: counts.output, total: counts.total };
|
|
1387
|
+
}
|
|
1388
|
+
function validTokenBreakdowns(counts) {
|
|
1389
|
+
const breakdowns = [
|
|
1390
|
+
{ key: "cached_input", tokens: counts.cached_input },
|
|
1391
|
+
{ key: "cache_write_input", tokens: counts.cache_write_input },
|
|
1392
|
+
{ key: "reasoning_output", tokens: counts.reasoning_output }
|
|
1393
|
+
];
|
|
1394
|
+
const stated = breakdowns.filter((breakdown) => breakdown.tokens !== void 0).map((breakdown) => ({ key: breakdown.key, tokens: Number(breakdown.tokens) }));
|
|
1395
|
+
for (const breakdown of stated) {
|
|
1396
|
+
if (!safeCount(breakdown.tokens)) invalid("Ceal Host token observation counts are invalid.");
|
|
1397
|
+
}
|
|
1398
|
+
const cached = counts.cached_input ?? 0;
|
|
1399
|
+
const created = counts.cache_write_input ?? 0;
|
|
1400
|
+
const reasoning = counts.reasoning_output ?? 0;
|
|
1401
|
+
if (cached + created > counts.input || reasoning > counts.output) invalid("Ceal Host token observation counts are invalid.");
|
|
1402
|
+
return stated;
|
|
1403
|
+
}
|
|
1404
|
+
function hostObservationNotRecorded(reason) {
|
|
1405
|
+
if (typeof reason !== "string" || reason.length === 0 || reason.length > 256) invalid("Ceal Host observation gap reason is invalid.");
|
|
1406
|
+
return { status: "not_recorded", reason };
|
|
1407
|
+
}
|
|
1408
|
+
function safeCount(value) {
|
|
1409
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
1410
|
+
}
|
|
1323
1411
|
function validateCealHostObservationDraft(value) {
|
|
1324
|
-
const
|
|
1325
|
-
requirePrefixedRef(
|
|
1326
|
-
requireClosedValue(
|
|
1327
|
-
requireIsoTimestamp(
|
|
1328
|
-
assertHostObservationValue(
|
|
1412
|
+
const record5 = operationRecord(value, "Ceal Host observation draft is invalid.");
|
|
1413
|
+
requirePrefixedRef(record5.observation_ref, CEAL_HOST_OBSERVATION_REF_PREFIX, "Ceal Host observation reference is invalid.");
|
|
1414
|
+
requireClosedValue(record5.observation_kind, CEAL_HOST_OBSERVATION_KINDS, "Ceal Host observation kind is invalid.");
|
|
1415
|
+
requireIsoTimestamp(record5.observed_at, "Ceal Host observation time is invalid.");
|
|
1416
|
+
assertHostObservationValue(record5.value, record5.observation_kind);
|
|
1329
1417
|
}
|
|
1330
1418
|
var CEAL_REQUEST_MAX_OBSERVATION_VALUE_BYTES = 64 * 1024;
|
|
1331
1419
|
var CEAL_HOST_OBSERVATION_KINDS = vocabulary(["prompt", "session", "turn", "model", "token", "cost", "timing"]);
|
|
@@ -1355,6 +1443,10 @@ function inspectHostKeys(value) {
|
|
|
1355
1443
|
// packages/ceal-client-protocol/src/host-prompt-ref.ts
|
|
1356
1444
|
var CEAL_HOST_PROMPT_DIGEST_PATTERN = "^[a-f0-9]{64}$";
|
|
1357
1445
|
var PROMPT_DIGEST = new RegExp(CEAL_HOST_PROMPT_DIGEST_PATTERN, "u");
|
|
1446
|
+
function hostProducerPromptRef(promptSha256) {
|
|
1447
|
+
if (!PROMPT_DIGEST.test(promptSha256)) throw new TypeError("Canonical Host prompt digest is invalid.");
|
|
1448
|
+
return `ceal-host-prompt:${promptSha256}`;
|
|
1449
|
+
}
|
|
1358
1450
|
|
|
1359
1451
|
// packages/ceal-client-protocol/src/request-effect-approval.ts
|
|
1360
1452
|
var CEAL_EFFECT_APPROVAL_DECISION_CHECK_PHRASES = Object.freeze([
|
|
@@ -1467,12 +1559,85 @@ var CEAL_HOST_MINT_REQUEST_CONTEXT_REFUSAL_REASONS = vocabulary([
|
|
|
1467
1559
|
"host_input_mismatch",
|
|
1468
1560
|
"host_prompt_mismatch"
|
|
1469
1561
|
]);
|
|
1562
|
+
var CEAL_HOST_AUTHENTICATION_CAUSES = vocabulary([
|
|
1563
|
+
"invalid_arguments",
|
|
1564
|
+
"options_invalid",
|
|
1565
|
+
"session_unavailable",
|
|
1566
|
+
"context_unavailable",
|
|
1567
|
+
"context_replaced",
|
|
1568
|
+
"no_binding",
|
|
1569
|
+
"context_stale",
|
|
1570
|
+
"context_expired",
|
|
1571
|
+
"host_source_refused",
|
|
1572
|
+
"host_credential_refused",
|
|
1573
|
+
"gateway_unavailable",
|
|
1574
|
+
"gateway_rate_limited",
|
|
1575
|
+
"gateway_refused",
|
|
1576
|
+
"gateway_response_invalid",
|
|
1577
|
+
"producer_frame_invalid",
|
|
1578
|
+
"relay_permission_denied",
|
|
1579
|
+
"relay_unavailable",
|
|
1580
|
+
"transient_retryable",
|
|
1581
|
+
"mint_failed",
|
|
1582
|
+
"relay_failed",
|
|
1583
|
+
"host_refusal_invalid"
|
|
1584
|
+
]);
|
|
1585
|
+
var CEAL_HOST_AUTHENTICATION_POLICY = Object.freeze({
|
|
1586
|
+
invalid_arguments: Object.freeze({ classification: "host_arguments_invalid", next_action: "answer_with_gap" }),
|
|
1587
|
+
options_invalid: Object.freeze({ classification: "host_configuration_invalid", next_action: "answer_with_gap" }),
|
|
1588
|
+
session_unavailable: Object.freeze({ classification: "host_session_unavailable", next_action: "answer_with_gap" }),
|
|
1589
|
+
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; send a new message to start a fresh Host turn, then retry this command on that turn (run `ceal-host install` only if the Host hook registration is missing or drifted)" }),
|
|
1590
|
+
context_replaced: Object.freeze({ classification: "host_invocation_context_stale", next_action: "refresh_host_invocation_context" }),
|
|
1591
|
+
no_binding: Object.freeze({ classification: "host_binding_unavailable", next_action: "refresh_host_invocation_context" }),
|
|
1592
|
+
context_stale: Object.freeze({ classification: "host_invocation_context_stale", next_action: "refresh_host_invocation_context" }),
|
|
1593
|
+
context_expired: Object.freeze({ classification: "host_invocation_context_expired", next_action: "refresh_host_invocation_context" }),
|
|
1594
|
+
host_source_refused: Object.freeze({ classification: "host_source_refused", next_action: "answer_with_gap" }),
|
|
1595
|
+
host_credential_refused: Object.freeze({ classification: "host_registration_credential_refused", next_action: "ask_admin" }),
|
|
1596
|
+
gateway_unavailable: Object.freeze({ classification: "host_mint_gateway_unavailable", next_action: "ask_admin" }),
|
|
1597
|
+
gateway_rate_limited: Object.freeze({ classification: "host_mint_gateway_rate_limited", next_action: "answer_with_gap" }),
|
|
1598
|
+
gateway_refused: Object.freeze({ classification: "host_mint_gateway_refused", next_action: "answer_with_gap" }),
|
|
1599
|
+
gateway_response_invalid: Object.freeze({ classification: "host_mint_gateway_response_invalid", next_action: "answer_with_gap" }),
|
|
1600
|
+
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" }),
|
|
1601
|
+
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" }),
|
|
1602
|
+
relay_unavailable: Object.freeze({ classification: "host_relay_unavailable", next_action: "answer_with_gap" }),
|
|
1603
|
+
transient_retryable: Object.freeze({ classification: "host_mint_transient_retryable", next_action: "retry" }),
|
|
1604
|
+
mint_failed: Object.freeze({ classification: "host_mint_failed", next_action: "answer_with_gap" }),
|
|
1605
|
+
relay_failed: Object.freeze({ classification: "host_relay_failed", next_action: "answer_with_gap" }),
|
|
1606
|
+
host_refusal_invalid: Object.freeze({ classification: "host_refusal_invalid", next_action: "answer_with_gap" })
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
// packages/ceal-client-protocol/src/host-service-diagnostic.ts
|
|
1610
|
+
var CEAL_HOST_SERVICE_DIAGNOSTIC_SCHEMA = "ceal.host_service_diagnostic.v1";
|
|
1611
|
+
var CEAL_HOST_SERVICE_DIAGNOSTIC_PRODUCERS = vocabulary(["claude", "codex"]);
|
|
1612
|
+
var CEAL_HOST_SERVICE_DIAGNOSTIC_EVENTS = vocabulary([
|
|
1613
|
+
"hook_invoked",
|
|
1614
|
+
"registration_written",
|
|
1615
|
+
"hook_failed",
|
|
1616
|
+
"watcher_turn_observed",
|
|
1617
|
+
"current_prompt_registered",
|
|
1618
|
+
"current_prompt_registration_failed",
|
|
1619
|
+
"current_prompt_capacity_exhausted",
|
|
1620
|
+
"activation_recorded",
|
|
1621
|
+
"activation_failed",
|
|
1622
|
+
"watcher_scan_failed",
|
|
1623
|
+
"activation_wait_skipped",
|
|
1624
|
+
"activation_wait_engaged",
|
|
1625
|
+
"activation_wait_completed",
|
|
1626
|
+
"activation_wait_exhausted"
|
|
1627
|
+
]);
|
|
1628
|
+
var CEAL_HOST_SERVICE_DIAGNOSTIC_FAILURE_KINDS = vocabulary([
|
|
1629
|
+
"malformed_line",
|
|
1630
|
+
"other",
|
|
1631
|
+
"payload_rejected",
|
|
1632
|
+
"read_error",
|
|
1633
|
+
"size_budget",
|
|
1634
|
+
"symlink"
|
|
1635
|
+
]);
|
|
1470
1636
|
|
|
1471
1637
|
// packages/ceal-client-protocol/src/client-result-materialization.ts
|
|
1472
1638
|
var CEAL_UNREAD_REASONS = vocabulary(["blocked", "unavailable", "too_large", "unsupported", "download_failed", "permission_denied"]);
|
|
1473
|
-
var
|
|
1474
|
-
var
|
|
1475
|
-
var FIELDS = /* @__PURE__ */ new Set([...REQUIRED_FIELDS2, "local_result_root", "continue_argv", "model_disclosure"]);
|
|
1639
|
+
var REQUIRED_FIELDS2 = Object.freeze(["schema_version", "operation_id", "path", "sha256"]);
|
|
1640
|
+
var FIELDS = /* @__PURE__ */ new Set([...REQUIRED_FIELDS2, "items_path", "row_count", "result_complete", "coverage", "local_result_root", "model_disclosure", "continue_argv", "how_to_read"]);
|
|
1476
1641
|
|
|
1477
1642
|
// packages/ceal-client-protocol/src/gateway-cache-origin-validation.ts
|
|
1478
1643
|
var CEAL_MAX_CACHE_ORIGIN_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
@@ -1502,14 +1667,6 @@ var CEAL_GATEWAY_ANNOUNCEMENT_POLICY_NON_CLAIMS = vocabulary([
|
|
|
1502
1667
|
"target_specific_scope_not_declared"
|
|
1503
1668
|
]);
|
|
1504
1669
|
|
|
1505
|
-
// packages/ceal-client-protocol/src/gateway-write-identity.ts
|
|
1506
|
-
var CEAL_GATEWAY_WRITE_IDENTITY_ROLES = Object.freeze(["replay_identity", "lookup_handle", "collision_evidence"]);
|
|
1507
|
-
var CEAL_GATEWAY_WRITE_IDENTITY_FIELDS = Object.freeze({
|
|
1508
|
-
replay_identity: "idempotency_claim_sha256",
|
|
1509
|
-
lookup_handle: "write_request_sha256",
|
|
1510
|
-
collision_evidence: "normalized_mutation_sha256"
|
|
1511
|
-
});
|
|
1512
|
-
|
|
1513
1670
|
// packages/ceal-client-protocol/src/gateway-scoped-identity-projection-validation.ts
|
|
1514
1671
|
var PROJECTION_REVISION_KEYS = Object.freeze(["graph_revision", "subject_key_revision", "projection_revision"]);
|
|
1515
1672
|
|
|
@@ -1522,6 +1679,18 @@ var CEAL_SUPPORTED_GATEWAY_PROTOCOL_RANGE = Object.freeze({
|
|
|
1522
1679
|
// packages/ceal-client-protocol/src/admin-projection-vocabulary.ts
|
|
1523
1680
|
var ADMIN_PROJECTION_STATES = vocabulary(["ready", "empty", "partial", "unknown"]);
|
|
1524
1681
|
var ADMIN_VIEW_KINDS = vocabulary(["request_detail", "resource_access_timeline", "weekly_usage"]);
|
|
1682
|
+
var ADMIN_RECORD_PROJECTION_KINDS = vocabulary([
|
|
1683
|
+
"request_list",
|
|
1684
|
+
"usage_series",
|
|
1685
|
+
"connector_health",
|
|
1686
|
+
"person_connections",
|
|
1687
|
+
"client_sessions",
|
|
1688
|
+
"knowledge_base",
|
|
1689
|
+
"change_history",
|
|
1690
|
+
"approval_queue",
|
|
1691
|
+
"access_group_list",
|
|
1692
|
+
"capability_catalogue"
|
|
1693
|
+
]);
|
|
1525
1694
|
var ADMIN_GAP_KINDS = vocabulary(["unknown", "unavailable", "partial"]);
|
|
1526
1695
|
var ADMIN_UNAVAILABLE_REASONS = vocabulary([
|
|
1527
1696
|
"not_recorded",
|
|
@@ -1545,7 +1714,11 @@ var ADMIN_SOURCE_OWNERS = vocabulary([
|
|
|
1545
1714
|
"person",
|
|
1546
1715
|
"resource",
|
|
1547
1716
|
"prompt",
|
|
1548
|
-
"usage"
|
|
1717
|
+
"usage",
|
|
1718
|
+
"connector",
|
|
1719
|
+
"session",
|
|
1720
|
+
"knowledge",
|
|
1721
|
+
"policy"
|
|
1549
1722
|
]);
|
|
1550
1723
|
var ADMIN_COMPLETENESS_STATES = vocabulary(["complete", "partial", "unknown"]);
|
|
1551
1724
|
var ADMIN_OPERATION_OUTCOMES = vocabulary(["completed", "failed", "denied", "cancelled", "unknown"]);
|
|
@@ -1589,14 +1762,19 @@ var SAFE_REF2 = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
|
1589
1762
|
var HOST_PRODUCER_SERVE_COMMAND_PREFIX = ["serve", "--config"];
|
|
1590
1763
|
var HostProducerServiceError = class extends Error {
|
|
1591
1764
|
code;
|
|
1592
|
-
constructor(
|
|
1593
|
-
super(message ===
|
|
1765
|
+
constructor(code2, message = code2) {
|
|
1766
|
+
super(message === code2 ? code2 : `${code2}: ${message}`);
|
|
1594
1767
|
this.name = "HostProducerServiceError";
|
|
1595
|
-
this.code =
|
|
1768
|
+
this.code = code2;
|
|
1596
1769
|
}
|
|
1597
1770
|
};
|
|
1598
1771
|
var HOST_PRODUCER_CONFIG_KEYS = ["schema_version", "host_source_ref", "producer_installation_ref", "producer_epoch", "outbox_dir", "relay_endpoint"];
|
|
1599
1772
|
var HOST_PRODUCER_TIMING_DEFAULTS = Object.freeze({ reconcile_interval_ms: 250, retry_backoff_ms: 1e3, ipc_timeout_ms: 5e3 });
|
|
1773
|
+
function hostProducerExtraPath(extras, key) {
|
|
1774
|
+
const value = extras[key];
|
|
1775
|
+
if (value === void 0) throw hostProducerError("invalid_config");
|
|
1776
|
+
return value;
|
|
1777
|
+
}
|
|
1600
1778
|
function parseHostProducerServeArgs(args, platform = process.platform) {
|
|
1601
1779
|
return parseProducerServeArgs(args, platform, hostProducerError);
|
|
1602
1780
|
}
|
|
@@ -1623,8 +1801,8 @@ async function serveHostProducer(descriptor, config, signal, dependencies = {})
|
|
|
1623
1801
|
async function runHostProducerCli(descriptor, args, signal, dependencies = {}) {
|
|
1624
1802
|
await serveHostProducer(descriptor, await readHostProducerConfig(descriptor, parseHostProducerServeArgs(args)), signal, dependencies);
|
|
1625
1803
|
}
|
|
1626
|
-
function hostProducerError(
|
|
1627
|
-
return new HostProducerServiceError(
|
|
1804
|
+
function hostProducerError(code2, message = code2) {
|
|
1805
|
+
return new HostProducerServiceError(code2, message);
|
|
1628
1806
|
}
|
|
1629
1807
|
function parseProducerServeArgs(args, platform, error) {
|
|
1630
1808
|
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>"));
|
|
@@ -1775,11 +1953,20 @@ var FRAME_KINDS = new Set(HOST_PRODUCER_FRAME_KINDS);
|
|
|
1775
1953
|
function isHostProducerFrameKind(value) {
|
|
1776
1954
|
return typeof value === "string" && FRAME_KINDS.has(value);
|
|
1777
1955
|
}
|
|
1956
|
+
var HOST_PRODUCER_FRAME_ERROR_CODES = [
|
|
1957
|
+
"invalid_frame_encoding",
|
|
1958
|
+
"invalid_frame_length",
|
|
1959
|
+
"frame_truncated",
|
|
1960
|
+
"unsupported_frame_schema",
|
|
1961
|
+
"invalid_frame_shape",
|
|
1962
|
+
"invalid_producer_identity",
|
|
1963
|
+
"frame_too_large"
|
|
1964
|
+
];
|
|
1778
1965
|
var HostProducerFrameError = class extends Error {
|
|
1779
1966
|
code;
|
|
1780
|
-
constructor(
|
|
1967
|
+
constructor(code2, message = code2) {
|
|
1781
1968
|
super(message);
|
|
1782
|
-
this.code =
|
|
1969
|
+
this.code = code2;
|
|
1783
1970
|
}
|
|
1784
1971
|
};
|
|
1785
1972
|
function encodeHostProducerFrame(frame) {
|
|
@@ -1809,6 +1996,9 @@ function decodeHostProducerFrame(input) {
|
|
|
1809
1996
|
validateFrame(value);
|
|
1810
1997
|
return value;
|
|
1811
1998
|
}
|
|
1999
|
+
function isHostSafeRef(value) {
|
|
2000
|
+
return safeRef(value);
|
|
2001
|
+
}
|
|
1812
2002
|
function validateFrame(value) {
|
|
1813
2003
|
if (!record(value)) throw new HostProducerFrameError("invalid_frame_shape");
|
|
1814
2004
|
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)}`);
|
|
@@ -1896,6 +2086,15 @@ function exactOptional(value, required, optional) {
|
|
|
1896
2086
|
return required.every((key) => keys.includes(key)) && keys.every((key) => required.includes(key) || optional.includes(key));
|
|
1897
2087
|
}
|
|
1898
2088
|
|
|
2089
|
+
// host/relay/src/host-producer-core.ts
|
|
2090
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2091
|
+
import { O_NOFOLLOW, O_RDONLY as O_RDONLY2 } from "node:constants";
|
|
2092
|
+
import { mkdir as mkdir2, open as open4, readFile, rename as rename2, writeFile } from "node:fs/promises";
|
|
2093
|
+
import { join as join2 } from "node:path";
|
|
2094
|
+
|
|
2095
|
+
// host/relay/src/relay-ipc.ts
|
|
2096
|
+
import { createConnection, createServer } from "node:net";
|
|
2097
|
+
|
|
1899
2098
|
// host/relay/src/durable-atomic-replacement.ts
|
|
1900
2099
|
import { O_RDONLY } from "node:constants";
|
|
1901
2100
|
import { lstat as lstat2, mkdir, open as open2, rename, unlink } from "node:fs/promises";
|
|
@@ -1984,20 +2183,67 @@ function errorCode(error) {
|
|
|
1984
2183
|
// host/relay/src/relay-request-binding-store.ts
|
|
1985
2184
|
var MAX_STATE_BYTES = 8 * 1024 * 1024;
|
|
1986
2185
|
|
|
1987
|
-
// host/relay/src/relay-quarantine-store.ts
|
|
1988
|
-
var RELAY_QUARANTINE_MAX_BYTES = 16 * 1024 * 1024;
|
|
1989
|
-
|
|
1990
|
-
// host/relay/src/relay-admission-store.ts
|
|
1991
|
-
var DEFAULT_LIMITS = { global_count: 8192, global_bytes: 64 * 1024 * 1024, producer_count: 4096, producer_bytes: 32 * 1024 * 1024 };
|
|
1992
|
-
var RETRY_SECONDS = [1, 2, 4, 8, 16, 32, 60];
|
|
1993
|
-
var MAX_RETRYABLE_ATTEMPTS = RETRY_SECONDS.length + 10;
|
|
1994
|
-
|
|
1995
|
-
// host/relay/src/relay-ipc.ts
|
|
1996
|
-
import { createConnection, createServer } from "node:net";
|
|
1997
|
-
|
|
1998
2186
|
// host/relay/src/current-prompt-custody.ts
|
|
1999
2187
|
var HOST_CURRENT_PROMPT_MAX_BYTES = 256 * 1024;
|
|
2000
2188
|
|
|
2189
|
+
// host/service-log.ts
|
|
2190
|
+
import { copyFile, open as open3, stat, truncate } from "node:fs/promises";
|
|
2191
|
+
import { isAbsolute } from "node:path";
|
|
2192
|
+
var HOST_SERVICE_LOG_ENVIRONMENT = "CEAL_HOST_SERVICE_LOG";
|
|
2193
|
+
function writeHostServiceDiagnostic(producer_id, event, failure_kind, cause, sink) {
|
|
2194
|
+
const line = `${JSON.stringify({ schema_version: CEAL_HOST_SERVICE_DIAGNOSTIC_SCHEMA, producer_id, event, ...event === "watcher_scan_failed" && failure_kind !== void 0 ? { failure_kind } : {}, ...event === "watcher_scan_failed" && cause !== void 0 ? { cause } : {} })}
|
|
2195
|
+
`;
|
|
2196
|
+
try {
|
|
2197
|
+
(sink ?? defaultHostServiceDiagnosticSink)(line);
|
|
2198
|
+
} catch {
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
function defaultHostServiceDiagnosticSink(line) {
|
|
2202
|
+
process.stderr.write(line);
|
|
2203
|
+
}
|
|
2204
|
+
var HOST_SERVICE_LOG_MAX_BYTES = 1024 * 1024;
|
|
2205
|
+
var HOST_SERVICE_LOG_CHECK_INTERVAL_MS = 6e4;
|
|
2206
|
+
function hostServiceLogPreviousPath(path) {
|
|
2207
|
+
return `${path}.1`;
|
|
2208
|
+
}
|
|
2209
|
+
async function rotateHostServiceLog(path, maxBytes = HOST_SERVICE_LOG_MAX_BYTES) {
|
|
2210
|
+
let size;
|
|
2211
|
+
try {
|
|
2212
|
+
size = (await stat(path)).size;
|
|
2213
|
+
} catch (error) {
|
|
2214
|
+
if (isNotFound(error)) return false;
|
|
2215
|
+
throw error;
|
|
2216
|
+
}
|
|
2217
|
+
if (size <= maxBytes) return false;
|
|
2218
|
+
await copyFile(path, hostServiceLogPreviousPath(path));
|
|
2219
|
+
await truncate(path, 0);
|
|
2220
|
+
return true;
|
|
2221
|
+
}
|
|
2222
|
+
function boundHostServiceLog(environment = process.env) {
|
|
2223
|
+
const path = environment[HOST_SERVICE_LOG_ENVIRONMENT];
|
|
2224
|
+
if (path === void 0 || !isAbsolute(path)) return () => void 0;
|
|
2225
|
+
let reported = false;
|
|
2226
|
+
const check = () => {
|
|
2227
|
+
rotateHostServiceLog(path).catch((error) => {
|
|
2228
|
+
if (reported) return;
|
|
2229
|
+
reported = true;
|
|
2230
|
+
process.stderr.write(`[host-service-log] rotation_failed code=${errorCode2(error)}
|
|
2231
|
+
`);
|
|
2232
|
+
});
|
|
2233
|
+
};
|
|
2234
|
+
check();
|
|
2235
|
+
const timer = setInterval(check, HOST_SERVICE_LOG_CHECK_INTERVAL_MS);
|
|
2236
|
+
timer.unref();
|
|
2237
|
+
return () => clearInterval(timer);
|
|
2238
|
+
}
|
|
2239
|
+
var HOST_SERVICE_LOG_TAIL_BYTES = 64 * 1024;
|
|
2240
|
+
function isNotFound(error) {
|
|
2241
|
+
return errorCode2(error) === "ENOENT";
|
|
2242
|
+
}
|
|
2243
|
+
function errorCode2(error) {
|
|
2244
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "unknown";
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2001
2247
|
// host/relay/src/relay-ipc.ts
|
|
2002
2248
|
var HOST_RELAY_REQUEST_BINDING_KEYS = Object.freeze([
|
|
2003
2249
|
"schema_version",
|
|
@@ -2015,6 +2261,9 @@ var HOST_RELAY_REQUEST_BINDING_KEYS = Object.freeze([
|
|
|
2015
2261
|
"host_input_ref"
|
|
2016
2262
|
]);
|
|
2017
2263
|
var HOST_RELAY_IPC_RESPONSE_SCHEMA = "ceal.host_relay_admission.v1";
|
|
2264
|
+
var HOST_RELAY_CURRENT_PROMPT_REGISTER_SCHEMA = "ceal.host_relay_current_prompt_register.v1";
|
|
2265
|
+
var HOST_RELAY_CURRENT_PROMPT_RELEASE_SCHEMA = "ceal.host_relay_current_prompt_release.v1";
|
|
2266
|
+
var HOST_RELAY_CURRENT_PROMPT_RESULT_SCHEMA = "ceal.host_relay_current_prompt_result.v1";
|
|
2018
2267
|
var HOST_RELAY_IPC_LENGTH_BYTES = Uint32Array.BYTES_PER_ELEMENT;
|
|
2019
2268
|
var HOST_RELAY_ADMISSION_RESPONSE_MAX_BYTES = 4096;
|
|
2020
2269
|
var HOST_RELAY_INVOCATION_MINT_REFUSAL_REASONS = vocabulary([
|
|
@@ -2025,6 +2274,8 @@ var HOST_RELAY_INVOCATION_MINT_REFUSAL_REASONS = vocabulary([
|
|
|
2025
2274
|
"gateway_unavailable",
|
|
2026
2275
|
"gateway_refused",
|
|
2027
2276
|
"gateway_response_invalid",
|
|
2277
|
+
"gateway_rate_limited",
|
|
2278
|
+
"producer_frame_invalid",
|
|
2028
2279
|
"deadline",
|
|
2029
2280
|
"cancelled",
|
|
2030
2281
|
"mint_failed",
|
|
@@ -2038,6 +2289,17 @@ var HOST_RELAY_INVOCATION_MINT_REFUSAL_REASONS = vocabulary([
|
|
|
2038
2289
|
"host_input_mismatch",
|
|
2039
2290
|
"host_prompt_mismatch"
|
|
2040
2291
|
]);
|
|
2292
|
+
var HOST_RELAY_DIRECT_WIRE_MINT_REASONS = vocabulary([
|
|
2293
|
+
"context_stale",
|
|
2294
|
+
"context_expired",
|
|
2295
|
+
"host_source_refused",
|
|
2296
|
+
"host_credential_refused",
|
|
2297
|
+
"gateway_unavailable",
|
|
2298
|
+
"gateway_rate_limited",
|
|
2299
|
+
"gateway_refused",
|
|
2300
|
+
"gateway_response_invalid",
|
|
2301
|
+
"producer_frame_invalid"
|
|
2302
|
+
]);
|
|
2041
2303
|
var HOST_RELAY_INVOCATION_MINT_OUTCOMES = vocabulary(["completed", "caller_cancelled", "deadline", "failed", "no_binding"]);
|
|
2042
2304
|
var HOST_RELAY_EXCHANGE_FAILURES = vocabulary([
|
|
2043
2305
|
"permission_denied",
|
|
@@ -2053,14 +2315,23 @@ var HostRelayIpcError = class extends Error {
|
|
|
2053
2315
|
code;
|
|
2054
2316
|
/** Set only when the client-side socket exchange itself failed. */
|
|
2055
2317
|
failure;
|
|
2056
|
-
constructor(
|
|
2318
|
+
constructor(code2, message = code2, failure) {
|
|
2057
2319
|
super(message);
|
|
2058
|
-
this.code =
|
|
2320
|
+
this.code = code2;
|
|
2059
2321
|
this.failure = failure;
|
|
2060
2322
|
}
|
|
2061
2323
|
};
|
|
2324
|
+
var HostRelayCurrentPromptCapacityRefusal = class extends Error {
|
|
2325
|
+
constructor() {
|
|
2326
|
+
super("current_prompt_capacity_exhausted");
|
|
2327
|
+
this.name = "HostRelayCurrentPromptCapacityRefusal";
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
function hostRelayExchangeFailure(error) {
|
|
2331
|
+
return error instanceof HostRelayIpcError ? error.failure : void 0;
|
|
2332
|
+
}
|
|
2062
2333
|
function socketErrorFailure(error) {
|
|
2063
|
-
switch (
|
|
2334
|
+
switch (errorCode3(error)) {
|
|
2064
2335
|
case "EACCES":
|
|
2065
2336
|
case "EPERM":
|
|
2066
2337
|
return "permission_denied";
|
|
@@ -2085,6 +2356,25 @@ var HostRelayIpcAdmissionPort = class {
|
|
|
2085
2356
|
return { code: response2.code, admitted_through_sequence: response2.admitted_through_sequence, delivered_through_sequence: response2.delivered_through_sequence };
|
|
2086
2357
|
}
|
|
2087
2358
|
};
|
|
2359
|
+
var HostRelayCurrentPromptPort = class {
|
|
2360
|
+
endpoint;
|
|
2361
|
+
timeout_ms;
|
|
2362
|
+
constructor(endpoint, timeoutMs = 5e3) {
|
|
2363
|
+
this.endpoint = endpoint;
|
|
2364
|
+
this.timeout_ms = timeoutMs;
|
|
2365
|
+
}
|
|
2366
|
+
async register(input) {
|
|
2367
|
+
const request2 = { schema_version: HOST_RELAY_CURRENT_PROMPT_REGISTER_SCHEMA, ...input };
|
|
2368
|
+
const response2 = await exchange(this.endpoint, encodeLengthPrefixed(request2), this.timeout_ms, decodeCurrentPromptResponseBuffer);
|
|
2369
|
+
if (response2.status === "capacity_exhausted") throw new HostRelayCurrentPromptCapacityRefusal();
|
|
2370
|
+
if (response2.status !== "registered") throw new HostRelayIpcError("relay_unavailable", response2.status);
|
|
2371
|
+
}
|
|
2372
|
+
async release(identity) {
|
|
2373
|
+
const request2 = { schema_version: HOST_RELAY_CURRENT_PROMPT_RELEASE_SCHEMA, ...identity };
|
|
2374
|
+
const response2 = await exchange(this.endpoint, encodeLengthPrefixed(request2), this.timeout_ms, decodeCurrentPromptResponseBuffer);
|
|
2375
|
+
if (response2.status !== "released") throw new HostRelayIpcError("relay_unavailable", response2.status, "response_invalid");
|
|
2376
|
+
}
|
|
2377
|
+
};
|
|
2088
2378
|
function submitHostProducerFrame(endpoint, frame, timeoutMs = 5e3) {
|
|
2089
2379
|
return exchange(endpoint, encodeHostProducerFrame(frame), timeoutMs, decodeResponseBuffer);
|
|
2090
2380
|
}
|
|
@@ -2163,142 +2453,604 @@ function decodeResponseBuffer(buffer) {
|
|
|
2163
2453
|
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}` }])}.`);
|
|
2164
2454
|
return decodeHostRelayIpcResponse(buffer);
|
|
2165
2455
|
}
|
|
2166
|
-
function
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
import { mkdir as mkdir3, readFile as readFile2, rename as rename2, writeFile } from "node:fs/promises";
|
|
2172
|
-
import { join as join3 } from "node:path";
|
|
2173
|
-
|
|
2174
|
-
// host/claude/src/claude-outbox.ts
|
|
2175
|
-
import { open as open3, mkdir as mkdir2, readdir, readFile, unlink as unlink2 } from "node:fs/promises";
|
|
2176
|
-
import { join as join2 } from "node:path";
|
|
2177
|
-
|
|
2178
|
-
// host/claude/src/claude-types.ts
|
|
2179
|
-
var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "Stop", "StopFailure"];
|
|
2180
|
-
var CLAUDE_HOST_ERROR_CODES = ["invalid_input", "storage", "configuration"];
|
|
2181
|
-
function isClaudeHostErrorCode(value) {
|
|
2182
|
-
return typeof value === "string" && CLAUDE_HOST_ERROR_CODES.some((code3) => code3 === value);
|
|
2456
|
+
function decodeCurrentPromptResponseBuffer(buffer) {
|
|
2457
|
+
const value = decodeLengthPrefixedJson(buffer, HOST_RELAY_ADMISSION_RESPONSE_MAX_BYTES);
|
|
2458
|
+
if (value === null) return null;
|
|
2459
|
+
if (!record2(value) || !exactKeys2(value, ["schema_version", "status"]) || value.schema_version !== HOST_RELAY_CURRENT_PROMPT_RESULT_SCHEMA || value.status !== "registered" && value.status !== "capacity_exhausted" && value.status !== "current_prompt_unavailable" && value.status !== "released") throw new HostRelayIpcError("relay_unavailable", "Host Relay current-prompt response is invalid.", "response_invalid");
|
|
2460
|
+
return { schema_version: HOST_RELAY_CURRENT_PROMPT_RESULT_SCHEMA, status: value.status };
|
|
2183
2461
|
}
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2462
|
+
function decodeLengthPrefixedJson(input, maximum) {
|
|
2463
|
+
if (input.length < HOST_RELAY_IPC_LENGTH_BYTES) return null;
|
|
2464
|
+
const length = input.readUInt32BE(0);
|
|
2465
|
+
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}`)}.`);
|
|
2466
|
+
if (input.length < length + HOST_RELAY_IPC_LENGTH_BYTES) return null;
|
|
2467
|
+
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}` }])}.`);
|
|
2468
|
+
try {
|
|
2469
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(input.subarray(HOST_RELAY_IPC_LENGTH_BYTES)));
|
|
2470
|
+
} catch {
|
|
2471
|
+
throw new HostRelayIpcError("relay_unavailable", "Host Relay IPC message is invalid.");
|
|
2193
2472
|
}
|
|
2194
|
-
};
|
|
2195
|
-
function isClaudeHookEvent(value) {
|
|
2196
|
-
return typeof value === "string" && CLAUDE_HOOK_EVENTS.some((event) => event === value);
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
// host/invocation-context.ts
|
|
2200
|
-
var HOST_INVOCATION_IDENTITY_KEYS = ["host_session_ref", "host_turn_ref", "host_source_ref"];
|
|
2201
|
-
var HOST_INVOCATION_CONTEXT_REQUIRED_KEYS = ["schema_version", "identity", "prompt_ref", "expires_at"];
|
|
2202
|
-
function invocationContextKey(identity) {
|
|
2203
|
-
return cealRequestSha256({ host_source_ref: identity.host_source_ref, host_session_ref: identity.host_session_ref });
|
|
2204
|
-
}
|
|
2205
|
-
function hasHostInvocationIdentityKeys(value) {
|
|
2206
|
-
return exactKeys2(value, HOST_INVOCATION_IDENTITY_KEYS);
|
|
2207
2473
|
}
|
|
2208
|
-
function
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
identity,
|
|
2215
|
-
prompt_ref: promptRef,
|
|
2216
|
-
expires_at: expiresAt,
|
|
2217
|
-
...descriptor.working_directory === void 0 ? {} : { working_directory: descriptor.working_directory }
|
|
2218
|
-
};
|
|
2219
|
-
descriptor.validate(context);
|
|
2220
|
-
return Object.freeze(context);
|
|
2474
|
+
function encodeLengthPrefixed(value) {
|
|
2475
|
+
const body = Buffer.from(JSON.stringify(value), "utf8");
|
|
2476
|
+
const output = Buffer.allocUnsafe(body.length + HOST_RELAY_IPC_LENGTH_BYTES);
|
|
2477
|
+
output.writeUInt32BE(body.length, 0);
|
|
2478
|
+
body.copy(output, HOST_RELAY_IPC_LENGTH_BYTES);
|
|
2479
|
+
return output;
|
|
2221
2480
|
}
|
|
2222
|
-
function exactKeys2(value,
|
|
2223
|
-
const
|
|
2224
|
-
return
|
|
2481
|
+
function exactKeys2(value, required, optional = []) {
|
|
2482
|
+
const keys = Object.keys(value);
|
|
2483
|
+
return required.every((key) => Object.hasOwn(value, key)) && keys.every((key) => required.includes(key) || optional.includes(key));
|
|
2225
2484
|
}
|
|
2226
|
-
function
|
|
2227
|
-
|
|
2228
|
-
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
2229
|
-
return actual.length >= required.length && actual.length <= allowed.size && required.every((key) => Object.hasOwn(value, key)) && actual.every((key) => allowed.has(key));
|
|
2485
|
+
function errorCode3(error) {
|
|
2486
|
+
return record2(error) && typeof error.code === "string" ? error.code : null;
|
|
2230
2487
|
}
|
|
2231
2488
|
|
|
2232
|
-
// host/
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
function validateClaudeHostInvocationContext(value) {
|
|
2237
|
-
if (!contextShape(value)) throw new ClaudeHostError("storage", "Claude Host invocation context is malformed.");
|
|
2238
|
-
validateIdentity2(value.identity);
|
|
2239
|
-
validateContextReferences(value);
|
|
2240
|
-
validateContextTimes(value);
|
|
2489
|
+
// host/relay/src/host-producer-core.ts
|
|
2490
|
+
var HOST_TRANSCRIPT_CURSOR_REASONS = vocabulary(["transcript_replaced", "transcript_truncated", "transcript_rewritten", "transcript_malformed"]);
|
|
2491
|
+
function isHostTranscriptCursorReason(value) {
|
|
2492
|
+
return typeof value === "string" && HOST_TRANSCRIPT_CURSOR_REASONS.some((reason) => reason === value);
|
|
2241
2493
|
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2494
|
+
var TRANSCRIPT_MISSING_REASON = "transcript_missing";
|
|
2495
|
+
var TRANSCRIPT_SCAN_READ_ERROR_ERRNOS = ["EBADF", "EACCES", "EIO", "EMFILE", "ENFILE", "ENOENT", "ENOTDIR", "EPERM"];
|
|
2496
|
+
function transcriptScanFailureKind(error, adapterFailureKind) {
|
|
2497
|
+
if (transcriptErrorCode(error) === "ELOOP") return "symlink";
|
|
2498
|
+
const adapter = adapterFailureKind(error);
|
|
2499
|
+
if (adapter !== void 0) return adapter;
|
|
2500
|
+
const code2 = transcriptErrorCode(error);
|
|
2501
|
+
if (code2 === "EFBIG") return "size_budget";
|
|
2502
|
+
if (code2 !== null && TRANSCRIPT_SCAN_READ_ERROR_ERRNOS.includes(code2)) return "read_error";
|
|
2503
|
+
return "other";
|
|
2244
2504
|
}
|
|
2245
|
-
function
|
|
2246
|
-
|
|
2505
|
+
async function registerHostCurrentPromptDiagnosed(register, diagnostic) {
|
|
2506
|
+
try {
|
|
2507
|
+
await register();
|
|
2508
|
+
diagnostic("current_prompt_registered");
|
|
2509
|
+
return true;
|
|
2510
|
+
} catch (error) {
|
|
2511
|
+
if (!(error instanceof HostRelayCurrentPromptCapacityRefusal)) diagnostic("current_prompt_registration_failed");
|
|
2512
|
+
throw error;
|
|
2513
|
+
}
|
|
2247
2514
|
}
|
|
2248
|
-
function
|
|
2249
|
-
|
|
2515
|
+
function transcriptErrorCode(error) {
|
|
2516
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
2517
|
+
}
|
|
2518
|
+
var HOST_TRANSCRIPT_READ_CHUNK_BYTES = 256 * 1024;
|
|
2519
|
+
async function readTranscriptCompleteChunk(handle, offset, size) {
|
|
2520
|
+
const available = Math.max(0, size - offset);
|
|
2521
|
+
if (available === 0) return "empty";
|
|
2522
|
+
const chunks = [];
|
|
2523
|
+
let position = offset;
|
|
2524
|
+
let total = 0;
|
|
2525
|
+
let lastNewline = -1;
|
|
2526
|
+
while (position < size) {
|
|
2527
|
+
const buffer = Buffer.alloc(Math.min(HOST_TRANSCRIPT_READ_CHUNK_BYTES, size - position));
|
|
2528
|
+
const read = await handle.read(buffer, 0, buffer.length, position);
|
|
2529
|
+
if (read.bytesRead === 0) break;
|
|
2530
|
+
const chunk = buffer.subarray(0, read.bytesRead);
|
|
2531
|
+
chunks.push(chunk);
|
|
2532
|
+
const newline = chunk.lastIndexOf(10);
|
|
2533
|
+
if (newline >= 0) lastNewline = total + newline;
|
|
2534
|
+
total += chunk.length;
|
|
2535
|
+
position += chunk.length;
|
|
2536
|
+
if (lastNewline >= 0) break;
|
|
2537
|
+
}
|
|
2538
|
+
if (lastNewline < 0) return "partial";
|
|
2539
|
+
return Buffer.concat(chunks, total).subarray(0, lastNewline + 1);
|
|
2540
|
+
}
|
|
2541
|
+
async function readTranscriptAnchor(handle, offset, anchorBytes) {
|
|
2542
|
+
const start = Math.max(0, offset - anchorBytes);
|
|
2543
|
+
const anchor = Buffer.alloc(offset - start);
|
|
2544
|
+
if (anchor.length > 0) await handle.read(anchor, 0, anchor.length, start);
|
|
2545
|
+
return { start, sha256: transcriptSha256(anchor) };
|
|
2546
|
+
}
|
|
2547
|
+
async function transcriptAnchorMatches(handle, cursor) {
|
|
2548
|
+
const buffer = Buffer.alloc(cursor.offset - cursor.anchor_start);
|
|
2549
|
+
if (buffer.length > 0) await handle.read(buffer, 0, buffer.length, cursor.anchor_start);
|
|
2550
|
+
return transcriptSha256(buffer) === cursor.anchor_sha256;
|
|
2551
|
+
}
|
|
2552
|
+
function transcriptCursorIdentityReason(cursor, transcriptPath, device, inode) {
|
|
2553
|
+
if (cursor.device !== device || cursor.inode !== inode || cursor.transcript_path !== transcriptPath) return "transcript_replaced";
|
|
2554
|
+
return null;
|
|
2250
2555
|
}
|
|
2251
|
-
function
|
|
2252
|
-
if (
|
|
2253
|
-
if (
|
|
2556
|
+
async function transcriptCursorBoundsReason(handle, cursor, size) {
|
|
2557
|
+
if (size < cursor.offset) return "transcript_truncated";
|
|
2558
|
+
if (cursor.offset > 0 && !await transcriptAnchorMatches(handle, cursor)) return "transcript_rewritten";
|
|
2559
|
+
return null;
|
|
2254
2560
|
}
|
|
2255
|
-
function
|
|
2256
|
-
return
|
|
2561
|
+
function transcriptSha256(value) {
|
|
2562
|
+
return createHash3("sha256").update(value).digest("hex");
|
|
2257
2563
|
}
|
|
2258
|
-
function
|
|
2259
|
-
return value
|
|
2564
|
+
function transcriptObjectSha256(value) {
|
|
2565
|
+
return createHash3("sha256").update(JSON.stringify(value), "utf8").digest("hex");
|
|
2260
2566
|
}
|
|
2261
|
-
function
|
|
2262
|
-
return value ===
|
|
2567
|
+
function isTranscriptRecord(value) {
|
|
2568
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2263
2569
|
}
|
|
2264
|
-
function
|
|
2265
|
-
return
|
|
2570
|
+
function isMissingTranscriptError(error) {
|
|
2571
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2266
2572
|
}
|
|
2267
|
-
function
|
|
2268
|
-
|
|
2573
|
+
function transcriptTimestamp(now, fail) {
|
|
2574
|
+
const value = now();
|
|
2575
|
+
if (!Number.isFinite(value.getTime())) throw fail();
|
|
2576
|
+
return value.toISOString();
|
|
2269
2577
|
}
|
|
2270
|
-
function
|
|
2271
|
-
return
|
|
2578
|
+
function transcriptExpiration(now, retentionMs, fail) {
|
|
2579
|
+
return new Date(Date.parse(transcriptTimestamp(now, fail)) + retentionMs).toISOString();
|
|
2272
2580
|
}
|
|
2273
|
-
function
|
|
2274
|
-
|
|
2581
|
+
function createTranscriptWatcherBase(options, owner) {
|
|
2582
|
+
if (typeof options.outbox_dir !== "string" || options.outbox_dir.length === 0) throw owner.invalid_outbox();
|
|
2583
|
+
return {
|
|
2584
|
+
outboxRetentionMs: options.retention_ms ?? owner.default_retention_ms,
|
|
2585
|
+
now: options.now ?? (() => /* @__PURE__ */ new Date()),
|
|
2586
|
+
log: options.log ?? ((line) => {
|
|
2587
|
+
process.stderr.write(`${line}
|
|
2588
|
+
`);
|
|
2589
|
+
}),
|
|
2590
|
+
producerInstallationRef: options.producer_installation_ref
|
|
2591
|
+
};
|
|
2275
2592
|
}
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
if (
|
|
2285
|
-
this.directory = directory;
|
|
2286
|
-
this.entries = join2(directory, "entries");
|
|
2287
|
-
this.activeContexts = join2(directory, "active-contexts");
|
|
2593
|
+
async function scanTranscriptRegistrations(registrations, scanOne) {
|
|
2594
|
+
let records = 0;
|
|
2595
|
+
let enqueued = 0;
|
|
2596
|
+
let quarantined = 0;
|
|
2597
|
+
for (const registration of registrations) {
|
|
2598
|
+
const result = await scanOne(registration);
|
|
2599
|
+
records += result.records;
|
|
2600
|
+
enqueued += result.enqueued;
|
|
2601
|
+
if (result.quarantined) quarantined += 1;
|
|
2288
2602
|
}
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2603
|
+
return { registrations: registrations.length, records, enqueued, quarantined };
|
|
2604
|
+
}
|
|
2605
|
+
function formatRetiredTranscriptLog(producerLabel, transcriptPath) {
|
|
2606
|
+
return `${producerLabel} retired a transcript registration: transcript_path=${JSON.stringify(transcriptPath)}, reason=${TRANSCRIPT_MISSING_REASON}.`;
|
|
2607
|
+
}
|
|
2608
|
+
var HostTranscriptWatcher = class {
|
|
2609
|
+
outboxRetentionMs;
|
|
2610
|
+
now;
|
|
2611
|
+
log;
|
|
2612
|
+
producerInstallationRef;
|
|
2613
|
+
clockFailure;
|
|
2614
|
+
constructor(options, owner) {
|
|
2615
|
+
const base = createTranscriptWatcherBase(options, owner);
|
|
2616
|
+
this.outboxRetentionMs = base.outboxRetentionMs;
|
|
2617
|
+
this.now = base.now;
|
|
2618
|
+
this.log = base.log;
|
|
2619
|
+
this.producerInstallationRef = base.producerInstallationRef;
|
|
2620
|
+
this.clockFailure = owner.clock_failure;
|
|
2292
2621
|
}
|
|
2293
|
-
async
|
|
2294
|
-
|
|
2295
|
-
await this.prepare();
|
|
2296
|
-
await durableAtomicReplace(this.activeContextPath(identity), JSON.stringify(context));
|
|
2622
|
+
async scanOnce() {
|
|
2623
|
+
return scanTranscriptRegistrations(await this.listRegistrations(), (registration) => this.scanRegistration(registration));
|
|
2297
2624
|
}
|
|
2298
|
-
async
|
|
2625
|
+
async scanRegistration(registration) {
|
|
2626
|
+
try {
|
|
2627
|
+
return await this.openTranscript(registration, (handle) => this.scanOpened(handle, registration), {
|
|
2628
|
+
missing: () => this.retireMissing(registration),
|
|
2629
|
+
failed: (handle, error) => this.scanFailed(handle, registration, error)
|
|
2630
|
+
});
|
|
2631
|
+
} catch (error) {
|
|
2632
|
+
if (error instanceof HostRelayCurrentPromptCapacityRefusal) return { records: 0, enqueued: 0, quarantined: false };
|
|
2633
|
+
throw error;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
async openTranscript(registration, scan, handlers) {
|
|
2637
|
+
let handle;
|
|
2638
|
+
try {
|
|
2639
|
+
handle = await open4(this.transcriptPathOf(registration), O_RDONLY2 | O_NOFOLLOW);
|
|
2640
|
+
return await scan(handle);
|
|
2641
|
+
} catch (error) {
|
|
2642
|
+
if (handle === void 0 && isMissingTranscriptError(error)) return handlers.missing();
|
|
2643
|
+
if (hostRelayExchangeFailure(error) !== void 0 || error instanceof HostRelayCurrentPromptCapacityRefusal) throw error;
|
|
2644
|
+
return handlers.failed(handle, error);
|
|
2645
|
+
} finally {
|
|
2646
|
+
await handle?.close();
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
/**
|
|
2650
|
+
* A transcript the Host deleted never comes back, so retrying its
|
|
2651
|
+
* registration once per reconcile interval is a failure with no end.
|
|
2652
|
+
* Retire it at the scan that finds it missing and say so once. Only
|
|
2653
|
+
* ENOENT means gone; a permission or busy failure is about this moment,
|
|
2654
|
+
* not about the file, and keeps the registration for the next scan.
|
|
2655
|
+
*/
|
|
2656
|
+
async retireMissingTranscript(transcriptPath, producerLabel, retire) {
|
|
2657
|
+
await retire();
|
|
2658
|
+
this.log(formatRetiredTranscriptLog(producerLabel, transcriptPath));
|
|
2659
|
+
return { records: 0, enqueued: 0, quarantined: false };
|
|
2660
|
+
}
|
|
2661
|
+
stamp() {
|
|
2662
|
+
return transcriptTimestamp(this.now, this.clockFailure);
|
|
2663
|
+
}
|
|
2664
|
+
expire(retentionMs) {
|
|
2665
|
+
return transcriptExpiration(this.now, retentionMs, this.clockFailure);
|
|
2666
|
+
}
|
|
2667
|
+
sha(value) {
|
|
2668
|
+
return transcriptSha256(value);
|
|
2669
|
+
}
|
|
2670
|
+
objectSha(value) {
|
|
2671
|
+
return transcriptObjectSha256(value);
|
|
2672
|
+
}
|
|
2673
|
+
isRecord(value) {
|
|
2674
|
+
return isTranscriptRecord(value);
|
|
2675
|
+
}
|
|
2676
|
+
readChunk(handle, offset, size) {
|
|
2677
|
+
return readTranscriptCompleteChunk(handle, offset, size);
|
|
2678
|
+
}
|
|
2679
|
+
readAnchor(handle, offset, anchorBytes) {
|
|
2680
|
+
return readTranscriptAnchor(handle, offset, anchorBytes);
|
|
2681
|
+
}
|
|
2682
|
+
cursorIdentityReason(cursor, transcriptPath, device, inode) {
|
|
2683
|
+
return transcriptCursorIdentityReason(cursor, transcriptPath, device, inode);
|
|
2684
|
+
}
|
|
2685
|
+
cursorBoundsReason(handle, cursor, size) {
|
|
2686
|
+
return transcriptCursorBoundsReason(handle, cursor, size);
|
|
2687
|
+
}
|
|
2688
|
+
};
|
|
2689
|
+
var HOST_PRODUCER_STATE_IDENTITY = {
|
|
2690
|
+
codex: { file_name: "codex-producer-state.json", schema: "ceal.codex_producer_state.v1" },
|
|
2691
|
+
claude: { file_name: "claude-producer-state.json", schema: "ceal.claude_producer_state.v1" }
|
|
2692
|
+
};
|
|
2693
|
+
var HostProducerDrainCore = class {
|
|
2694
|
+
host;
|
|
2695
|
+
relay;
|
|
2696
|
+
statePath;
|
|
2697
|
+
stateSchema;
|
|
2698
|
+
installationRef;
|
|
2699
|
+
epoch;
|
|
2700
|
+
now;
|
|
2701
|
+
tail = Promise.resolve();
|
|
2702
|
+
constructor(host, runtime) {
|
|
2703
|
+
if (!isHostSafeRef(runtime.producer_installation_ref)) throw host.fail("configuration", `${host.producer_label} producer installation identity is invalid.`);
|
|
2704
|
+
const epoch = runtime.producer_epoch ?? 1;
|
|
2705
|
+
if (!Number.isSafeInteger(epoch) || epoch <= 0) throw host.fail("configuration", `${host.producer_label} producer epoch is invalid.`);
|
|
2706
|
+
this.host = host;
|
|
2707
|
+
this.relay = runtime.relay;
|
|
2708
|
+
this.statePath = join2(host.outbox_directory, HOST_PRODUCER_STATE_IDENTITY[host.producer_id].file_name);
|
|
2709
|
+
this.stateSchema = HOST_PRODUCER_STATE_IDENTITY[host.producer_id].schema;
|
|
2710
|
+
this.installationRef = runtime.producer_installation_ref;
|
|
2711
|
+
this.epoch = epoch;
|
|
2712
|
+
this.now = runtime.now ?? (() => /* @__PURE__ */ new Date());
|
|
2713
|
+
}
|
|
2714
|
+
drainOutbox() {
|
|
2715
|
+
const run = this.tail.then(() => this.drainLocked());
|
|
2716
|
+
this.tail = run.then(() => void 0, () => void 0);
|
|
2717
|
+
return run;
|
|
2718
|
+
}
|
|
2719
|
+
async drainLocked() {
|
|
2720
|
+
const read = await this.host.listEnvelopes();
|
|
2721
|
+
let admitted = 0;
|
|
2722
|
+
let expired = 0;
|
|
2723
|
+
let retained = 0;
|
|
2724
|
+
for (const envelope of read.entries) {
|
|
2725
|
+
const reserved = await this.hasReservation(this.host.submissionRefOf(envelope));
|
|
2726
|
+
if (!reserved && Date.parse(this.host.expiresAtOf(envelope)) <= this.now().getTime()) {
|
|
2727
|
+
await this.host.removeEnvelope(envelope);
|
|
2728
|
+
expired += 1;
|
|
2729
|
+
continue;
|
|
2730
|
+
}
|
|
2731
|
+
const frames = await this.reserve(envelope);
|
|
2732
|
+
let complete = true;
|
|
2733
|
+
for (const frame of frames) {
|
|
2734
|
+
const outcome = await this.relay.admit(frame);
|
|
2735
|
+
if (outcome.code !== "admitted" && outcome.code !== "already_admitted") {
|
|
2736
|
+
complete = false;
|
|
2737
|
+
break;
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
if (!complete) {
|
|
2741
|
+
retained += 1;
|
|
2742
|
+
break;
|
|
2743
|
+
}
|
|
2744
|
+
await this.host.removeEnvelope(envelope);
|
|
2745
|
+
await this.release(this.host.submissionRefOf(envelope));
|
|
2746
|
+
admitted += 1;
|
|
2747
|
+
}
|
|
2748
|
+
return { admitted, retained: retained + Math.max(0, read.entries.length - admitted - expired - retained), expired, malformed: read.malformed };
|
|
2749
|
+
}
|
|
2750
|
+
async hasReservation(submissionRef2) {
|
|
2751
|
+
return (await this.readState()).pending[submissionRef2] !== void 0;
|
|
2752
|
+
}
|
|
2753
|
+
async reserve(envelope) {
|
|
2754
|
+
const state = await this.readState();
|
|
2755
|
+
const submissionRef2 = this.host.submissionRefOf(envelope);
|
|
2756
|
+
const existing = state.pending[submissionRef2];
|
|
2757
|
+
if (existing !== void 0) return existing;
|
|
2758
|
+
const drafts = await this.host.mapEnvelope(envelope);
|
|
2759
|
+
const frames = drafts.map((draft, offset) => ({
|
|
2760
|
+
schema_version: "ceal.host_producer_frame.v1",
|
|
2761
|
+
producer_id: this.host.producer_id,
|
|
2762
|
+
producer_installation_ref: this.installationRef,
|
|
2763
|
+
producer_epoch: this.epoch,
|
|
2764
|
+
sequence: state.next_sequence + offset,
|
|
2765
|
+
frame_kind: draft.frame_kind,
|
|
2766
|
+
occurred_at: this.host.createdAtOf(envelope),
|
|
2767
|
+
payload: draft.payload
|
|
2768
|
+
}));
|
|
2769
|
+
await this.writeState({ ...state, next_sequence: state.next_sequence + frames.length, pending: { ...state.pending, [submissionRef2]: frames } });
|
|
2770
|
+
return frames;
|
|
2771
|
+
}
|
|
2772
|
+
async release(submissionRef2) {
|
|
2773
|
+
const state = await this.readState();
|
|
2774
|
+
if (state.pending[submissionRef2] === void 0) return;
|
|
2775
|
+
const pending = { ...state.pending };
|
|
2776
|
+
delete pending[submissionRef2];
|
|
2777
|
+
await this.writeState({ ...state, pending });
|
|
2778
|
+
}
|
|
2779
|
+
async readState() {
|
|
2780
|
+
try {
|
|
2781
|
+
const value = JSON.parse(await readFile(this.statePath, "utf8"));
|
|
2782
|
+
return this.decodeState(value);
|
|
2783
|
+
} catch (error) {
|
|
2784
|
+
if (errorCode4(error) !== "ENOENT") throw error;
|
|
2785
|
+
return { schema_version: this.stateSchema, producer_installation_ref: this.installationRef, producer_epoch: this.epoch, next_sequence: 1, pending: {} };
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
async writeState(state) {
|
|
2789
|
+
await mkdir2(this.host.outbox_directory, { recursive: true, mode: 448 });
|
|
2790
|
+
const temporary = `${this.statePath}.${process.pid}.new`;
|
|
2791
|
+
await writeFile(temporary, JSON.stringify(state), { mode: 384 });
|
|
2792
|
+
await rename2(temporary, this.statePath);
|
|
2793
|
+
}
|
|
2794
|
+
decodeState(value) {
|
|
2795
|
+
const label = this.host.producer_label;
|
|
2796
|
+
const fail = (code2, message) => this.host.fail(code2, message);
|
|
2797
|
+
if (!isDrainStateHeader(value, this.stateSchema, this.installationRef)) {
|
|
2798
|
+
throw fail("storage", `${label} producer state header is malformed or belongs to another installation (${refusalFacts([
|
|
2799
|
+
{ name: "schema_version", value: isTranscriptRecord(value) ? value.schema_version : void 0, expected: this.stateSchema },
|
|
2800
|
+
{ name: "producer_installation_ref", value: isTranscriptRecord(value) ? value.producer_installation_ref : void 0, expected: this.installationRef }
|
|
2801
|
+
])}).`);
|
|
2802
|
+
}
|
|
2803
|
+
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}`)}).`);
|
|
2804
|
+
const pending = {};
|
|
2805
|
+
const storedEpoch = Number(value.producer_epoch);
|
|
2806
|
+
for (const [submissionRef2, frames] of Object.entries(value.pending)) {
|
|
2807
|
+
if (!isHostSafeRef(submissionRef2) || !Array.isArray(frames) || frames.length === 0) throw fail("storage", `${label} producer pending reservation is malformed.`);
|
|
2808
|
+
pending[submissionRef2] = frames.map((frame) => this.decodeStoredFrame(frame, storedEpoch));
|
|
2809
|
+
}
|
|
2810
|
+
return { schema_version: this.stateSchema, producer_installation_ref: this.installationRef, producer_epoch: this.epoch, next_sequence: Number(value.next_sequence), pending };
|
|
2811
|
+
}
|
|
2812
|
+
decodeStoredFrame(value, storedEpoch) {
|
|
2813
|
+
const label = this.host.producer_label;
|
|
2814
|
+
const json = Buffer.from(JSON.stringify(value), "utf8");
|
|
2815
|
+
const encoded = Buffer.allocUnsafe(json.length + 4);
|
|
2816
|
+
encoded.writeUInt32BE(json.length, 0);
|
|
2817
|
+
json.copy(encoded, 4);
|
|
2818
|
+
const frame = decodeHostProducerFrame(encoded);
|
|
2819
|
+
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([
|
|
2820
|
+
{ name: "producer_id", value: frame.producer_id, expected: this.host.producer_id },
|
|
2821
|
+
{ name: "producer_installation_ref", value: frame.producer_installation_ref, expected: this.installationRef },
|
|
2822
|
+
{ name: "producer_epoch", value: frame.producer_epoch, expected: `at most ${storedEpoch}` }
|
|
2823
|
+
])}).`);
|
|
2824
|
+
return frame;
|
|
2825
|
+
}
|
|
2826
|
+
};
|
|
2827
|
+
function isDrainStateHeader(value, schema, installationRef) {
|
|
2828
|
+
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);
|
|
2829
|
+
}
|
|
2830
|
+
function errorCode4(error) {
|
|
2831
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : null;
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// host/relay/src/relay-quarantine-store.ts
|
|
2835
|
+
var RELAY_QUARANTINE_MAX_BYTES = 16 * 1024 * 1024;
|
|
2836
|
+
var RELAY_QUARANTINE_FRAME_REASONS = ["frame_identity_conflict", "sequence_gap", "gateway_refused"];
|
|
2837
|
+
var RELAY_QUARANTINE_REASONS = [...RELAY_QUARANTINE_FRAME_REASONS, ...HOST_PRODUCER_FRAME_ERROR_CODES];
|
|
2838
|
+
|
|
2839
|
+
// host/relay/src/relay-admission-store.ts
|
|
2840
|
+
var DEFAULT_LIMITS = { global_count: 8192, global_bytes: 64 * 1024 * 1024, producer_count: 4096, producer_bytes: 32 * 1024 * 1024 };
|
|
2841
|
+
var RETRY_SECONDS = [1, 2, 4, 8, 16, 32, 60];
|
|
2842
|
+
var MAX_RETRYABLE_ATTEMPTS = RETRY_SECONDS.length + 10;
|
|
2843
|
+
|
|
2844
|
+
// host/claude/src/claude-decoder.ts
|
|
2845
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2846
|
+
|
|
2847
|
+
// host/claude/src/claude-types.ts
|
|
2848
|
+
var CLAUDE_HOST_ERROR_CODES = ["invalid_input", "payload_rejected", "storage", "configuration"];
|
|
2849
|
+
function isClaudeHostErrorCode(value) {
|
|
2850
|
+
return typeof value === "string" && CLAUDE_HOST_ERROR_CODES.some((code2) => code2 === value);
|
|
2851
|
+
}
|
|
2852
|
+
var CLAUDE_INVOCATION_CONTEXT_SCHEMA_VERSION = "ceal.claude_invocation_context.v1";
|
|
2853
|
+
var CLAUDE_OUTBOX_SCHEMA_VERSION = "ceal.claude_observation_outbox.v1";
|
|
2854
|
+
var CLAUDE_SESSION_SCHEMA_VERSION = "ceal.claude_session.v1";
|
|
2855
|
+
var CLAUDE_TRANSCRIPT_REGISTRATION_SCHEMA_VERSION = "ceal.claude_transcript_registration.v1";
|
|
2856
|
+
var CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION = "ceal.claude_transcript_cursor.v1";
|
|
2857
|
+
var CLAUDE_DEFAULT_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
2858
|
+
var CLAUDE_TRANSCRIPT_MAX_TURN_MESSAGES = 1024;
|
|
2859
|
+
var CLAUDE_TRANSCRIPT_MAX_SUBAGENT_FILES = 64;
|
|
2860
|
+
var CLAUDE_TRANSCRIPT_ANCHOR_BYTES = 256;
|
|
2861
|
+
var ClaudeHostError = class extends Error {
|
|
2862
|
+
name = "ClaudeHostError";
|
|
2863
|
+
code;
|
|
2864
|
+
diagnostic_kind;
|
|
2865
|
+
constructor(code2, message, diagnostic_kind) {
|
|
2866
|
+
super(message);
|
|
2867
|
+
if (!isClaudeHostErrorCode(code2)) throw new TypeError("Claude Host error code is invalid.");
|
|
2868
|
+
this.code = code2;
|
|
2869
|
+
this.diagnostic_kind = diagnostic_kind;
|
|
2870
|
+
}
|
|
2871
|
+
};
|
|
2872
|
+
|
|
2873
|
+
// host/claude/src/claude-decoder.ts
|
|
2874
|
+
var MAX_HOOK_BYTES = 256 * 1024;
|
|
2875
|
+
function claudeLocalTurnRef(sessionId, turnId) {
|
|
2876
|
+
return `claude-turn:${cealRequestSha256({ session_id: sessionId, prompt_id: turnId })}`;
|
|
2877
|
+
}
|
|
2878
|
+
function claudeLocalInputRef(sessionId, turnId) {
|
|
2879
|
+
return `claude-input:${cealRequestSha256({ session_id: sessionId, prompt_id: turnId })}`;
|
|
2880
|
+
}
|
|
2881
|
+
function claudeWorkspaceRef(cwd) {
|
|
2882
|
+
return `workspace:${digest(cwd)}`;
|
|
2883
|
+
}
|
|
2884
|
+
function digest(value) {
|
|
2885
|
+
return createHash4("sha256").update(value, "utf8").digest("hex");
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// host/claude/src/claude-outbox.ts
|
|
2889
|
+
import { mkdir as mkdir3, readdir, readFile as readFile2, unlink as unlink2 } from "node:fs/promises";
|
|
2890
|
+
import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
|
|
2891
|
+
|
|
2892
|
+
// host/invocation-context.ts
|
|
2893
|
+
var HOST_INVOCATION_IDENTITY_KEYS = ["host_session_ref", "host_turn_ref", "host_source_ref"];
|
|
2894
|
+
var HOST_INVOCATION_CONTEXT_REQUIRED_KEYS = ["schema_version", "identity", "prompt_ref", "expires_at"];
|
|
2895
|
+
function invocationContextKey(identity) {
|
|
2896
|
+
return cealRequestSha256({ host_source_ref: identity.host_source_ref, host_session_ref: identity.host_session_ref });
|
|
2897
|
+
}
|
|
2898
|
+
function hasHostInvocationIdentityKeys(value) {
|
|
2899
|
+
return exactKeys3(value, HOST_INVOCATION_IDENTITY_KEYS);
|
|
2900
|
+
}
|
|
2901
|
+
function hasHostInvocationContextKeys(value, optional = []) {
|
|
2902
|
+
return requiredKeys(value, HOST_INVOCATION_CONTEXT_REQUIRED_KEYS, optional);
|
|
2903
|
+
}
|
|
2904
|
+
async function readLatestSessionProjection(file, hostSourceRef, hostSessionRef, readProjection) {
|
|
2905
|
+
const context = await readProjection(file);
|
|
2906
|
+
if (context === null || context.identity.host_source_ref !== hostSourceRef || context.identity.host_session_ref !== hostSessionRef) return null;
|
|
2907
|
+
return context;
|
|
2908
|
+
}
|
|
2909
|
+
function isLiveSessionProjection(context, now) {
|
|
2910
|
+
return context !== null && Date.parse(context.expires_at) > now.getTime();
|
|
2911
|
+
}
|
|
2912
|
+
function invocationContextFromTurn(identity, promptRef, expiresAt, descriptor) {
|
|
2913
|
+
const context = {
|
|
2914
|
+
schema_version: descriptor.schema_version,
|
|
2915
|
+
identity,
|
|
2916
|
+
prompt_ref: promptRef,
|
|
2917
|
+
expires_at: expiresAt,
|
|
2918
|
+
...descriptor.turn_generation_ref === void 0 ? {} : { turn_generation_ref: descriptor.turn_generation_ref },
|
|
2919
|
+
...descriptor.working_directory === void 0 ? {} : { working_directory: descriptor.working_directory }
|
|
2920
|
+
};
|
|
2921
|
+
descriptor.validate(context);
|
|
2922
|
+
return Object.freeze(context);
|
|
2923
|
+
}
|
|
2924
|
+
function isHostInvocationTurnSelection(value) {
|
|
2925
|
+
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]));
|
|
2926
|
+
}
|
|
2927
|
+
function invocationTurnSelectionMatches(context, selection) {
|
|
2928
|
+
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;
|
|
2929
|
+
}
|
|
2930
|
+
function exactKeys3(value, keys) {
|
|
2931
|
+
const actual = Object.keys(value);
|
|
2932
|
+
return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key)) && actual.every((key) => keys.includes(key));
|
|
2933
|
+
}
|
|
2934
|
+
function requiredKeys(value, required, optional) {
|
|
2935
|
+
const actual = Object.keys(value);
|
|
2936
|
+
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
2937
|
+
return actual.length >= required.length && actual.length <= allowed.size && required.every((key) => Object.hasOwn(value, key)) && actual.every((key) => allowed.has(key));
|
|
2938
|
+
}
|
|
2939
|
+
function isRecord2(value) {
|
|
2940
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2941
|
+
}
|
|
2942
|
+
function safeRef2(value) {
|
|
2943
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value);
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
// host/claude/src/claude-invocation-context.ts
|
|
2947
|
+
function claudeInvocationContextFromTurn(identity, promptRef, expiresAt, workingDirectory, turnGenerationRef) {
|
|
2948
|
+
return invocationContextFromTurn(identity, promptRef, expiresAt, { schema_version: CLAUDE_INVOCATION_CONTEXT_SCHEMA_VERSION, validate: validateClaudeHostInvocationContext, working_directory: workingDirectory, turn_generation_ref: turnGenerationRef });
|
|
2949
|
+
}
|
|
2950
|
+
function validateClaudeHostInvocationContext(value) {
|
|
2951
|
+
if (!contextShape(value)) throw new ClaudeHostError("storage", "Claude Host invocation context is malformed.");
|
|
2952
|
+
validateIdentity2(value.identity);
|
|
2953
|
+
validateContextReferences(value);
|
|
2954
|
+
validateContextTimes(value);
|
|
2955
|
+
}
|
|
2956
|
+
function contextShape(value) {
|
|
2957
|
+
return record3(value) && value.schema_version === CLAUDE_INVOCATION_CONTEXT_SCHEMA_VERSION && hasHostInvocationContextKeys(value, ["request_ref", "bound_at", "working_directory", "turn_generation_ref"]);
|
|
2958
|
+
}
|
|
2959
|
+
function validateIdentity2(value) {
|
|
2960
|
+
if (!validIdentity(value)) throw new ClaudeHostError("storage", "Claude Host invocation identity is malformed.");
|
|
2961
|
+
}
|
|
2962
|
+
function validateContextReferences(value) {
|
|
2963
|
+
if (!safeRef3(value.prompt_ref) || !optionalSafeRef(value.request_ref) || !optionalSafeRef(value.turn_generation_ref)) throw new ClaudeHostError("storage", "Claude Host invocation context is malformed.");
|
|
2964
|
+
}
|
|
2965
|
+
function validateContextTimes(value) {
|
|
2966
|
+
if (!timestamp2(value.expires_at) || !optionalTimestamp(value.bound_at) || !optionalDirectory(value.working_directory)) throw new ClaudeHostError("storage", "Claude Host invocation context is malformed.");
|
|
2967
|
+
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)}`)}).`);
|
|
2968
|
+
}
|
|
2969
|
+
function validIdentity(value) {
|
|
2970
|
+
return record3(value) && hasHostInvocationIdentityKeys(value) && [value.host_session_ref, value.host_turn_ref, value.host_source_ref].every(safeRef3);
|
|
2971
|
+
}
|
|
2972
|
+
function optionalSafeRef(value) {
|
|
2973
|
+
return value === void 0 || safeRef3(value);
|
|
2974
|
+
}
|
|
2975
|
+
function optionalTimestamp(value) {
|
|
2976
|
+
return value === void 0 || timestamp2(value);
|
|
2977
|
+
}
|
|
2978
|
+
function optionalDirectory(value) {
|
|
2979
|
+
return value === void 0 || typeof value === "string" && value.startsWith("/") && !value.includes("\0") && value.length <= 4096;
|
|
2980
|
+
}
|
|
2981
|
+
function safeRef3(value) {
|
|
2982
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value);
|
|
2983
|
+
}
|
|
2984
|
+
function timestamp2(value) {
|
|
2985
|
+
return typeof value === "string" && value.length <= 64 && Number.isFinite(Date.parse(value));
|
|
2986
|
+
}
|
|
2987
|
+
function record3(value) {
|
|
2988
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
// host/claude/src/claude-outbox.ts
|
|
2992
|
+
var SAFE_REF4 = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
2993
|
+
var CLAUDE_TRANSCRIPT_PATH_MAX_CHARACTERS = 4096;
|
|
2994
|
+
var CLAUDE_TRANSCRIPT_SUBAGENT_NAME_MAX_CHARACTERS = 256;
|
|
2995
|
+
var ClaudeOutboxStore = class {
|
|
2996
|
+
envelopes;
|
|
2997
|
+
sessions;
|
|
2998
|
+
registrations;
|
|
2999
|
+
cursors;
|
|
3000
|
+
directory;
|
|
3001
|
+
activeContexts;
|
|
3002
|
+
turnSelections;
|
|
3003
|
+
constructor(directory) {
|
|
3004
|
+
if (!directory.startsWith("/")) throw new ClaudeHostError("configuration", "Claude outbox directory must be absolute.");
|
|
3005
|
+
this.directory = directory;
|
|
3006
|
+
this.envelopes = join3(directory, "envelopes");
|
|
3007
|
+
this.sessions = join3(directory, "sessions");
|
|
3008
|
+
this.registrations = join3(directory, "transcript-registrations");
|
|
3009
|
+
this.cursors = join3(directory, "transcript-cursors");
|
|
3010
|
+
this.activeContexts = join3(directory, "active-contexts");
|
|
3011
|
+
this.turnSelections = join3(directory, "turn-selections");
|
|
3012
|
+
}
|
|
3013
|
+
async prepare() {
|
|
3014
|
+
for (const path of [this.envelopes, this.sessions, this.registrations, this.cursors, this.activeContexts, this.turnSelections]) {
|
|
3015
|
+
await mkdir3(path, { recursive: true, mode: 448 });
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
async activateInvocationContext(identity, promptRef, expiresAt, workingDirectory, turnGenerationRef) {
|
|
3019
|
+
const context = claudeInvocationContextFromTurn(identity, promptRef, expiresAt, workingDirectory, turnGenerationRef);
|
|
3020
|
+
await this.prepare();
|
|
3021
|
+
await durableAtomicReplace(this.activeContextPath(identity), JSON.stringify(context));
|
|
3022
|
+
}
|
|
3023
|
+
async readInvocationContext(hostSourceRef, hostSessionRef, now = /* @__PURE__ */ new Date()) {
|
|
2299
3024
|
validateInvocationContextReadArguments(hostSourceRef, hostSessionRef, now);
|
|
3025
|
+
const context = await this.readRawInvocationContext(hostSourceRef, hostSessionRef);
|
|
3026
|
+
return isLiveSessionProjection(context, now) ? context : null;
|
|
3027
|
+
}
|
|
3028
|
+
async writeInvocationTurnSelection(selection) {
|
|
3029
|
+
if (!isHostInvocationTurnSelection(selection)) throw new ClaudeHostError("invalid_input", "Claude Host turn selection is invalid.");
|
|
2300
3030
|
await this.prepare();
|
|
2301
|
-
|
|
3031
|
+
await durableAtomicReplace(this.turnSelectionPath(selection.host_source_ref, selection.host_session_ref), JSON.stringify(selection));
|
|
3032
|
+
}
|
|
3033
|
+
async attachInvocationTurnGeneration(identity, promptRef, turnGenerationRef) {
|
|
3034
|
+
const selection = await this.readInvocationTurnSelection(identity.host_source_ref, identity.host_session_ref);
|
|
3035
|
+
if (selection === null || selection.prompt_ref !== promptRef || selection.turn_generation_ref !== turnGenerationRef) return false;
|
|
3036
|
+
const path = this.activeContextPath(identity);
|
|
3037
|
+
const active = await readProjectionFile(path);
|
|
3038
|
+
if (active === null || active.prompt_ref !== promptRef || active.identity.host_turn_ref !== identity.host_turn_ref) return false;
|
|
3039
|
+
const updated = { ...active, turn_generation_ref: turnGenerationRef };
|
|
3040
|
+
validateClaudeHostInvocationContext(updated);
|
|
3041
|
+
await durableAtomicReplace(path, JSON.stringify(updated));
|
|
3042
|
+
return true;
|
|
3043
|
+
}
|
|
3044
|
+
/**
|
|
3045
|
+
* The latest Claude session projection even when past expiry, so the
|
|
3046
|
+
* invocation authenticator can report an expired context distinctly from
|
|
3047
|
+
* a missing one. Every other caller keeps the fail-closed read above.
|
|
3048
|
+
*/
|
|
3049
|
+
async readRawInvocationContext(hostSourceRef, hostSessionRef) {
|
|
3050
|
+
if (!SAFE_REF4.test(hostSourceRef) || !SAFE_REF4.test(hostSessionRef)) throw new ClaudeHostError("invalid_input", "Claude Host context identity or clock is invalid.");
|
|
3051
|
+
await this.prepare();
|
|
3052
|
+
const context = await readLatestSessionProjection(this.activeContextPath({ host_source_ref: hostSourceRef, host_session_ref: hostSessionRef }), hostSourceRef, hostSessionRef, readProjectionFile);
|
|
3053
|
+
return invocationTurnSelectionMatches(context, await this.readInvocationTurnSelection(hostSourceRef, hostSessionRef)) ? context : null;
|
|
2302
3054
|
}
|
|
2303
3055
|
async listInvocationContexts(now = /* @__PURE__ */ new Date()) {
|
|
2304
3056
|
await this.prepare();
|
|
@@ -2306,106 +3058,175 @@ var ClaudeOutboxStore = class {
|
|
|
2306
3058
|
for (const name of await readdir(this.activeContexts)) {
|
|
2307
3059
|
if (!name.endsWith(".json")) continue;
|
|
2308
3060
|
try {
|
|
2309
|
-
const value = JSON.parse(await
|
|
3061
|
+
const value = JSON.parse(await readFile2(join3(this.activeContexts, name), "utf8"));
|
|
2310
3062
|
validateClaudeHostInvocationContext(value);
|
|
2311
|
-
if (Date.parse(value.expires_at) > now.getTime()) contexts.push(value);
|
|
3063
|
+
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);
|
|
2312
3064
|
} catch {
|
|
2313
3065
|
}
|
|
2314
3066
|
}
|
|
2315
3067
|
return contexts;
|
|
2316
3068
|
}
|
|
2317
|
-
async
|
|
2318
|
-
|
|
3069
|
+
async putEnvelope(entry) {
|
|
3070
|
+
validateEnvelope(entry);
|
|
2319
3071
|
await this.prepare();
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
3072
|
+
await durableAtomicReplace(this.envelopePath(entry.submission_ref), JSON.stringify(entry));
|
|
3073
|
+
}
|
|
3074
|
+
async listEnvelopes() {
|
|
3075
|
+
await this.prepare();
|
|
3076
|
+
const entries = [];
|
|
3077
|
+
let malformed = 0;
|
|
3078
|
+
for (const name of await readdir(this.envelopes)) {
|
|
3079
|
+
if (!name.endsWith(".json")) continue;
|
|
2323
3080
|
try {
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
3081
|
+
const value = JSON.parse(await readFile2(join3(this.envelopes, name), "utf8"));
|
|
3082
|
+
validateEnvelope(value);
|
|
3083
|
+
entries.push(value);
|
|
3084
|
+
} catch {
|
|
3085
|
+
malformed += 1;
|
|
2328
3086
|
}
|
|
3087
|
+
}
|
|
3088
|
+
entries.sort((left, right) => left.created_at.localeCompare(right.created_at));
|
|
3089
|
+
return { entries, malformed };
|
|
3090
|
+
}
|
|
3091
|
+
async removeEnvelope(entry) {
|
|
3092
|
+
validateEnvelope(entry);
|
|
3093
|
+
try {
|
|
3094
|
+
await unlink2(this.envelopePath(entry.submission_ref));
|
|
2329
3095
|
} catch (error) {
|
|
2330
|
-
if (code(error) !== "
|
|
2331
|
-
const existing = JSON.parse(await readFile(path, "utf8"));
|
|
2332
|
-
validateBatch(existing);
|
|
2333
|
-
if (JSON.stringify(existing) !== JSON.stringify(batch)) throw new ClaudeHostError("storage", "Claude hook batch identity conflicts with different bytes.");
|
|
3096
|
+
if (code(error) !== "ENOENT") throw error;
|
|
2334
3097
|
}
|
|
2335
3098
|
}
|
|
2336
|
-
async
|
|
3099
|
+
async saveSession(snapshot) {
|
|
3100
|
+
validateSession(snapshot);
|
|
2337
3101
|
await this.prepare();
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
3102
|
+
await durableAtomicReplace(join3(this.sessions, `${cealRequestSha256({ source: snapshot.host_source_ref, session: snapshot.host_session_ref })}.json`), JSON.stringify(snapshot));
|
|
3103
|
+
}
|
|
3104
|
+
async readSession(sourceRef, sessionRef) {
|
|
3105
|
+
if (!SAFE_REF4.test(sourceRef) || !SAFE_REF4.test(sessionRef)) throw new ClaudeHostError("invalid_input", "Claude session identity is invalid.");
|
|
3106
|
+
await this.prepare();
|
|
3107
|
+
try {
|
|
3108
|
+
const value = JSON.parse(await readFile2(join3(this.sessions, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`), "utf8"));
|
|
3109
|
+
validateSession(value);
|
|
3110
|
+
return value;
|
|
3111
|
+
} catch (error) {
|
|
3112
|
+
if (code(error) === "ENOENT") return null;
|
|
3113
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Claude session snapshot is malformed.");
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
async registerTranscript(registration) {
|
|
3117
|
+
validateRegistration(registration);
|
|
3118
|
+
await this.prepare();
|
|
3119
|
+
const path = this.registrationPath(registration.host_source_ref, registration.host_session_ref);
|
|
3120
|
+
const existing = await this.readRegistrationFile(path);
|
|
3121
|
+
if (existing !== null && (existing.claude_session_id !== registration.claude_session_id || existing.transcript_path !== registration.transcript_path)) {
|
|
3122
|
+
throw new ClaudeHostError("storage", "Claude transcript registration conflicts with the source-bound session.");
|
|
3123
|
+
}
|
|
3124
|
+
await durableAtomicReplace(path, JSON.stringify(registration));
|
|
3125
|
+
}
|
|
3126
|
+
/**
|
|
3127
|
+
* End a registration whose transcript the Host has deleted. The cursor is
|
|
3128
|
+
* keyed by the same session and describes byte offsets of a file that no
|
|
3129
|
+
* longer exists, so it leaves with the registration.
|
|
3130
|
+
*/
|
|
3131
|
+
async retireTranscriptRegistration(registration) {
|
|
3132
|
+
validateRegistration(registration);
|
|
3133
|
+
await this.prepare();
|
|
3134
|
+
await this.removeStateFile(this.registrationPath(registration.host_source_ref, registration.host_session_ref));
|
|
3135
|
+
await this.removeStateFile(this.cursorPath(registration.host_source_ref, registration.host_session_ref));
|
|
3136
|
+
}
|
|
3137
|
+
async listTranscriptRegistrations() {
|
|
3138
|
+
await this.prepare();
|
|
3139
|
+
const registrations = [];
|
|
3140
|
+
for (const name of await readdir(this.registrations)) {
|
|
2341
3141
|
if (!name.endsWith(".json")) continue;
|
|
2342
3142
|
try {
|
|
2343
|
-
const value = JSON.parse(await
|
|
2344
|
-
|
|
2345
|
-
|
|
3143
|
+
const value = JSON.parse(await readFile2(join3(this.registrations, name), "utf8"));
|
|
3144
|
+
validateRegistration(value);
|
|
3145
|
+
registrations.push(value);
|
|
2346
3146
|
} catch {
|
|
2347
|
-
malformed += 1;
|
|
2348
3147
|
}
|
|
2349
3148
|
}
|
|
2350
|
-
|
|
2351
|
-
return { entries: batches, malformed };
|
|
3149
|
+
return registrations.sort((left, right) => left.registered_at.localeCompare(right.registered_at));
|
|
2352
3150
|
}
|
|
2353
|
-
async
|
|
3151
|
+
async readInvocationTurnSelection(hostSourceRef, hostSessionRef) {
|
|
3152
|
+
if (!SAFE_REF4.test(hostSourceRef) || !SAFE_REF4.test(hostSessionRef)) throw new ClaudeHostError("invalid_input", "Claude Host turn selection identity is invalid.");
|
|
3153
|
+
await this.prepare();
|
|
2354
3154
|
try {
|
|
2355
|
-
await
|
|
3155
|
+
const value = JSON.parse(await readFile2(this.turnSelectionPath(hostSourceRef, hostSessionRef), "utf8"));
|
|
3156
|
+
if (!isHostInvocationTurnSelection(value) || value.host_source_ref !== hostSourceRef || value.host_session_ref !== hostSessionRef) throw new ClaudeHostError("storage", "Claude Host turn selection is malformed.");
|
|
3157
|
+
return value;
|
|
2356
3158
|
} catch (error) {
|
|
2357
|
-
if (code(error)
|
|
3159
|
+
if (code(error) === "ENOENT") return null;
|
|
3160
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Unable to read the Claude Host turn selection.");
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
async readTranscriptCursor(registration) {
|
|
3164
|
+
validateRegistration(registration);
|
|
3165
|
+
await this.prepare();
|
|
3166
|
+
try {
|
|
3167
|
+
const value = JSON.parse(await readFile2(this.cursorPath(registration.host_source_ref, registration.host_session_ref), "utf8"));
|
|
3168
|
+
validateCursor(value);
|
|
3169
|
+
return value;
|
|
3170
|
+
} catch (error) {
|
|
3171
|
+
if (code(error) === "ENOENT") return null;
|
|
3172
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Claude transcript cursor is malformed.");
|
|
2358
3173
|
}
|
|
2359
3174
|
}
|
|
2360
|
-
|
|
2361
|
-
|
|
3175
|
+
async writeTranscriptCursor(cursor) {
|
|
3176
|
+
validateCursor(cursor);
|
|
3177
|
+
await this.prepare();
|
|
3178
|
+
await durableAtomicReplace(this.cursorPath(cursor.host_source_ref, cursor.host_session_ref), JSON.stringify(cursor));
|
|
3179
|
+
}
|
|
3180
|
+
envelopePath(ref) {
|
|
3181
|
+
return join3(this.envelopes, `${cealRequestSha256(ref)}.json`);
|
|
3182
|
+
}
|
|
3183
|
+
registrationPath(sourceRef, sessionRef) {
|
|
3184
|
+
return join3(this.registrations, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`);
|
|
3185
|
+
}
|
|
3186
|
+
turnSelectionPath(sourceRef, sessionRef) {
|
|
3187
|
+
return join3(this.turnSelections, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`);
|
|
3188
|
+
}
|
|
3189
|
+
cursorPath(sourceRef, sessionRef) {
|
|
3190
|
+
return join3(this.cursors, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`);
|
|
3191
|
+
}
|
|
3192
|
+
async readRegistrationFile(path) {
|
|
3193
|
+
try {
|
|
3194
|
+
const value = JSON.parse(await readFile2(path, "utf8"));
|
|
3195
|
+
validateRegistration(value);
|
|
3196
|
+
return value;
|
|
3197
|
+
} catch (error) {
|
|
3198
|
+
if (code(error) === "ENOENT") return null;
|
|
3199
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Claude transcript registration is malformed.");
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
async removeStateFile(path) {
|
|
3203
|
+
try {
|
|
3204
|
+
await unlink2(path);
|
|
3205
|
+
} catch (error) {
|
|
3206
|
+
if (code(error) !== "ENOENT") throw error;
|
|
3207
|
+
}
|
|
2362
3208
|
}
|
|
2363
3209
|
activeContextPath(identity) {
|
|
2364
|
-
return
|
|
3210
|
+
return join3(this.activeContexts, `${invocationContextKey(identity)}.json`);
|
|
2365
3211
|
}
|
|
2366
3212
|
};
|
|
2367
3213
|
function validateInvocationContextReadArguments(hostSourceRef, hostSessionRef, now) {
|
|
2368
3214
|
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.");
|
|
2369
3215
|
}
|
|
2370
|
-
async function
|
|
3216
|
+
async function readProjectionFile(file) {
|
|
2371
3217
|
try {
|
|
2372
|
-
const context = JSON.parse(await
|
|
3218
|
+
const context = JSON.parse(await readFile2(file, "utf8"));
|
|
2373
3219
|
validateClaudeHostInvocationContext(context);
|
|
2374
|
-
return
|
|
3220
|
+
return context;
|
|
2375
3221
|
} catch (error) {
|
|
2376
3222
|
if (code(error) === "ENOENT") return null;
|
|
2377
3223
|
if (error instanceof ClaudeHostError) throw error;
|
|
2378
3224
|
throw new ClaudeHostError("storage", "Unable to read the Claude Host invocation context.");
|
|
2379
3225
|
}
|
|
2380
3226
|
}
|
|
2381
|
-
function
|
|
2382
|
-
return context.identity.host_source_ref === hostSourceRef && context.identity.host_session_ref === hostSessionRef;
|
|
2383
|
-
}
|
|
2384
|
-
function compareBatch(left, right) {
|
|
2385
|
-
return rank(left.hook_event) - rank(right.hook_event) || left.occurred_at.localeCompare(right.occurred_at) || left.batch_ref.localeCompare(right.batch_ref);
|
|
2386
|
-
}
|
|
2387
|
-
function rank(event) {
|
|
2388
|
-
return event === "SessionStart" ? 0 : event === "UserPromptSubmit" ? 1 : 2;
|
|
2389
|
-
}
|
|
2390
|
-
function validateBatch(value) {
|
|
2391
|
-
if (!validBatchHeader(value)) throw new ClaudeHostError("storage", "Claude hook batch is malformed.");
|
|
2392
|
-
for (const [index, draft] of value.frames.entries()) {
|
|
2393
|
-
if (!record4(draft) || !hostFrameKind(draft.frame_kind) || !record4(draft.payload)) throw new ClaudeHostError("storage", "Claude frame draft is malformed.");
|
|
2394
|
-
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 });
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2397
|
-
function validBatchHeader(value) {
|
|
2398
|
-
return record4(value) && value.schema_version === CLAUDE_OUTBOX_SCHEMA && safeRef3(value.batch_ref) && isClaudeHookEvent(value.hook_event) && safeRef3(value.local_session_ref) && optionalRef(value.local_turn_ref) && timestamp3(value.occurred_at) && Array.isArray(value.frames) && value.frames.length > 0 && value.frames.length <= 3;
|
|
2399
|
-
}
|
|
2400
|
-
function hostFrameKind(value) {
|
|
2401
|
-
return isHostProducerFrameKind(value);
|
|
2402
|
-
}
|
|
2403
|
-
function safeRef3(value) {
|
|
3227
|
+
function safeRef4(value) {
|
|
2404
3228
|
return typeof value === "string" && SAFE_REF4.test(value);
|
|
2405
3229
|
}
|
|
2406
|
-
function optionalRef(value) {
|
|
2407
|
-
return value === void 0 || typeof value === "string" && SAFE_REF4.test(value);
|
|
2408
|
-
}
|
|
2409
3230
|
function timestamp3(value) {
|
|
2410
3231
|
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
2411
3232
|
}
|
|
@@ -2415,131 +3236,811 @@ function record4(value) {
|
|
|
2415
3236
|
function code(error) {
|
|
2416
3237
|
return record4(error) && typeof error.code === "string" ? error.code : null;
|
|
2417
3238
|
}
|
|
3239
|
+
function validateEnvelope(value) {
|
|
3240
|
+
if (!record4(value)) throw new ClaudeHostError("storage", "Claude outbox entry is malformed.");
|
|
3241
|
+
if (!sameKeys(value, ["schema_version", "identity", "host_input_ref", "input_kind", "prompt_ref", "observations", "origin_input", "submission_ref", "created_at", "expires_at"], ["request_ref"])) {
|
|
3242
|
+
throw new ClaudeHostError("storage", `Claude outbox entry key set is malformed (${refusalFact("keys", Object.keys(value), "exactly the outbox entry key set")}).`);
|
|
3243
|
+
}
|
|
3244
|
+
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}`)}).`);
|
|
3245
|
+
validateEnvelopeIdentity(value);
|
|
3246
|
+
validateEnvelopeObservations(value.observations);
|
|
3247
|
+
if (typeof value.origin_input !== "boolean") throw new ClaudeHostError("storage", "Claude outbox origin disposition is malformed.");
|
|
3248
|
+
if (!timestamp3(value.created_at) || !timestamp3(value.expires_at)) throw new ClaudeHostError("storage", "Claude outbox timestamps are malformed.");
|
|
3249
|
+
}
|
|
3250
|
+
function validateEnvelopeIdentity(value) {
|
|
3251
|
+
validateIdentity3(value.identity);
|
|
3252
|
+
if (!safeRef4(value.host_input_ref)) throw new ClaudeHostError("storage", "Claude outbox input reference is malformed.");
|
|
3253
|
+
if (!isCealHostInputKind(value.input_kind)) throw new ClaudeHostError("storage", "Claude outbox input provenance is malformed.");
|
|
3254
|
+
if (!safeRef4(value.prompt_ref) || !safeRef4(value.submission_ref)) throw new ClaudeHostError("storage", "Claude outbox references are malformed.");
|
|
3255
|
+
if (value.request_ref !== void 0 && !safeRef4(value.request_ref)) throw new ClaudeHostError("storage", "Claude outbox Request reference is malformed.");
|
|
3256
|
+
}
|
|
3257
|
+
function validateEnvelopeObservations(value) {
|
|
3258
|
+
if (!Array.isArray(value)) throw new ClaudeHostError("storage", "Claude outbox observations are malformed.");
|
|
3259
|
+
try {
|
|
3260
|
+
for (const observation2 of value) validateCealHostObservationDraft(observation2);
|
|
3261
|
+
} catch {
|
|
3262
|
+
throw new ClaudeHostError("storage", "Claude outbox observations are malformed.");
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
function validateSession(value) {
|
|
3266
|
+
if (!record4(value)) throw new ClaudeHostError("storage", "Claude session snapshot is malformed.");
|
|
3267
|
+
if (!sameKeys(value, ["schema_version", "host_session_ref", "host_source_ref", "observed_at", "value", "expires_at"])) {
|
|
3268
|
+
throw new ClaudeHostError("storage", `Claude session snapshot key set is malformed (${refusalFact("keys", Object.keys(value), "exactly the session snapshot key set")}).`);
|
|
3269
|
+
}
|
|
3270
|
+
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}`)}).`);
|
|
3271
|
+
if (!safeRef4(value.host_session_ref) || !safeRef4(value.host_source_ref) || !timestamp3(value.observed_at) || !timestamp3(value.expires_at)) {
|
|
3272
|
+
throw new ClaudeHostError("storage", "Claude session snapshot fields are malformed.");
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
function validateRegistration(value) {
|
|
3276
|
+
if (!record4(value)) throw new ClaudeHostError("storage", "Claude transcript registration is malformed.");
|
|
3277
|
+
if (!sameKeys(value, ["schema_version", "host_source_ref", "host_session_ref", "claude_session_id", "transcript_path", "registered_at", "expires_at"], ["cwd"])) {
|
|
3278
|
+
throw new ClaudeHostError("storage", `Claude transcript registration key set is malformed (${refusalFact("keys", Object.keys(value), "exactly the registration key set")}).`);
|
|
3279
|
+
}
|
|
3280
|
+
if (value.schema_version !== CLAUDE_TRANSCRIPT_REGISTRATION_SCHEMA_VERSION) throw new ClaudeHostError("storage", "Claude transcript registration is malformed.");
|
|
3281
|
+
if (![value.host_source_ref, value.host_session_ref, value.claude_session_id].every((candidate) => typeof candidate === "string" && SAFE_REF4.test(candidate))) {
|
|
3282
|
+
throw new ClaudeHostError("storage", "Claude transcript registration references are malformed.");
|
|
3283
|
+
}
|
|
3284
|
+
validateRegistrationPaths(value.cwd, value.transcript_path);
|
|
3285
|
+
if (!timestamp3(value.registered_at) || !timestamp3(value.expires_at)) throw new ClaudeHostError("storage", "Claude transcript registration timestamps are malformed.");
|
|
3286
|
+
}
|
|
3287
|
+
function validateRegistrationPaths(cwd, transcriptPath) {
|
|
3288
|
+
if (cwd !== void 0 && (typeof cwd !== "string" || !isAbsolute2(cwd) || cwd.length > CLAUDE_TRANSCRIPT_PATH_MAX_CHARACTERS || cwd.includes("\0"))) {
|
|
3289
|
+
throw new ClaudeHostError("storage", "Claude transcript registration working directory is malformed.");
|
|
3290
|
+
}
|
|
3291
|
+
if (typeof transcriptPath !== "string" || transcriptPath.length === 0 || transcriptPath.length > CLAUDE_TRANSCRIPT_PATH_MAX_CHARACTERS) {
|
|
3292
|
+
throw new ClaudeHostError("storage", "Claude transcript registration path is malformed.");
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
function validateCursor(value) {
|
|
3296
|
+
if (!record4(value)) throw new ClaudeHostError("storage", "Claude transcript cursor is malformed.");
|
|
3297
|
+
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"])) {
|
|
3298
|
+
throw new ClaudeHostError("storage", "Claude transcript cursor key set is malformed.");
|
|
3299
|
+
}
|
|
3300
|
+
if (value.schema_version !== CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION) throw new ClaudeHostError("storage", "Claude transcript cursor is malformed.");
|
|
3301
|
+
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.");
|
|
3302
|
+
if (typeof value.transcript_path !== "string" || value.transcript_path.length === 0) throw new ClaudeHostError("storage", "Claude transcript cursor path is malformed.");
|
|
3303
|
+
validateCursorPosition(value);
|
|
3304
|
+
validateCursorTurn(value.open_turn, value.unattributed, value.session_observed);
|
|
3305
|
+
validateSubagentCursors(value.subagents);
|
|
3306
|
+
if (value.reason !== void 0 && !isHostTranscriptCursorReason(value.reason)) throw new ClaudeHostError("storage", "Claude transcript cursor reason is malformed.");
|
|
3307
|
+
}
|
|
3308
|
+
function validateCursorPosition(value) {
|
|
3309
|
+
if (!validCursorPosition(value)) throw new ClaudeHostError("storage", "Claude transcript cursor position is malformed.");
|
|
3310
|
+
if (value.device !== void 0 && typeof value.device !== "string") throw new ClaudeHostError("storage", "Claude transcript cursor device is malformed.");
|
|
3311
|
+
if (value.inode !== void 0 && typeof value.inode !== "string") throw new ClaudeHostError("storage", "Claude transcript cursor inode is malformed.");
|
|
3312
|
+
if (value.device === void 0 && value.offset !== 0) throw new ClaudeHostError("storage", "Claude transcript cursor is unpinned past its start.");
|
|
3313
|
+
if (value.status !== "active" && value.status !== "quarantined" || !timestamp3(value.updated_at)) throw new ClaudeHostError("storage", "Claude transcript cursor status is malformed.");
|
|
3314
|
+
}
|
|
3315
|
+
function validateCursorTurn(openTurn, unattributed, sessionObserved) {
|
|
3316
|
+
if (openTurn !== void 0 && !validOpenTurn(openTurn)) throw new ClaudeHostError("storage", "Claude transcript turn cursor is malformed.");
|
|
3317
|
+
if (unattributed !== void 0 && !validUnattributed(unattributed)) throw new ClaudeHostError("storage", "Claude transcript unattributed cursor is malformed.");
|
|
3318
|
+
if (sessionObserved !== void 0 && typeof sessionObserved !== "boolean") throw new ClaudeHostError("storage", "Claude transcript cursor session marker is malformed.");
|
|
3319
|
+
}
|
|
3320
|
+
function validateSubagentCursors(value) {
|
|
3321
|
+
if (!record4(value)) throw new ClaudeHostError("storage", "Claude transcript subagent cursors are malformed.");
|
|
3322
|
+
for (const [name, entry] of Object.entries(value)) validateSubagentEntry(name, entry);
|
|
3323
|
+
}
|
|
3324
|
+
function validateSubagentEntry(name, entry) {
|
|
3325
|
+
if (name.length === 0 || name.length > CLAUDE_TRANSCRIPT_SUBAGENT_NAME_MAX_CHARACTERS || name.includes("/")) throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3326
|
+
if (name.includes("\0")) throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3327
|
+
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.");
|
|
3328
|
+
validateSubagentIdentity(entry);
|
|
3329
|
+
}
|
|
3330
|
+
function validateSubagentIdentity(entry) {
|
|
3331
|
+
if (entry.device !== void 0 && typeof entry.device !== "string" || entry.inode !== void 0 && typeof entry.inode !== "string") {
|
|
3332
|
+
throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3333
|
+
}
|
|
3334
|
+
if (!nonNegativeInteger(entry.anchor_start) || typeof entry.anchor_sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(entry.anchor_sha256)) {
|
|
3335
|
+
throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3336
|
+
}
|
|
3337
|
+
if (entry.device === void 0 && Number(entry.offset) !== 0) throw new ClaudeHostError("storage", "Claude transcript subagent cursor is unpinned past its start.");
|
|
3338
|
+
}
|
|
3339
|
+
function validCursorPosition(value) {
|
|
3340
|
+
return nonNegativeInteger(value.offset) && nonNegativeInteger(value.anchor_start) && typeof value.anchor_sha256 === "string" && /^[a-f0-9]{64}$/u.test(value.anchor_sha256);
|
|
3341
|
+
}
|
|
3342
|
+
function validOpenTurn(value) {
|
|
3343
|
+
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;
|
|
3344
|
+
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;
|
|
3345
|
+
if (!timestamp3(value.started_at) || !timestamp3(value.last_observed_at)) return false;
|
|
3346
|
+
if (!validMessageIds(value.message_ids)) return false;
|
|
3347
|
+
if (!validTurnCounts(value.counts)) return false;
|
|
3348
|
+
return value.last_terminal_digest === void 0 || typeof value.last_terminal_digest === "string" && /^[a-f0-9]{64}$/u.test(value.last_terminal_digest);
|
|
3349
|
+
}
|
|
3350
|
+
function validMessageIds(value) {
|
|
3351
|
+
if (!Array.isArray(value) || value.length > CLAUDE_TRANSCRIPT_MAX_TURN_MESSAGES) return false;
|
|
3352
|
+
if (!value.every((candidate) => typeof candidate === "string" && candidate.length > 0 && candidate.length <= 256)) return false;
|
|
3353
|
+
return new Set(value).size === value.length;
|
|
3354
|
+
}
|
|
3355
|
+
function validUnattributed(value) {
|
|
3356
|
+
if (!record4(value) || !sameKeys(value, ["input", "cached_input", "cache_write_input", "output", "message_ids", "last_observed_at"])) return false;
|
|
3357
|
+
if (!validMessageIds(value.message_ids) || value.message_ids.length === 0) return false;
|
|
3358
|
+
if (!timestamp3(value.last_observed_at)) return false;
|
|
3359
|
+
return validTurnCounts(value);
|
|
3360
|
+
}
|
|
3361
|
+
function validTurnCounts(value) {
|
|
3362
|
+
if (!record4(value) || !sameKeys(value, ["input", "cached_input", "cache_write_input", "output"])) return false;
|
|
3363
|
+
return [value.input, value.cached_input, value.cache_write_input, value.output].every(nonNegativeInteger);
|
|
3364
|
+
}
|
|
3365
|
+
function validateIdentity3(value) {
|
|
3366
|
+
if (!record4(value) || ![value.host_session_ref, value.host_turn_ref, value.host_source_ref].every((candidate) => typeof candidate === "string" && SAFE_REF4.test(candidate))) {
|
|
3367
|
+
throw new ClaudeHostError("storage", "Claude Host identity is malformed.");
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
function nonNegativeInteger(value) {
|
|
3371
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
3372
|
+
}
|
|
3373
|
+
function sameKeys(value, required, optional = []) {
|
|
3374
|
+
return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
|
|
3375
|
+
}
|
|
2418
3376
|
|
|
2419
3377
|
// host/claude/src/claude-producer.ts
|
|
2420
|
-
var STATE_SCHEMA = "ceal.claude_producer_state.v1";
|
|
2421
|
-
var SAFE_REF5 = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
2422
3378
|
var ClaudeProducer = class {
|
|
3379
|
+
producer_id = "claude";
|
|
3380
|
+
producer_label = "Claude";
|
|
3381
|
+
outbox_directory;
|
|
2423
3382
|
store;
|
|
2424
|
-
|
|
2425
|
-
installation;
|
|
2426
|
-
epoch;
|
|
2427
|
-
options;
|
|
2428
|
-
tail = Promise.resolve();
|
|
3383
|
+
core;
|
|
2429
3384
|
constructor(options) {
|
|
2430
|
-
if (!SAFE_REF5.test(options.producer_installation_ref)) throw new ClaudeHostError("configuration", "Claude producer installation identity is invalid.");
|
|
2431
|
-
this.options = options;
|
|
2432
|
-
this.epoch = options.producer_epoch ?? 1;
|
|
2433
|
-
if (!Number.isSafeInteger(this.epoch) || this.epoch <= 0) throw new ClaudeHostError("configuration", "Claude producer epoch is invalid.");
|
|
2434
3385
|
this.store = new ClaudeOutboxStore(options.outbox_dir);
|
|
2435
|
-
this.
|
|
2436
|
-
this.
|
|
3386
|
+
this.outbox_directory = this.store.directory;
|
|
3387
|
+
this.core = new HostProducerDrainCore(this, options);
|
|
2437
3388
|
}
|
|
2438
3389
|
drainOutbox() {
|
|
2439
|
-
|
|
2440
|
-
this.tail = run.then(() => void 0, () => void 0);
|
|
2441
|
-
return run;
|
|
3390
|
+
return this.core.drainOutbox();
|
|
2442
3391
|
}
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
3392
|
+
fail(code2, message) {
|
|
3393
|
+
return new ClaudeHostError(code2, message);
|
|
3394
|
+
}
|
|
3395
|
+
listEnvelopes() {
|
|
3396
|
+
return this.store.listEnvelopes();
|
|
3397
|
+
}
|
|
3398
|
+
removeEnvelope(envelope) {
|
|
3399
|
+
return this.store.removeEnvelope(envelope);
|
|
3400
|
+
}
|
|
3401
|
+
submissionRefOf(envelope) {
|
|
3402
|
+
return envelope.submission_ref;
|
|
3403
|
+
}
|
|
3404
|
+
expiresAtOf(envelope) {
|
|
3405
|
+
return envelope.expires_at;
|
|
3406
|
+
}
|
|
3407
|
+
createdAtOf(envelope) {
|
|
3408
|
+
return envelope.created_at;
|
|
3409
|
+
}
|
|
3410
|
+
async mapEnvelope(envelope) {
|
|
3411
|
+
const prompt = observation(envelope, "prompt");
|
|
3412
|
+
if (prompt !== void 0) return this.mapInput(envelope, prompt);
|
|
3413
|
+
return [mapTerminal(envelope)];
|
|
3414
|
+
}
|
|
3415
|
+
async mapInput(envelope, prompt) {
|
|
3416
|
+
const drafts = [];
|
|
3417
|
+
const session = observation(envelope, "session");
|
|
3418
|
+
if (session !== void 0) {
|
|
3419
|
+
const registrations = await this.store.listTranscriptRegistrations();
|
|
3420
|
+
const registration = registrations.find((item) => item.host_source_ref === envelope.identity.host_source_ref && item.host_session_ref === envelope.identity.host_session_ref);
|
|
3421
|
+
const directoryIdentity = registration?.cwd ?? envelope.identity.host_session_ref;
|
|
3422
|
+
drafts.push({
|
|
3423
|
+
frame_kind: "session_started",
|
|
3424
|
+
payload: compact({
|
|
3425
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3426
|
+
working_directory_ref: claudeWorkspaceRef(directoryIdentity),
|
|
3427
|
+
model: recordText(session.value, "model"),
|
|
3428
|
+
observations: [session]
|
|
3429
|
+
})
|
|
3430
|
+
});
|
|
3431
|
+
}
|
|
3432
|
+
const turn = observation(envelope, "turn");
|
|
3433
|
+
if (turn !== void 0) drafts.push({
|
|
3434
|
+
frame_kind: "turn_started",
|
|
3435
|
+
payload: compact({
|
|
3436
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3437
|
+
local_turn_ref: localTurnRef(envelope),
|
|
3438
|
+
observations: [turn]
|
|
3439
|
+
})
|
|
3440
|
+
});
|
|
3441
|
+
if (!isCealHostPromptObservationValue(prompt.value)) throw new ClaudeHostError("storage", "Claude prompt observation is malformed.");
|
|
3442
|
+
drafts.push({
|
|
3443
|
+
frame_kind: "user_input",
|
|
3444
|
+
payload: compact({
|
|
3445
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3446
|
+
local_turn_ref: localTurnRef(envelope),
|
|
3447
|
+
local_input_ref: envelope.host_input_ref,
|
|
3448
|
+
input_kind: envelope.input_kind,
|
|
3449
|
+
origin_input: envelope.origin_input,
|
|
3450
|
+
observations: [prompt],
|
|
3451
|
+
context_expires_at: envelope.expires_at
|
|
3452
|
+
})
|
|
3453
|
+
});
|
|
3454
|
+
return drafts;
|
|
3455
|
+
}
|
|
3456
|
+
};
|
|
3457
|
+
function mapTerminal(envelope) {
|
|
3458
|
+
const usage = envelope.observations.filter((item) => ["token", "cost"].includes(item.observation_kind));
|
|
3459
|
+
return {
|
|
3460
|
+
frame_kind: "turn_terminal",
|
|
3461
|
+
payload: compact({
|
|
3462
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3463
|
+
local_turn_ref: localTurnRef(envelope),
|
|
3464
|
+
terminal_state: "completed",
|
|
3465
|
+
usage: usage.length === 0 ? void 0 : Object.fromEntries(usage.map((item) => [item.observation_kind, item.value])),
|
|
3466
|
+
observations: envelope.observations
|
|
3467
|
+
})
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
function observation(envelope, kind) {
|
|
3471
|
+
return envelope.observations.find((item) => item.observation_kind === kind);
|
|
3472
|
+
}
|
|
3473
|
+
function localTurnRef(envelope) {
|
|
3474
|
+
return envelope.identity.host_turn_ref;
|
|
3475
|
+
}
|
|
3476
|
+
function compact(value) {
|
|
3477
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== void 0));
|
|
3478
|
+
}
|
|
3479
|
+
function asRecord(value) {
|
|
3480
|
+
return isRecord3(value) ? value : void 0;
|
|
3481
|
+
}
|
|
3482
|
+
function recordText(value, key) {
|
|
3483
|
+
const record5 = asRecord(value);
|
|
3484
|
+
return typeof record5?.[key] === "string" ? record5[key] : void 0;
|
|
3485
|
+
}
|
|
3486
|
+
function isRecord3(value) {
|
|
3487
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
// host/claude/src/claude-transcript-watcher.ts
|
|
3491
|
+
import { O_NOFOLLOW as O_NOFOLLOW2, O_RDONLY as O_RDONLY3 } from "node:constants";
|
|
3492
|
+
import { open as open5, readdir as readdir2 } from "node:fs/promises";
|
|
3493
|
+
import { join as join4 } from "node:path";
|
|
3494
|
+
var TOKEN_OBSERVATION_SOURCE = "claude_transcript.message.usage";
|
|
3495
|
+
var ClaudeTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
3496
|
+
store;
|
|
3497
|
+
registerCurrentPrompt;
|
|
3498
|
+
releaseCurrentPrompt;
|
|
3499
|
+
diagnostic;
|
|
3500
|
+
constructor(options) {
|
|
3501
|
+
super(options, {
|
|
3502
|
+
default_retention_ms: CLAUDE_DEFAULT_RETENTION_MS,
|
|
3503
|
+
invalid_outbox: () => new ClaudeHostError("configuration", "Claude transcript watcher outbox is invalid."),
|
|
3504
|
+
clock_failure: () => new ClaudeHostError("storage", "Claude watcher clock is invalid.")
|
|
3505
|
+
});
|
|
3506
|
+
this.store = new ClaudeOutboxStore(options.outbox_dir);
|
|
3507
|
+
this.registerCurrentPrompt = options.register_current_prompt;
|
|
3508
|
+
this.releaseCurrentPrompt = options.release_current_prompt;
|
|
3509
|
+
this.diagnostic = options.diagnostic ?? (() => void 0);
|
|
3510
|
+
}
|
|
3511
|
+
transcriptPathOf(registration) {
|
|
3512
|
+
return registration.transcript_path;
|
|
3513
|
+
}
|
|
3514
|
+
async listRegistrations() {
|
|
3515
|
+
return this.store.listTranscriptRegistrations();
|
|
3516
|
+
}
|
|
3517
|
+
async scanOpened(handle, registration) {
|
|
3518
|
+
const { size, device, inode } = await ownedClaudeTranscriptFile(handle);
|
|
3519
|
+
const cursor = await this.preflight(registration, handle, size, device, inode);
|
|
3520
|
+
if (cursor === null) return { records: 0, enqueued: 0, quarantined: true };
|
|
3521
|
+
if (cursor.status === "quarantined") {
|
|
3522
|
+
if (cursor.reason !== "transcript_malformed") await this.releaseStoredPromptBestEffort(registration.host_session_ref, cursor);
|
|
3523
|
+
return { records: 0, enqueued: 0, quarantined: true };
|
|
3524
|
+
}
|
|
3525
|
+
const draft = mutableCursor(cursor);
|
|
3526
|
+
await this.scanOpenedTranscript(registration, handle, draft, size, device, inode);
|
|
3527
|
+
return { records: draft.consumed, enqueued: draft.enqueued, quarantined: false };
|
|
3528
|
+
}
|
|
3529
|
+
async scanFailed(handle, registration, error) {
|
|
3530
|
+
this.diagnostic("watcher_scan_failed", claudeTranscriptScanFailureKind(error), claudeTranscriptScanCause(handle, error));
|
|
3531
|
+
if (handle === void 0 && isSymlinkTranscript(error)) return await this.quarantineUnopened(registration, "transcript_malformed");
|
|
3532
|
+
if (error instanceof ClaudeHostError && error.code === "payload_rejected") return await this.quarantineDraft(await this.preservedCursor(registration));
|
|
3533
|
+
await this.quarantineFailure(registration, handle);
|
|
3534
|
+
return { records: 0, enqueued: 0, quarantined: true };
|
|
3535
|
+
}
|
|
3536
|
+
async retireMissing(registration) {
|
|
3537
|
+
await this.releaseStoredPromptBestEffort(registration.host_session_ref, await this.store.readTranscriptCursor(registration).catch(() => null));
|
|
3538
|
+
return this.retireMissingTranscript(registration.transcript_path, "Claude", () => this.store.retireTranscriptRegistration(registration));
|
|
3539
|
+
}
|
|
3540
|
+
initialCursor(registration) {
|
|
3541
|
+
return {
|
|
3542
|
+
schema_version: CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION,
|
|
3543
|
+
host_source_ref: registration.host_source_ref,
|
|
3544
|
+
host_session_ref: registration.host_session_ref,
|
|
3545
|
+
transcript_path: registration.transcript_path,
|
|
3546
|
+
offset: 0,
|
|
3547
|
+
anchor_start: 0,
|
|
3548
|
+
anchor_sha256: this.sha(Buffer.alloc(0)),
|
|
3549
|
+
subagents: {},
|
|
3550
|
+
status: "active",
|
|
3551
|
+
updated_at: this.stamp()
|
|
3552
|
+
};
|
|
3553
|
+
}
|
|
3554
|
+
async scanOpenedTranscript(registration, handle, draft, size, device, inode) {
|
|
3555
|
+
const main = await this.readChunk(handle, draft.offset, size);
|
|
3556
|
+
if (main !== "empty" && main !== "partial") {
|
|
3557
|
+
await this.consumeMainChunk(registration, main, draft);
|
|
3558
|
+
draft.offset += main.length;
|
|
3559
|
+
}
|
|
3560
|
+
draft.consumed += await this.consumeSubagents(registration, draft);
|
|
3561
|
+
await this.emitOpenTerminal(registration, draft);
|
|
3562
|
+
await this.commitCursor(registration, handle, draft, device, inode);
|
|
3563
|
+
}
|
|
3564
|
+
async preflight(registration, handle, size, device, inode) {
|
|
3565
|
+
const existing = await this.store.readTranscriptCursor(registration);
|
|
3566
|
+
if (existing?.status === "quarantined") return existing;
|
|
3567
|
+
const cursor = existing ?? this.initialCursor(registration);
|
|
3568
|
+
if (cursor.device === void 0) {
|
|
3569
|
+
if (cursor.offset !== 0) return await this.quarantineStored(cursor, device, inode, "transcript_replaced");
|
|
3570
|
+
return { ...cursor, device, inode };
|
|
3571
|
+
}
|
|
3572
|
+
const reason = this.cursorIdentityReason(cursor, registration.transcript_path, device, inode) ?? await this.cursorBoundsReason(handle, cursor, size);
|
|
3573
|
+
if (reason !== null) return await this.quarantineStored(cursor, device, inode, reason);
|
|
3574
|
+
return cursor;
|
|
3575
|
+
}
|
|
3576
|
+
async consumeMainChunk(registration, complete, draft) {
|
|
3577
|
+
for (const line of complete.toString("utf8").split("\n").filter((value) => value.length > 0)) {
|
|
3578
|
+
draft.consumed += 1;
|
|
3579
|
+
const record5 = parseTranscriptLine(line);
|
|
3580
|
+
if (record5 === null) throw new ClaudeHostError("payload_rejected", "Claude transcript line is not valid JSON.", "malformed_line");
|
|
3581
|
+
if (isUserTurnLine(record5)) await this.openTurn(registration, draft, record5);
|
|
3582
|
+
else if (isAssistantUsageLine(record5)) this.accumulateUsage(draft, toUsage(record5.uuid, record5.timestamp, record5.message.usage));
|
|
3583
|
+
else if (isAssistantLine(record5) && record5.message.usage !== void 0) throw new ClaudeHostError("payload_rejected", "Claude transcript usage is malformed.");
|
|
3584
|
+
if (isAssistantTerminalLine(record5)) await this.releaseOpenTurn(registration, draft);
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
async openTurn(registration, draft, record5) {
|
|
3588
|
+
await this.closeTurn(registration, draft, record5.timestamp);
|
|
3589
|
+
const sessionRef = registration.host_session_ref;
|
|
3590
|
+
const turnRef = claudeLocalTurnRef(registration.claude_session_id, record5.uuid);
|
|
3591
|
+
const inputRef = claudeLocalInputRef(registration.claude_session_id, record5.uuid);
|
|
3592
|
+
const promptRef = hostProducerPromptRef(this.sha(record5.message.content));
|
|
3593
|
+
const identity = { host_source_ref: registration.host_source_ref, host_session_ref: sessionRef, host_turn_ref: turnRef };
|
|
3594
|
+
const observations = [
|
|
3595
|
+
{ 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) } },
|
|
3596
|
+
{ 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 } }
|
|
3597
|
+
];
|
|
3598
|
+
if (!draft.session_observed) {
|
|
3599
|
+
const session = await this.store.readSession(registration.host_source_ref, sessionRef);
|
|
3600
|
+
if (session !== null) observations.push({ observation_ref: observationRef("transcript_session", sessionRef, session.value), observation_kind: "session", observed_at: record5.timestamp, value: session.value });
|
|
3601
|
+
draft.session_observed = true;
|
|
3602
|
+
}
|
|
3603
|
+
await this.persistOpenedTurnContext(registration, draft, record5, inputRef, turnRef, promptRef, identity, observations);
|
|
3604
|
+
}
|
|
3605
|
+
async persistOpenedTurnContext(registration, draft, record5, inputRef, turnRef, promptRef, identity, observations) {
|
|
3606
|
+
const sessionRef = registration.host_session_ref;
|
|
3607
|
+
const expiresAt = this.expire(this.outboxRetentionMs);
|
|
3608
|
+
const custodyRegistered = await this.registerPrompt(sessionRef, turnRef, promptRef, record5.message.content, expiresAt);
|
|
3609
|
+
if (custodyRegistered) this.diagnostic("watcher_turn_observed");
|
|
3610
|
+
try {
|
|
3611
|
+
await this.store.putEnvelope({
|
|
3612
|
+
schema_version: "ceal.claude_observation_outbox.v1",
|
|
3613
|
+
identity,
|
|
3614
|
+
host_input_ref: inputRef,
|
|
3615
|
+
input_kind: "human",
|
|
3616
|
+
prompt_ref: promptRef,
|
|
3617
|
+
observations,
|
|
3618
|
+
origin_input: true,
|
|
3619
|
+
submission_ref: submissionRef("input", turnRef, inputRef, promptRef),
|
|
3620
|
+
created_at: record5.timestamp,
|
|
3621
|
+
expires_at: expiresAt
|
|
3622
|
+
});
|
|
3623
|
+
draft.enqueued += 1;
|
|
3624
|
+
await this.activateOpenedTurnContext(identity, promptRef, expiresAt);
|
|
3625
|
+
this.setOpenedTurn(draft, record5, inputRef, turnRef, promptRef);
|
|
3626
|
+
} catch (error) {
|
|
3627
|
+
if (custodyRegistered) await this.releasePrompt(sessionRef, { turn_key: turnRef, prompt_ref: promptRef }).catch(() => void 0);
|
|
3628
|
+
throw error;
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
async activateOpenedTurnContext(identity, promptRef, expiresAt) {
|
|
3632
|
+
const selection = await this.store.readInvocationTurnSelection(identity.host_source_ref, identity.host_session_ref);
|
|
3633
|
+
const turnGenerationRef = selection?.prompt_ref === promptRef ? selection.turn_generation_ref : void 0;
|
|
3634
|
+
await this.activateContext(identity, promptRef, turnGenerationRef, expiresAt);
|
|
3635
|
+
}
|
|
3636
|
+
setOpenedTurn(draft, record5, inputRef, turnRef, promptRef) {
|
|
3637
|
+
const folded = foldUnattributed(draft.unattributed);
|
|
3638
|
+
draft.open_turn = {
|
|
3639
|
+
turn_key: turnRef,
|
|
3640
|
+
user_uuid: record5.uuid,
|
|
3641
|
+
prompt_ref: promptRef,
|
|
3642
|
+
host_input_ref: inputRef,
|
|
3643
|
+
started_at: record5.timestamp,
|
|
3644
|
+
last_observed_at: folded?.last_observed_at ?? record5.timestamp,
|
|
3645
|
+
message_ids: folded?.message_ids ?? [],
|
|
3646
|
+
counts: folded?.counts ?? zeroCounts()
|
|
3647
|
+
};
|
|
3648
|
+
draft.unattributed = void 0;
|
|
3649
|
+
}
|
|
3650
|
+
async closeTurn(registration, draft, at) {
|
|
3651
|
+
const turn = draft.open_turn;
|
|
3652
|
+
draft.open_turn = void 0;
|
|
3653
|
+
if (turn === void 0) return;
|
|
3654
|
+
const digest2 = terminalDigest(turn);
|
|
3655
|
+
if (digest2 !== turn.last_terminal_digest) {
|
|
3656
|
+
await this.putTerminalEnvelope(registration, turn, digest2, turn.message_ids.length === 0 ? at : void 0);
|
|
3657
|
+
draft.enqueued += 1;
|
|
3658
|
+
}
|
|
3659
|
+
await this.releasePrompt(registration.host_session_ref, turn);
|
|
3660
|
+
}
|
|
3661
|
+
async releaseOpenTurn(registration, draft) {
|
|
3662
|
+
if (draft.open_turn !== void 0) await this.releasePrompt(registration.host_session_ref, draft.open_turn);
|
|
3663
|
+
}
|
|
3664
|
+
async releasePrompt(sessionRef, turn) {
|
|
3665
|
+
if (this.producerInstallationRef === void 0 || this.releaseCurrentPrompt === void 0) return;
|
|
3666
|
+
await this.releaseCurrentPrompt({ producer_id: "claude", producer_installation_ref: this.producerInstallationRef, local_session_ref: sessionRef, local_turn_ref: turn.turn_key, prompt_ref: turn.prompt_ref });
|
|
3667
|
+
}
|
|
3668
|
+
accumulateUsage(draft, usage) {
|
|
3669
|
+
const turn = draft.open_turn;
|
|
3670
|
+
if (turn === void 0) {
|
|
3671
|
+
draft.unattributed = accumulateUnattributed(draft.unattributed, usage);
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
if (turn.message_ids.includes(usage.uuid)) return;
|
|
3675
|
+
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");
|
|
3676
|
+
draft.open_turn = {
|
|
3677
|
+
...turn,
|
|
3678
|
+
last_observed_at: usage.observed_at > turn.last_observed_at ? usage.observed_at : turn.last_observed_at,
|
|
3679
|
+
message_ids: [...turn.message_ids, usage.uuid],
|
|
3680
|
+
counts: addCounts(turn.counts, usage)
|
|
3681
|
+
};
|
|
3682
|
+
}
|
|
3683
|
+
/**
|
|
3684
|
+
* Subagent transcripts live beside the session transcript at
|
|
3685
|
+
* `<session-id>/subagents/*.jsonl` with a `.meta.json` sibling the scan
|
|
3686
|
+
* ignores. Their usage belongs to the turn open when the scan runs: a
|
|
3687
|
+
* subagent only runs while its parent turn is in flight. A straggler line
|
|
3688
|
+
* that lands after the next user message therefore attributes to the open
|
|
3689
|
+
* turn, never to a closed one.
|
|
3690
|
+
*/
|
|
3691
|
+
async consumeSubagents(registration, draft) {
|
|
3692
|
+
const directory = subagentsDirectory(registration.transcript_path);
|
|
3693
|
+
let names;
|
|
3694
|
+
try {
|
|
3695
|
+
names = (await readdir2(directory)).filter((name) => name.endsWith(".jsonl") && !name.endsWith(".meta.json")).sort();
|
|
3696
|
+
} catch (error) {
|
|
3697
|
+
if (isMissingTranscriptError(error) || isNotDirectory(error)) return 0;
|
|
3698
|
+
throw new ClaudeHostError("payload_rejected", "Claude subagent transcript directory is unavailable.");
|
|
3699
|
+
}
|
|
3700
|
+
const listed = new Set(names);
|
|
3701
|
+
for (const name of Object.keys(draft.subagents)) if (!listed.has(name)) delete draft.subagents[name];
|
|
3702
|
+
if (names.length > CLAUDE_TRANSCRIPT_MAX_SUBAGENT_FILES) {
|
|
3703
|
+
this.log(`Claude ignored subagent transcripts past its file bound: transcript_path=${JSON.stringify(registration.transcript_path)}, files=${names.length}.`);
|
|
3704
|
+
names = names.slice(0, CLAUDE_TRANSCRIPT_MAX_SUBAGENT_FILES);
|
|
3705
|
+
}
|
|
3706
|
+
let consumed = 0;
|
|
3707
|
+
for (const name of names) {
|
|
3708
|
+
consumed += await this.consumeSubagentFile(draft, directory, name);
|
|
3709
|
+
}
|
|
3710
|
+
return consumed;
|
|
3711
|
+
}
|
|
3712
|
+
async consumeSubagentFile(draft, directory, name) {
|
|
3713
|
+
const path = join4(directory, name);
|
|
3714
|
+
let handle;
|
|
3715
|
+
try {
|
|
3716
|
+
handle = await open5(path, O_RDONLY3 | O_NOFOLLOW2);
|
|
3717
|
+
const { size, device, inode } = await ownedClaudeTranscriptFile(handle);
|
|
3718
|
+
const entry = draft.subagents[name] ?? { offset: 0, anchor_start: 0, anchor_sha256: this.sha(Buffer.alloc(0)) };
|
|
3719
|
+
const pinned = await this.preflightSubagent(entry, size, device, inode);
|
|
3720
|
+
const chunk = await this.readChunk(handle, pinned.offset, size);
|
|
3721
|
+
if (chunk === "empty" || chunk === "partial") {
|
|
3722
|
+
draft.subagents[name] = pinned;
|
|
3723
|
+
return 0;
|
|
2456
3724
|
}
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
3725
|
+
const consumed = this.consumeSubagentLines(draft, chunk);
|
|
3726
|
+
const offset = pinned.offset + chunk.length;
|
|
3727
|
+
const anchor = await this.readAnchor(handle, offset, CLAUDE_TRANSCRIPT_ANCHOR_BYTES);
|
|
3728
|
+
draft.subagents[name] = { device, inode, offset, anchor_start: anchor.start, anchor_sha256: anchor.sha256 };
|
|
3729
|
+
return consumed;
|
|
3730
|
+
} catch (error) {
|
|
3731
|
+
if (isMissingTranscriptError(error)) {
|
|
3732
|
+
delete draft.subagents[name];
|
|
3733
|
+
return 0;
|
|
2460
3734
|
}
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
3735
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("payload_rejected", "Claude subagent transcript is unavailable.");
|
|
3736
|
+
} finally {
|
|
3737
|
+
await handle?.close();
|
|
2464
3738
|
}
|
|
2465
|
-
return { admitted, retained: retained + Math.max(0, read.entries.length - admitted - retained), malformed: read.malformed };
|
|
2466
3739
|
}
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
const
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
3740
|
+
consumeSubagentLines(draft, chunk) {
|
|
3741
|
+
let consumed = 0;
|
|
3742
|
+
for (const line of chunk.toString("utf8").split("\n").filter((value) => value.length > 0)) {
|
|
3743
|
+
consumed += 1;
|
|
3744
|
+
const record5 = parseTranscriptLine(line);
|
|
3745
|
+
if (record5 === null) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript line is not valid JSON.", "malformed_line");
|
|
3746
|
+
if (isAssistantUsageLine(record5)) this.accumulateUsage(draft, toUsage(record5.uuid, record5.timestamp, record5.message.usage));
|
|
3747
|
+
else if (isAssistantLine(record5) && record5.message.usage !== void 0) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript usage is malformed.");
|
|
3748
|
+
}
|
|
3749
|
+
return consumed;
|
|
2474
3750
|
}
|
|
2475
|
-
async
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
3751
|
+
async preflightSubagent(entry, size, device, inode) {
|
|
3752
|
+
if (entry.device === void 0) {
|
|
3753
|
+
if (entry.offset !== 0) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript cursor is unpinned past its start.");
|
|
3754
|
+
return { device, inode, offset: 0, anchor_start: 0, anchor_sha256: this.sha(Buffer.alloc(0)) };
|
|
3755
|
+
}
|
|
3756
|
+
if (entry.device !== device || entry.inode !== inode) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript was replaced.");
|
|
3757
|
+
if (size < entry.offset) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript was truncated.");
|
|
3758
|
+
return { device, inode, offset: entry.offset, anchor_start: entry.anchor_start, anchor_sha256: entry.anchor_sha256 };
|
|
2481
3759
|
}
|
|
2482
|
-
|
|
3760
|
+
/**
|
|
3761
|
+
* The open turn's terminal is emitted optimistically on every scan whose
|
|
3762
|
+
* new lines changed its totals, so usage is visible before the next user
|
|
3763
|
+
* message closes the turn. The emission digest changes with the content,
|
|
3764
|
+
* so a repeated scan over unchanged state emits nothing.
|
|
3765
|
+
*/
|
|
3766
|
+
async emitOpenTerminal(registration, draft) {
|
|
3767
|
+
const turn = draft.open_turn;
|
|
3768
|
+
if (turn === void 0 || turn.message_ids.length === 0) return;
|
|
3769
|
+
const digest2 = terminalDigest(turn);
|
|
3770
|
+
if (digest2 === turn.last_terminal_digest) return;
|
|
3771
|
+
await this.putTerminalEnvelope(registration, turn, digest2, void 0);
|
|
3772
|
+
draft.enqueued += 1;
|
|
3773
|
+
draft.open_turn = { ...turn, last_terminal_digest: digest2 };
|
|
3774
|
+
}
|
|
3775
|
+
async putTerminalEnvelope(registration, turn, digest2, gapAt) {
|
|
3776
|
+
const observedAt = gapAt ?? turn.last_observed_at;
|
|
3777
|
+
const tokenValue = turn.message_ids.length === 0 ? hostObservationNotRecorded("claude_turn_usage_not_observed") : availableTokenValue(turn.counts);
|
|
3778
|
+
const identity = { host_source_ref: registration.host_source_ref, host_session_ref: registration.host_session_ref, host_turn_ref: turn.turn_key };
|
|
3779
|
+
await this.store.putEnvelope({
|
|
3780
|
+
schema_version: "ceal.claude_observation_outbox.v1",
|
|
3781
|
+
identity,
|
|
3782
|
+
host_input_ref: turn.host_input_ref,
|
|
3783
|
+
input_kind: "host_automatic",
|
|
3784
|
+
prompt_ref: turn.prompt_ref,
|
|
3785
|
+
observations: [
|
|
3786
|
+
{ 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 } },
|
|
3787
|
+
{ observation_ref: observationRef("transcript_token", turn.turn_key, digest2), observation_kind: "token", observed_at: observedAt, value: tokenValue },
|
|
3788
|
+
{ observation_ref: observationRef("transcript_cost", turn.turn_key, digest2), observation_kind: "cost", observed_at: observedAt, value: hostObservationNotRecorded("provider_cost_not_observed") }
|
|
3789
|
+
],
|
|
3790
|
+
origin_input: false,
|
|
3791
|
+
submission_ref: submissionRef("terminal", turn.turn_key, digest2, turn.prompt_ref),
|
|
3792
|
+
created_at: observedAt,
|
|
3793
|
+
expires_at: this.expire(this.outboxRetentionMs)
|
|
3794
|
+
});
|
|
3795
|
+
}
|
|
3796
|
+
async commitCursor(registration, handle, draft, device, inode) {
|
|
3797
|
+
const anchor = await this.readAnchor(handle, draft.offset, CLAUDE_TRANSCRIPT_ANCHOR_BYTES);
|
|
3798
|
+
await this.store.writeTranscriptCursor({
|
|
3799
|
+
schema_version: CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION,
|
|
3800
|
+
host_source_ref: registration.host_source_ref,
|
|
3801
|
+
host_session_ref: registration.host_session_ref,
|
|
3802
|
+
transcript_path: registration.transcript_path,
|
|
3803
|
+
device,
|
|
3804
|
+
inode,
|
|
3805
|
+
offset: draft.offset,
|
|
3806
|
+
anchor_start: anchor.start,
|
|
3807
|
+
anchor_sha256: anchor.sha256,
|
|
3808
|
+
subagents: draft.subagents,
|
|
3809
|
+
...draft.open_turn === void 0 ? {} : { open_turn: draft.open_turn },
|
|
3810
|
+
...draft.unattributed === void 0 ? {} : { unattributed: draft.unattributed },
|
|
3811
|
+
...draft.session_observed ? { session_observed: true } : {},
|
|
3812
|
+
status: "active",
|
|
3813
|
+
updated_at: this.stamp()
|
|
3814
|
+
});
|
|
3815
|
+
}
|
|
3816
|
+
async registerPrompt(sessionRef, turnRef, promptRef, prompt, expiresAt) {
|
|
3817
|
+
if (this.producerInstallationRef === void 0 || this.registerCurrentPrompt === void 0) return false;
|
|
3818
|
+
const register = this.registerCurrentPrompt;
|
|
3819
|
+
const installationRef = this.producerInstallationRef;
|
|
3820
|
+
return registerHostCurrentPromptDiagnosed(
|
|
3821
|
+
() => register({
|
|
3822
|
+
producer_id: "claude",
|
|
3823
|
+
producer_installation_ref: installationRef,
|
|
3824
|
+
local_session_ref: sessionRef,
|
|
3825
|
+
local_turn_ref: turnRef,
|
|
3826
|
+
prompt_ref: promptRef,
|
|
3827
|
+
current_prompt: prompt,
|
|
3828
|
+
context_expires_at: expiresAt
|
|
3829
|
+
}),
|
|
3830
|
+
(event) => this.diagnostic(event)
|
|
3831
|
+
);
|
|
3832
|
+
}
|
|
3833
|
+
async releaseStoredPromptBestEffort(sessionRef, cursor) {
|
|
3834
|
+
await this.releaseOpenTurnBestEffort(sessionRef, cursor?.open_turn);
|
|
3835
|
+
}
|
|
3836
|
+
async releaseOpenTurnBestEffort(sessionRef, turn) {
|
|
3837
|
+
if (turn === void 0) return;
|
|
3838
|
+
await this.releasePrompt(sessionRef, turn).catch(() => void 0);
|
|
3839
|
+
}
|
|
3840
|
+
async activateContext(identity, promptRef, turnGenerationRef, expiresAt) {
|
|
2483
3841
|
try {
|
|
2484
|
-
|
|
3842
|
+
await this.store.activateInvocationContext(identity, promptRef, expiresAt, void 0, turnGenerationRef);
|
|
3843
|
+
this.diagnostic("activation_recorded");
|
|
2485
3844
|
} catch (error) {
|
|
2486
|
-
|
|
2487
|
-
|
|
3845
|
+
this.diagnostic("activation_failed");
|
|
3846
|
+
throw error;
|
|
2488
3847
|
}
|
|
2489
3848
|
}
|
|
2490
|
-
async
|
|
2491
|
-
await
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
3849
|
+
async quarantineStored(cursor, device, inode, reason) {
|
|
3850
|
+
await this.releaseStoredPromptBestEffort(cursor.host_session_ref, cursor);
|
|
3851
|
+
await this.store.writeTranscriptCursor({ ...cursor, device, inode, status: "quarantined", reason, updated_at: this.stamp() });
|
|
3852
|
+
return null;
|
|
3853
|
+
}
|
|
3854
|
+
async quarantineUnopened(registration, reason) {
|
|
3855
|
+
const existing = await this.store.readTranscriptCursor(registration).catch(() => null);
|
|
3856
|
+
await this.store.writeTranscriptCursor({ ...existing ?? this.initialCursor(registration), transcript_path: registration.transcript_path, status: "quarantined", reason, updated_at: this.stamp() });
|
|
3857
|
+
return { records: 0, enqueued: 0, quarantined: true };
|
|
3858
|
+
}
|
|
3859
|
+
async quarantineDraft(cursor) {
|
|
3860
|
+
await this.store.writeTranscriptCursor({ ...cursor, status: "quarantined", reason: "transcript_malformed", updated_at: this.stamp() });
|
|
3861
|
+
return { records: 0, enqueued: 0, quarantined: true };
|
|
3862
|
+
}
|
|
3863
|
+
async quarantineFailure(registration, handle) {
|
|
3864
|
+
if (handle === void 0) return;
|
|
3865
|
+
const stats = await handle.stat().catch(() => null);
|
|
3866
|
+
if (stats === null) return;
|
|
3867
|
+
const existing = await this.store.readTranscriptCursor(registration).catch(() => null);
|
|
3868
|
+
const cursor = existing ?? this.initialCursor(registration);
|
|
3869
|
+
await this.store.writeTranscriptCursor({ ...cursor, device: String(stats.dev), inode: String(stats.ino), status: "quarantined", reason: "transcript_malformed", updated_at: this.stamp() });
|
|
3870
|
+
}
|
|
3871
|
+
async preservedCursor(registration) {
|
|
3872
|
+
return await this.store.readTranscriptCursor(registration).catch(() => null) ?? this.initialCursor(registration);
|
|
2495
3873
|
}
|
|
2496
3874
|
};
|
|
2497
|
-
function
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
}
|
|
2508
|
-
return { schema_version: STATE_SCHEMA, producer_installation_ref: installation, producer_epoch: epoch, next_sequence: Number(value.next_sequence), pending };
|
|
2509
|
-
}
|
|
2510
|
-
function validStateHeader(value, installation) {
|
|
2511
|
-
return record5(value) && value.schema_version === STATE_SCHEMA && value.producer_installation_ref === installation && positiveInteger(value.producer_epoch) && positiveInteger(value.next_sequence) && record5(value.pending);
|
|
2512
|
-
}
|
|
2513
|
-
function positiveInteger(value) {
|
|
2514
|
-
return Number.isSafeInteger(value) && Number(value) > 0;
|
|
3875
|
+
function mutableCursor(cursor) {
|
|
3876
|
+
return {
|
|
3877
|
+
offset: cursor.offset,
|
|
3878
|
+
...cursor.open_turn === void 0 ? {} : { open_turn: cursor.open_turn },
|
|
3879
|
+
...cursor.unattributed === void 0 ? {} : { unattributed: cursor.unattributed },
|
|
3880
|
+
subagents: Object.fromEntries(Object.entries(cursor.subagents).map(([name, entry]) => [name, { ...entry }])),
|
|
3881
|
+
session_observed: cursor.session_observed ?? false,
|
|
3882
|
+
consumed: 0,
|
|
3883
|
+
enqueued: 0
|
|
3884
|
+
};
|
|
2515
3885
|
}
|
|
2516
|
-
function
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
3886
|
+
function zeroCounts() {
|
|
3887
|
+
return { input: 0, cached_input: 0, cache_write_input: 0, output: 0 };
|
|
3888
|
+
}
|
|
3889
|
+
function terminalDigest(turn) {
|
|
3890
|
+
return transcriptObjectSha256({ turn: turn.turn_key, messages: turn.message_ids, counts: turn.counts });
|
|
3891
|
+
}
|
|
3892
|
+
function availableTokenValue(counts) {
|
|
3893
|
+
const input = counts.input;
|
|
3894
|
+
const output = counts.output;
|
|
3895
|
+
if (!Number.isSafeInteger(input + output)) throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
3896
|
+
return createHostTokenObservationValue({
|
|
3897
|
+
input,
|
|
3898
|
+
output,
|
|
3899
|
+
total: input + output,
|
|
3900
|
+
cached_input: counts.cached_input,
|
|
3901
|
+
cache_write_input: counts.cache_write_input
|
|
3902
|
+
}, TOKEN_OBSERVATION_SOURCE);
|
|
3903
|
+
}
|
|
3904
|
+
function addCounts(current, next) {
|
|
3905
|
+
const counts = {
|
|
3906
|
+
input: current.input + next.input,
|
|
3907
|
+
cached_input: current.cached_input + next.cached_input,
|
|
3908
|
+
cache_write_input: current.cache_write_input + next.cache_write_input,
|
|
3909
|
+
output: current.output + next.output
|
|
3910
|
+
};
|
|
3911
|
+
if (![counts.input, counts.cached_input, counts.cache_write_input, counts.output].every((value) => Number.isSafeInteger(value) && value >= 0)) {
|
|
3912
|
+
throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
3913
|
+
}
|
|
3914
|
+
return counts;
|
|
2524
3915
|
}
|
|
2525
|
-
function
|
|
2526
|
-
|
|
3916
|
+
function accumulateUnattributed(current, usage) {
|
|
3917
|
+
const base = current ?? { input: 0, cached_input: 0, cache_write_input: 0, output: 0, message_ids: [], last_observed_at: usage.observed_at };
|
|
3918
|
+
if (base.message_ids.includes(usage.uuid)) return base;
|
|
3919
|
+
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");
|
|
3920
|
+
const counts = addCounts(base, usage);
|
|
3921
|
+
return {
|
|
3922
|
+
...counts,
|
|
3923
|
+
message_ids: [...base.message_ids, usage.uuid],
|
|
3924
|
+
last_observed_at: usage.observed_at > base.last_observed_at ? usage.observed_at : base.last_observed_at
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3927
|
+
function foldUnattributed(bucket) {
|
|
3928
|
+
if (bucket === void 0) return void 0;
|
|
3929
|
+
if (![bucket.input, bucket.cached_input, bucket.cache_write_input, bucket.output].every((value) => Number.isSafeInteger(value) && value >= 0)) {
|
|
3930
|
+
throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
3931
|
+
}
|
|
3932
|
+
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 };
|
|
3933
|
+
}
|
|
3934
|
+
function toUsage(uuid, observedAt, usage) {
|
|
3935
|
+
const input = usageNumber(usage.input_tokens);
|
|
3936
|
+
const cached = usageNumber(usage.cache_read_input_tokens ?? 0);
|
|
3937
|
+
const created = usageNumber(usage.cache_creation_input_tokens ?? 0);
|
|
3938
|
+
const output = usageNumber(usage.output_tokens);
|
|
3939
|
+
if (input === null || cached === null || created === null || output === null) throw new ClaudeHostError("payload_rejected", "Claude transcript usage is malformed.");
|
|
3940
|
+
const total = input + cached + created;
|
|
3941
|
+
if (![total, output].every((value) => Number.isSafeInteger(value) && value >= 0)) throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
3942
|
+
return { uuid, observed_at: observedAt, input: total, cached_input: cached, cache_write_input: created, output };
|
|
3943
|
+
}
|
|
3944
|
+
function usageNumber(value) {
|
|
3945
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
3946
|
+
}
|
|
3947
|
+
function parseTranscriptLine(line) {
|
|
3948
|
+
try {
|
|
3949
|
+
return JSON.parse(line);
|
|
3950
|
+
} catch {
|
|
3951
|
+
return null;
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
function lineIdentity(record5) {
|
|
3955
|
+
if (typeof record5.uuid !== "string" || record5.uuid.length === 0 || record5.uuid.length > 256) return null;
|
|
3956
|
+
if (typeof record5.timestamp !== "string" || !Number.isFinite(Date.parse(record5.timestamp))) return null;
|
|
3957
|
+
return { uuid: record5.uuid, timestamp: record5.timestamp };
|
|
3958
|
+
}
|
|
3959
|
+
function isUserTurnLine(record5) {
|
|
3960
|
+
if (!isTranscriptRecord(record5) || record5.type !== "user") return false;
|
|
3961
|
+
if (!isTranscriptRecord(record5.message) || typeof record5.message.content !== "string") return false;
|
|
3962
|
+
if (record5.isMeta === true && (!isTranscriptRecord(record5.origin) || typeof record5.origin.kind !== "string")) return false;
|
|
3963
|
+
const identity = lineIdentity(record5);
|
|
3964
|
+
if (identity === null) throw new ClaudeHostError("payload_rejected", "Claude transcript user line is malformed.");
|
|
3965
|
+
return true;
|
|
3966
|
+
}
|
|
3967
|
+
function isAssistantLine(record5) {
|
|
3968
|
+
return isTranscriptRecord(record5) && record5.type === "assistant" && isTranscriptRecord(record5.message);
|
|
3969
|
+
}
|
|
3970
|
+
function isAssistantTerminalLine(record5) {
|
|
3971
|
+
return isAssistantLine(record5) && (record5.message.stop_reason === "end_turn" || record5.message.stop_reason === "stop_sequence");
|
|
3972
|
+
}
|
|
3973
|
+
function isAssistantUsageLine(record5) {
|
|
3974
|
+
if (!isTranscriptRecord(record5)) return false;
|
|
3975
|
+
if (!isAssistantLine(record5) || record5.message.usage === void 0) return false;
|
|
3976
|
+
const identity = lineIdentity(record5);
|
|
3977
|
+
if (identity === null || !isTranscriptRecord(record5.message.usage)) throw new ClaudeHostError("payload_rejected", "Claude transcript usage line is malformed.");
|
|
3978
|
+
return true;
|
|
3979
|
+
}
|
|
3980
|
+
function subagentsDirectory(transcriptPath) {
|
|
3981
|
+
const stem = transcriptPath.endsWith(".jsonl") ? transcriptPath.slice(0, -".jsonl".length) : transcriptPath;
|
|
3982
|
+
return join4(stem, "subagents");
|
|
2527
3983
|
}
|
|
2528
|
-
function
|
|
2529
|
-
return
|
|
3984
|
+
function observationRef(event, identity, value) {
|
|
3985
|
+
return cealHostObservationRef("claude", transcriptObjectSha256({ event, identity, value }));
|
|
3986
|
+
}
|
|
3987
|
+
function submissionRef(kind, turn, digest2, prompt) {
|
|
3988
|
+
return `claude-submission:${transcriptObjectSha256({ kind, turn, digest: digest2, prompt })}`;
|
|
3989
|
+
}
|
|
3990
|
+
async function ownedClaudeTranscriptFile(handle) {
|
|
3991
|
+
const stats = await handle.stat();
|
|
3992
|
+
const uid = typeof process.geteuid === "function" ? process.geteuid() : stats.uid;
|
|
3993
|
+
if (!stats.isFile()) throw new ClaudeHostError("payload_rejected", "Claude transcript source is not a regular file.");
|
|
3994
|
+
if (stats.uid !== uid) throw new ClaudeHostError("payload_rejected", "Claude transcript source is owned by another user.");
|
|
3995
|
+
return { size: stats.size, device: String(stats.dev), inode: String(stats.ino) };
|
|
3996
|
+
}
|
|
3997
|
+
function isNotDirectory(error) {
|
|
3998
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOTDIR";
|
|
3999
|
+
}
|
|
4000
|
+
function claudeTranscriptScanCause(handle, error) {
|
|
4001
|
+
if (error instanceof ClaudeHostError) return error.diagnostic_kind ?? error.code;
|
|
4002
|
+
const code2 = transcriptErrorCode(error);
|
|
4003
|
+
if (code2 !== null) return `${handle === void 0 ? "open" : "scan"}:${code2}`;
|
|
4004
|
+
return `${handle === void 0 ? "open" : "scan"}:unknown`;
|
|
4005
|
+
}
|
|
4006
|
+
function claudeTranscriptScanFailureKind(error) {
|
|
4007
|
+
return transcriptScanFailureKind(error, (candidate) => {
|
|
4008
|
+
if (!(candidate instanceof ClaudeHostError)) return void 0;
|
|
4009
|
+
if (candidate.diagnostic_kind !== void 0) return candidate.diagnostic_kind;
|
|
4010
|
+
if (candidate.code === "payload_rejected") return "payload_rejected";
|
|
4011
|
+
if (candidate.code === "storage") return "read_error";
|
|
4012
|
+
return void 0;
|
|
4013
|
+
});
|
|
4014
|
+
}
|
|
4015
|
+
function isSymlinkTranscript(error) {
|
|
4016
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ELOOP";
|
|
2530
4017
|
}
|
|
2531
4018
|
|
|
2532
4019
|
// host/claude/src/claude-producer-runtime.ts
|
|
2533
4020
|
async function drainClaudeProducerOnce(options) {
|
|
2534
|
-
|
|
4021
|
+
const promptPort = new HostRelayCurrentPromptPort(options.relay_endpoint, options.timeout_ms);
|
|
4022
|
+
const scan = await new ClaudeTranscriptWatcher({
|
|
4023
|
+
outbox_dir: options.outbox_dir,
|
|
4024
|
+
producer_installation_ref: options.producer_installation_ref,
|
|
4025
|
+
register_current_prompt: (input) => promptPort.register(input),
|
|
4026
|
+
release_current_prompt: (identity) => promptPort.release(identity),
|
|
4027
|
+
diagnostic: (event, failure_kind, cause) => writeHostServiceDiagnostic("claude", event, failure_kind, cause)
|
|
4028
|
+
}).scanOnce().catch((error) => {
|
|
4029
|
+
writeHostServiceDiagnostic("claude", "watcher_scan_failed", claudeTranscriptScanFailureKind(error), claudeTranscriptScanCause(void 0, error));
|
|
4030
|
+
throw error;
|
|
4031
|
+
});
|
|
4032
|
+
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 } });
|
|
4033
|
+
return { scanned: scan.enqueued, ...await producer.drainOutbox() };
|
|
2535
4034
|
}
|
|
2536
4035
|
|
|
2537
4036
|
// host/claude/src/claude-producer-service.ts
|
|
2538
4037
|
var CLAUDE_PRODUCER_CONFIG_SCHEMA = "ceal.claude_producer_config.v1";
|
|
4038
|
+
var CLAUDE_TRANSCRIPTS_ROOT = "claude_transcripts_root";
|
|
2539
4039
|
var CLAUDE_PRODUCER_DESCRIPTOR = {
|
|
2540
4040
|
schema: CLAUDE_PRODUCER_CONFIG_SCHEMA,
|
|
2541
4041
|
producer_id: "claude",
|
|
2542
|
-
|
|
4042
|
+
extra_paths: [CLAUDE_TRANSCRIPTS_ROOT],
|
|
4043
|
+
config: (shared, extras) => ({ ...shared, claude_transcripts_root: hostProducerExtraPath(extras, CLAUDE_TRANSCRIPTS_ROOT) }),
|
|
2543
4044
|
runtime_options: (config) => Object.freeze({
|
|
2544
4045
|
outbox_dir: config.outbox_dir,
|
|
2545
4046
|
producer_installation_ref: config.producer_installation_ref,
|
|
@@ -2553,53 +4054,6 @@ function runClaudeProducerCli(args, signal, dependencies = {}) {
|
|
|
2553
4054
|
return runHostProducerCli(CLAUDE_PRODUCER_DESCRIPTOR, args, signal, dependencies);
|
|
2554
4055
|
}
|
|
2555
4056
|
|
|
2556
|
-
// host/service-log.ts
|
|
2557
|
-
import { copyFile, open as open4, stat, truncate } from "node:fs/promises";
|
|
2558
|
-
import { isAbsolute } from "node:path";
|
|
2559
|
-
var HOST_SERVICE_LOG_ENVIRONMENT = "CEAL_HOST_SERVICE_LOG";
|
|
2560
|
-
var HOST_SERVICE_LOG_MAX_BYTES = 1024 * 1024;
|
|
2561
|
-
var HOST_SERVICE_LOG_CHECK_INTERVAL_MS = 6e4;
|
|
2562
|
-
function hostServiceLogPreviousPath(path) {
|
|
2563
|
-
return `${path}.1`;
|
|
2564
|
-
}
|
|
2565
|
-
async function rotateHostServiceLog(path, maxBytes = HOST_SERVICE_LOG_MAX_BYTES) {
|
|
2566
|
-
let size;
|
|
2567
|
-
try {
|
|
2568
|
-
size = (await stat(path)).size;
|
|
2569
|
-
} catch (error) {
|
|
2570
|
-
if (isNotFound(error)) return false;
|
|
2571
|
-
throw error;
|
|
2572
|
-
}
|
|
2573
|
-
if (size <= maxBytes) return false;
|
|
2574
|
-
await copyFile(path, hostServiceLogPreviousPath(path));
|
|
2575
|
-
await truncate(path, 0);
|
|
2576
|
-
return true;
|
|
2577
|
-
}
|
|
2578
|
-
function boundHostServiceLog(environment = process.env) {
|
|
2579
|
-
const path = environment[HOST_SERVICE_LOG_ENVIRONMENT];
|
|
2580
|
-
if (path === void 0 || !isAbsolute(path)) return () => void 0;
|
|
2581
|
-
let reported = false;
|
|
2582
|
-
const check = () => {
|
|
2583
|
-
rotateHostServiceLog(path).catch((error) => {
|
|
2584
|
-
if (reported) return;
|
|
2585
|
-
reported = true;
|
|
2586
|
-
process.stderr.write(`[host-service-log] rotation_failed code=${errorCode3(error)}
|
|
2587
|
-
`);
|
|
2588
|
-
});
|
|
2589
|
-
};
|
|
2590
|
-
check();
|
|
2591
|
-
const timer = setInterval(check, HOST_SERVICE_LOG_CHECK_INTERVAL_MS);
|
|
2592
|
-
timer.unref();
|
|
2593
|
-
return () => clearInterval(timer);
|
|
2594
|
-
}
|
|
2595
|
-
var HOST_SERVICE_LOG_TAIL_BYTES = 64 * 1024;
|
|
2596
|
-
function isNotFound(error) {
|
|
2597
|
-
return errorCode3(error) === "ENOENT";
|
|
2598
|
-
}
|
|
2599
|
-
function errorCode3(error) {
|
|
2600
|
-
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "unknown";
|
|
2601
|
-
}
|
|
2602
|
-
|
|
2603
4057
|
// host/producer-command.ts
|
|
2604
4058
|
async function runProducerCommand(schemaVersion, run) {
|
|
2605
4059
|
const controller = new AbortController();
|
|
@@ -2610,8 +4064,8 @@ async function runProducerCommand(schemaVersion, run) {
|
|
|
2610
4064
|
try {
|
|
2611
4065
|
await run(process.argv.slice(2), controller.signal);
|
|
2612
4066
|
} catch (error) {
|
|
2613
|
-
const
|
|
2614
|
-
process.stderr.write(`${JSON.stringify({ schema_version: schemaVersion, ok: false, code:
|
|
4067
|
+
const code2 = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "producer_failed";
|
|
4068
|
+
process.stderr.write(`${JSON.stringify({ schema_version: schemaVersion, ok: false, code: code2 })}
|
|
2615
4069
|
`);
|
|
2616
4070
|
process.exitCode = 1;
|
|
2617
4071
|
} finally {
|