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