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