@corca-org/ceal-host-linux-arm64 1.0.17 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ceal-host +0 -0
- package/libexec/ceal-host/claude-hook.mjs +1944 -526
- package/libexec/ceal-host/claude-invocation-authentication.mjs +1827 -331
- package/libexec/ceal-host/claude-producer.mjs +1794 -413
- package/libexec/ceal-host/codex-hook.mjs +1407 -1332
- package/libexec/ceal-host/codex-invocation-authentication.mjs +3075 -856
- package/libexec/ceal-host/codex-producer.mjs +957 -918
- package/libexec/ceal-host/control.mjs +2289 -1452
- package/libexec/ceal-host/relay.mjs +822 -137
- package/package.json +1 -1
- package/share/ceal-host/bundle-manifest.json +12 -12
- package/share/ceal-host/bundle-manifest.sigstore.json +1 -1
|
@@ -3,20 +3,29 @@ import { createRequire as __cealCreateRequire } from "node:module";
|
|
|
3
3
|
const require = __cealCreateRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// host/claude/src/claude-types.ts
|
|
6
|
-
var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit"
|
|
7
|
-
var CLAUDE_HOST_ERROR_CODES = ["invalid_input", "storage", "configuration"];
|
|
6
|
+
var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit"];
|
|
7
|
+
var CLAUDE_HOST_ERROR_CODES = ["invalid_input", "payload_rejected", "storage", "configuration"];
|
|
8
8
|
function isClaudeHostErrorCode(value) {
|
|
9
|
-
return typeof value === "string" && CLAUDE_HOST_ERROR_CODES.some((
|
|
9
|
+
return typeof value === "string" && CLAUDE_HOST_ERROR_CODES.some((code2) => code2 === value);
|
|
10
10
|
}
|
|
11
|
-
var CLAUDE_OUTBOX_SCHEMA = "ceal.claude_hook_outbox.v1";
|
|
12
11
|
var CLAUDE_INVOCATION_CONTEXT_SCHEMA_VERSION = "ceal.claude_invocation_context.v1";
|
|
12
|
+
var CLAUDE_OUTBOX_SCHEMA_VERSION = "ceal.claude_observation_outbox.v1";
|
|
13
|
+
var CLAUDE_SESSION_SCHEMA_VERSION = "ceal.claude_session.v1";
|
|
14
|
+
var CLAUDE_TRANSCRIPT_REGISTRATION_SCHEMA_VERSION = "ceal.claude_transcript_registration.v1";
|
|
15
|
+
var CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION = "ceal.claude_transcript_cursor.v1";
|
|
16
|
+
var CLAUDE_DEFAULT_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
17
|
+
var CLAUDE_TRANSCRIPT_MAX_TURN_MESSAGES = 1024;
|
|
18
|
+
var CLAUDE_TRANSCRIPT_MAX_SUBAGENT_FILES = 64;
|
|
19
|
+
var CLAUDE_TRANSCRIPT_ANCHOR_BYTES = 256;
|
|
13
20
|
var ClaudeHostError = class extends Error {
|
|
14
21
|
name = "ClaudeHostError";
|
|
15
22
|
code;
|
|
16
|
-
|
|
23
|
+
diagnostic_kind;
|
|
24
|
+
constructor(code2, message, diagnostic_kind) {
|
|
17
25
|
super(message);
|
|
18
|
-
if (!isClaudeHostErrorCode(
|
|
19
|
-
this.code =
|
|
26
|
+
if (!isClaudeHostErrorCode(code2)) throw new TypeError("Claude Host error code is invalid.");
|
|
27
|
+
this.code = code2;
|
|
28
|
+
this.diagnostic_kind = diagnostic_kind;
|
|
20
29
|
}
|
|
21
30
|
};
|
|
22
31
|
function isClaudeHookEvent(value) {
|
|
@@ -133,6 +142,9 @@ var client_wire_contract_default = {
|
|
|
133
142
|
"document"
|
|
134
143
|
],
|
|
135
144
|
optional_fields: [
|
|
145
|
+
"operation_id",
|
|
146
|
+
"visible_output_max_bytes",
|
|
147
|
+
"local_delivery",
|
|
136
148
|
"verified_artifact"
|
|
137
149
|
],
|
|
138
150
|
bare_document_fields: [
|
|
@@ -246,6 +258,7 @@ var CEAL_WRITE_RECEIPT_PROVIDER_STATES = vocabulary(["outcome_unknown", "verifie
|
|
|
246
258
|
var CEAL_WRITE_RECEIPT_PROVIDER_READBACKS = vocabulary(["outcome_unknown", "verified", "not_applied"]);
|
|
247
259
|
var CEAL_PROTOCOL_VERSION = "1.4.0";
|
|
248
260
|
var CEAL_OPERATION_ACCESS_SCHEMA_VERSION = "ceal.operation_access.v1";
|
|
261
|
+
var CEAL_GATEWAY_CONTINUATION_CURSOR_PHASES = vocabulary(["scan", "delivery", "terminal"]);
|
|
249
262
|
var CEAL_GATEWAY_ACTIVE_CALL_PHASES = vocabulary([
|
|
250
263
|
"audited_call_pending",
|
|
251
264
|
"continuation_read_pending",
|
|
@@ -269,6 +282,7 @@ var CEAL_GATEWAY_ORGANIZATION_ROUTE_FACT_CALL_KEYS = Object.freeze([
|
|
|
269
282
|
// packages/ceal-client-protocol/src/operation-next-actions.ts
|
|
270
283
|
var CEAL_REPEAT_REQUEST_NEXT_ACTION = "repeat_request";
|
|
271
284
|
var CEAL_INSPECT_RECEIPT_NEXT_ACTION = "inspect_receipt";
|
|
285
|
+
var CEAL_ANSWER_WITH_GAP_NEXT_ACTION = "answer_with_gap";
|
|
272
286
|
var CALLER_RECOVERABLE_NEXT_ACTION_VALUES = [
|
|
273
287
|
"select_did_you_mean",
|
|
274
288
|
"choose_new_output_path",
|
|
@@ -322,10 +336,20 @@ var CALLER_RECOVERABLE_NEXT_ACTION_VALUES = [
|
|
|
322
336
|
"retry_after",
|
|
323
337
|
// The ref names a container of rows (a Notion database or data source), which
|
|
324
338
|
// has no body of its own; the caller searches its rows instead of reading it.
|
|
325
|
-
"search_container_rows"
|
|
339
|
+
"search_container_rows",
|
|
340
|
+
// A settled unknown write names the exact receipt Operation that can inspect
|
|
341
|
+
// its outcome without issuing another provider mutation.
|
|
342
|
+
"write_receipt.get",
|
|
343
|
+
// Command resolution carries the exact required Operation sequence beside
|
|
344
|
+
// this action, so the caller can execute each declared input in order.
|
|
345
|
+
"follow_required_action_sequence",
|
|
346
|
+
// The command needs caller-owned local materialization (for example an
|
|
347
|
+
// @path file) before the Gateway can receive the argument.
|
|
348
|
+
"materialize_local_argument"
|
|
326
349
|
];
|
|
327
350
|
var CEAL_CALLER_RECOVERABLE_NEXT_ACTIONS = Object.freeze(CALLER_RECOVERABLE_NEXT_ACTION_VALUES);
|
|
328
351
|
var NON_REMEDY_NEXT_ACTION_VALUES = [
|
|
352
|
+
CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
329
353
|
// Only an administrator can take the named step -- approving a place for an
|
|
330
354
|
// Operation, granting a withheld capability -- so a non-administrator asks
|
|
331
355
|
// one. The refusal names the administrator step; this word is the ask.
|
|
@@ -373,7 +397,58 @@ var NON_REMEDY_NEXT_ACTION_VALUES = [
|
|
|
373
397
|
"select_exact_approval"
|
|
374
398
|
];
|
|
375
399
|
var CEAL_NON_REMEDY_NEXT_ACTIONS = Object.freeze(NON_REMEDY_NEXT_ACTION_VALUES);
|
|
376
|
-
var
|
|
400
|
+
var CEAL_DECLARED_NEXT_ACTIONS = Object.freeze([...CEAL_CALLER_RECOVERABLE_NEXT_ACTIONS, ...CEAL_NON_REMEDY_NEXT_ACTIONS]);
|
|
401
|
+
var DECLARED = new Set(CEAL_DECLARED_NEXT_ACTIONS);
|
|
402
|
+
function defineCealNonModelNextActions(actions) {
|
|
403
|
+
return actions;
|
|
404
|
+
}
|
|
405
|
+
var NON_MODEL_NEXT_ACTION_VALUES = defineCealNonModelNextActions([
|
|
406
|
+
CEAL_INSPECT_RECEIPT_NEXT_ACTION,
|
|
407
|
+
"await_pending_approval",
|
|
408
|
+
"inspect_gateway",
|
|
409
|
+
"operator_action",
|
|
410
|
+
"none",
|
|
411
|
+
"check_gateway",
|
|
412
|
+
"inspect_context_store",
|
|
413
|
+
"inspect_local_cache",
|
|
414
|
+
"inspect_local_client",
|
|
415
|
+
"inspect_local_command",
|
|
416
|
+
"inspect_local_output",
|
|
417
|
+
"inspect_local_route",
|
|
418
|
+
"inspect_request",
|
|
419
|
+
"reconcile",
|
|
420
|
+
"reconcile_occurrence",
|
|
421
|
+
"retry_after_owner_recovery",
|
|
422
|
+
"select_exact_approval"
|
|
423
|
+
]);
|
|
424
|
+
var NON_MODEL_NEXT_ACTIONS = new Set(NON_MODEL_NEXT_ACTION_VALUES);
|
|
425
|
+
function isCealModelActionableNonRemedyNextAction(action) {
|
|
426
|
+
return !NON_MODEL_NEXT_ACTIONS.has(action);
|
|
427
|
+
}
|
|
428
|
+
var CEAL_MODEL_ACTIONABLE_NEXT_ACTIONS = Object.freeze([
|
|
429
|
+
...CEAL_CALLER_RECOVERABLE_NEXT_ACTIONS,
|
|
430
|
+
...CEAL_NON_REMEDY_NEXT_ACTIONS.filter(isCealModelActionableNonRemedyNextAction)
|
|
431
|
+
]);
|
|
432
|
+
var MODEL_ACTIONABLE = new Set(CEAL_MODEL_ACTIONABLE_NEXT_ACTIONS);
|
|
433
|
+
var NON_MODEL_ACTION_TO_MODEL_ACTION = Object.freeze({
|
|
434
|
+
[CEAL_INSPECT_RECEIPT_NEXT_ACTION]: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
435
|
+
await_pending_approval: "ask_admin",
|
|
436
|
+
inspect_gateway: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
437
|
+
operator_action: "ask_admin",
|
|
438
|
+
none: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
439
|
+
check_gateway: "ask_admin",
|
|
440
|
+
inspect_context_store: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
441
|
+
inspect_local_cache: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
442
|
+
inspect_local_client: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
443
|
+
inspect_local_command: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
444
|
+
inspect_local_output: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
445
|
+
inspect_local_route: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
446
|
+
inspect_request: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
447
|
+
reconcile: CEAL_ANSWER_WITH_GAP_NEXT_ACTION,
|
|
448
|
+
reconcile_occurrence: "ask_admin",
|
|
449
|
+
retry_after_owner_recovery: "ask_admin",
|
|
450
|
+
select_exact_approval: "ask_admin"
|
|
451
|
+
});
|
|
377
452
|
var RETRY_PROMISE_NEXT_ACTIONS = ["retry", "retry_after"];
|
|
378
453
|
var RETRY_PROMISE = new Set(RETRY_PROMISE_NEXT_ACTIONS);
|
|
379
454
|
var RECOVERABLE = new Set(CEAL_CALLER_RECOVERABLE_NEXT_ACTIONS);
|
|
@@ -430,9 +505,9 @@ var CEAL_OPERATION_APPROVAL_DISPOSITIONS = vocabulary([
|
|
|
430
505
|
var CealProtocolValidationError = class extends Error {
|
|
431
506
|
name = "CealProtocolValidationError";
|
|
432
507
|
code;
|
|
433
|
-
constructor(
|
|
434
|
-
super(
|
|
435
|
-
this.code =
|
|
508
|
+
constructor(code2) {
|
|
509
|
+
super(code2 === "invalid_gateway_request" ? "Ceal Gateway request is invalid." : "Ceal client response is invalid.");
|
|
510
|
+
this.code = code2;
|
|
436
511
|
}
|
|
437
512
|
};
|
|
438
513
|
var SAFE_REF = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
@@ -465,10 +540,10 @@ function assertSafeJsonArray(value, options, depth, count) {
|
|
|
465
540
|
if (value.length > CEAL_WIRE_SAFE_ARRAY_MAX_ITEMS) invalidByContext(options);
|
|
466
541
|
for (const item of value) assertSafeJsonValue(item, options, depth + 1, count);
|
|
467
542
|
}
|
|
468
|
-
function assertSafeJsonRecord(
|
|
469
|
-
const entries = Object.entries(
|
|
543
|
+
function assertSafeJsonRecord(record6, options, depth, count) {
|
|
544
|
+
const entries = Object.entries(record6);
|
|
470
545
|
if (entries.length > CEAL_WIRE_SAFE_ARRAY_MAX_ITEMS) invalidByContext(options);
|
|
471
|
-
const sourceUrlColumn = options.allowHttpsUrl ? compactSourceUrlColumn(
|
|
546
|
+
const sourceUrlColumn = options.allowHttpsUrl ? compactSourceUrlColumn(record6.fields) : null;
|
|
472
547
|
for (const [key, child] of entries) {
|
|
473
548
|
if (key === "credential_material_included" && child !== false) invalidByContext(options);
|
|
474
549
|
if (!isSafeNegativeMaterialAssertion(key, child)) assertSafeJsonKey(key, options);
|
|
@@ -643,21 +718,15 @@ var CEAL_OPERATION_ACCESS_CLASSES = vocabulary(["read", "write"]);
|
|
|
643
718
|
// packages/ceal-client-protocol/src/domain-vocabulary.ts
|
|
644
719
|
var CEAL_CONNECTOR_STATUSES = vocabulary(["active", "disabled", "disconnected"]);
|
|
645
720
|
var CEAL_CONNECTOR_CUSTODIES = vocabulary(["organization", "personal"]);
|
|
646
|
-
var CEAL_PROVIDER_BOUNDARY_KINDS = vocabulary([
|
|
647
|
-
"github_account",
|
|
648
|
-
"github_organization",
|
|
649
|
-
"google_workspace_domain",
|
|
650
|
-
"notion_workspace",
|
|
651
|
-
"slack_workspace"
|
|
652
|
-
]);
|
|
653
|
-
var CEAL_PROVIDER_BOUNDARY_EVIDENCE = vocabulary(["provider_readback", "operator_stated"]);
|
|
654
721
|
var CEAL_ACCESS_LEVELS = vocabulary(["none", "read", "read_write"]);
|
|
655
722
|
var CEAL_PERSONAL_ACCESS_CEILINGS = vocabulary(["disabled", "read", "read_write"]);
|
|
656
723
|
var CEAL_TARGET_REF_PATTERN = "^[^\\p{Cc}]+$";
|
|
657
724
|
var TARGET_REF = new RegExp(CEAL_TARGET_REF_PATTERN, "u");
|
|
658
725
|
var CEAL_TARGET_VISIBILITIES = vocabulary(["public", "private"]);
|
|
659
|
-
var CEAL_PRINCIPAL_KINDS = vocabulary(["human", "
|
|
726
|
+
var CEAL_PRINCIPAL_KINDS = vocabulary(["human", "runtime"]);
|
|
727
|
+
var CEAL_PERSISTED_PRINCIPAL_KINDS = vocabulary(["human", "service", "runtime"]);
|
|
660
728
|
var CEAL_ORGANIZATION_ROLES = vocabulary(["member", "admin"]);
|
|
729
|
+
var CEAL_CALENDAR_PERIODS = vocabulary(["day", "week", "month"]);
|
|
661
730
|
var LABEL_EXPECTATION = `public-safe display text of at most ${CEAL_PUBLIC_DISPLAY_NAME_MAX_BYTES} bytes`;
|
|
662
731
|
|
|
663
732
|
// packages/ceal-client-protocol/src/safe-json-budget.ts
|
|
@@ -833,7 +902,7 @@ var CEAL_SCHEMA_LITERALS = [
|
|
|
833
902
|
"ceal.operation_receipt.v1",
|
|
834
903
|
"ceal.operation_result.v1",
|
|
835
904
|
"ceal.operation_result.compact.v1",
|
|
836
|
-
"ceal.
|
|
905
|
+
"ceal.local_result_file.v1",
|
|
837
906
|
"ceal.local_result_root.v1",
|
|
838
907
|
"ceal.owner_bootstrap_exchange_request.v1",
|
|
839
908
|
"ceal.owner_bootstrap_exchange_result.v1",
|
|
@@ -927,41 +996,6 @@ var CEAL_VERIFIED_ARTIFACT_DESCRIPTOR_JSON_SCHEMA = Object.freeze({
|
|
|
927
996
|
}
|
|
928
997
|
});
|
|
929
998
|
|
|
930
|
-
// packages/ceal-client-protocol/src/provider-operation-vocabulary.ts
|
|
931
|
-
var CEAL_FILE_TYPE_FAMILIES = vocabulary(["all", "image", "pdf"]);
|
|
932
|
-
var CEAL_GITHUB_ISSUE_STATES = vocabulary(["open", "closed", "all"]);
|
|
933
|
-
var CEAL_NOTION_UPDATABLE_BLOCK_TYPES = vocabulary([
|
|
934
|
-
"paragraph",
|
|
935
|
-
"heading_1",
|
|
936
|
-
"heading_2",
|
|
937
|
-
"heading_3",
|
|
938
|
-
"bulleted_list_item",
|
|
939
|
-
"numbered_list_item",
|
|
940
|
-
"to_do",
|
|
941
|
-
"code"
|
|
942
|
-
]);
|
|
943
|
-
var CEAL_NOTION_NEUTRAL_TYPES = vocabulary([
|
|
944
|
-
"text",
|
|
945
|
-
"number",
|
|
946
|
-
"boolean",
|
|
947
|
-
"date",
|
|
948
|
-
"select",
|
|
949
|
-
"multi_select",
|
|
950
|
-
"people",
|
|
951
|
-
"url",
|
|
952
|
-
"email",
|
|
953
|
-
"phone",
|
|
954
|
-
"unsupported"
|
|
955
|
-
]);
|
|
956
|
-
var CEAL_NOTION_IDENTITY_GAP_REASONS = vocabulary([
|
|
957
|
-
"identity_unavailable",
|
|
958
|
-
"identity_stale",
|
|
959
|
-
"identity_ambiguous",
|
|
960
|
-
"identity_revoked",
|
|
961
|
-
"identity_unlinked"
|
|
962
|
-
]);
|
|
963
|
-
var CEAL_NOTION_STRING_VALUE_TYPES = vocabulary(["text", "select", "url", "email", "phone"]);
|
|
964
|
-
|
|
965
999
|
// packages/ceal-client-protocol/src/opaque-reference.ts
|
|
966
1000
|
var CEAL_OPAQUE_HANDLE_KINDS = vocabulary(["target", "message", "thread", "artifact", "document", "block", "object"]);
|
|
967
1001
|
var CEAL_OPAQUE_REF_KINDS = vocabulary([...CEAL_OPAQUE_HANDLE_KINDS, "result", "operation-receipt", "reply-receipt", "field", "notion-data-source"]);
|
|
@@ -973,7 +1007,7 @@ var OPTIONAL_FIELDS = CEAL_CLIENT_WIRE_CONTRACT.command_response.bare_document_o
|
|
|
973
1007
|
// packages/ceal-client-protocol/src/resource-row.ts
|
|
974
1008
|
var CEAL_RESOURCE_SEARCH_ORDERS = vocabulary(["relevance", "modified", "viewed", "created"]);
|
|
975
1009
|
var CEAL_RESOURCE_SEARCH_DIRECTIONS = vocabulary(["ascending", "descending"]);
|
|
976
|
-
var CEAL_RESOURCE_SEARCH_FILTERS = vocabulary(["query", "modified_since", "since", "until", "authored_by_me", "include_replies", "kinds", "container_ref", "container_refs", "container_names", "addressed_to_me", "unread"]);
|
|
1010
|
+
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"]);
|
|
977
1011
|
var CEAL_RESOURCE_TIME_BASES = vocabulary(["created", "modified", "viewed"]);
|
|
978
1012
|
var CEAL_REPOSITORY_VISIBILITIES = vocabulary(["public", "private", "internal"]);
|
|
979
1013
|
var CEAL_ACTIVITY_V3_KINDS = vocabulary(["authored", "edited", "messaged", "responded"]);
|
|
@@ -994,6 +1028,7 @@ var CEAL_ACTIVITY_TIMESTAMP_BASES = vocabulary([
|
|
|
994
1028
|
"resource_modified"
|
|
995
1029
|
]);
|
|
996
1030
|
var CEAL_ACTIVITY_CONNECTOR_KINDS = vocabulary(["slack", "github", "google-calendar", "google-drive", "notion"]);
|
|
1031
|
+
var CEAL_ACTIVITY_UNSUPPORTED_EVENT_KIND_CODE = "unsupported_event_kind";
|
|
997
1032
|
var CEAL_ACTIVITY_GAP_CODES = vocabulary([
|
|
998
1033
|
"provider_page_budget",
|
|
999
1034
|
"call_deadline",
|
|
@@ -1001,7 +1036,7 @@ var CEAL_ACTIVITY_GAP_CODES = vocabulary([
|
|
|
1001
1036
|
"provider_cursor_unavailable",
|
|
1002
1037
|
"malformed_provider_page",
|
|
1003
1038
|
"visibility_incomplete",
|
|
1004
|
-
|
|
1039
|
+
CEAL_ACTIVITY_UNSUPPORTED_EVENT_KIND_CODE,
|
|
1005
1040
|
"direct_messages_excluded"
|
|
1006
1041
|
]);
|
|
1007
1042
|
var CEAL_ACTIVITY_EVENT_KINDS = vocabulary([
|
|
@@ -1052,6 +1087,7 @@ var RESOURCE_ROW_KEY_RULES = Object.freeze({
|
|
|
1052
1087
|
ref: { required: true, timestamp: false },
|
|
1053
1088
|
next: { required: true, timestamp: false },
|
|
1054
1089
|
kind: { required: true, timestamp: false },
|
|
1090
|
+
matched_kinds: { required: false, timestamp: false },
|
|
1055
1091
|
title: { required: true, timestamp: false },
|
|
1056
1092
|
container_label: { required: true, timestamp: false },
|
|
1057
1093
|
container_ref: { required: false, timestamp: false },
|
|
@@ -1065,6 +1101,8 @@ var RESOURCE_ROW_KEY_RULES = Object.freeze({
|
|
|
1065
1101
|
modified_at: { required: false, timestamp: true },
|
|
1066
1102
|
modified_at_local: { required: false, timestamp: true },
|
|
1067
1103
|
viewed_by_me_at: { required: false, timestamp: true },
|
|
1104
|
+
own_ceal_write: { required: false, timestamp: false },
|
|
1105
|
+
owned_by_me: { required: false, timestamp: false },
|
|
1068
1106
|
visibility: { required: false, timestamp: false },
|
|
1069
1107
|
start: { required: false, timestamp: false },
|
|
1070
1108
|
end: { required: false, timestamp: false },
|
|
@@ -1098,6 +1136,15 @@ function cealExcludedTargetCoverageJsonSchema() {
|
|
|
1098
1136
|
};
|
|
1099
1137
|
}
|
|
1100
1138
|
|
|
1139
|
+
// packages/ceal-client-protocol/src/gateway-write-identity.ts
|
|
1140
|
+
var CEAL_GATEWAY_WRITE_IDENTITY_ROLES = Object.freeze(["replay_identity", "lookup_handle", "collision_evidence"]);
|
|
1141
|
+
var CEAL_GATEWAY_WRITE_REQUEST_REF_PATTERN = /^gateway-write-request:[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/u;
|
|
1142
|
+
var CEAL_GATEWAY_WRITE_IDENTITY_FIELDS = Object.freeze({
|
|
1143
|
+
replay_identity: "idempotency_claim_sha256",
|
|
1144
|
+
lookup_handle: "write_request_sha256",
|
|
1145
|
+
collision_evidence: "normalized_mutation_sha256"
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1101
1148
|
// packages/ceal-client-protocol/src/write-safety-finding.ts
|
|
1102
1149
|
var CEAL_WRITE_SAFETY_FINDING_STATUSES = vocabulary(["warning", "unchecked"]);
|
|
1103
1150
|
var CEAL_WRITE_SAFETY_AUDIENCES = vocabulary(["workspace", "person", "members", "unknown"]);
|
|
@@ -1125,7 +1172,11 @@ var CEAL_REFRESH_FAILURE_CODES = vocabulary([
|
|
|
1125
1172
|
"refresh_revoked",
|
|
1126
1173
|
"authority_replaced",
|
|
1127
1174
|
"refresh_recovery_unavailable",
|
|
1128
|
-
"refresh_recovery_expired"
|
|
1175
|
+
"refresh_recovery_expired",
|
|
1176
|
+
// The serving gate answers this while the Gateway is still starting and the
|
|
1177
|
+
// refresh was never attempted; the same attempt stays retryable. It is never
|
|
1178
|
+
// stored by the session owner, only answered at the serving edge.
|
|
1179
|
+
"gateway_starting"
|
|
1129
1180
|
]);
|
|
1130
1181
|
|
|
1131
1182
|
// packages/ceal-client-protocol/src/request-validation-primitives.ts
|
|
@@ -1158,7 +1209,7 @@ var CANCELLATION_STATES = vocabulary(["requested", "cancelled"]);
|
|
|
1158
1209
|
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"]);
|
|
1159
1210
|
var CEAL_REQUEST_JSON_SCHEMA = {
|
|
1160
1211
|
type: "object",
|
|
1161
|
-
maxProperties:
|
|
1212
|
+
maxProperties: 11,
|
|
1162
1213
|
additionalProperties: false,
|
|
1163
1214
|
properties: {
|
|
1164
1215
|
schema_version: { type: "string", const: CEAL_REQUEST_SCHEMA_VERSION, maxLength: 32 },
|
|
@@ -1182,7 +1233,8 @@ var CEAL_REQUEST_JSON_SCHEMA = {
|
|
|
1182
1233
|
host_turn_ref: { type: "string", minLength: 1, maxLength: 128, pattern: SAFE_REF.source },
|
|
1183
1234
|
host_input_ref: { type: "string", minLength: 1, maxLength: 128, pattern: SAFE_REF.source },
|
|
1184
1235
|
host_source_ref: { type: "string", minLength: 1, maxLength: 128, pattern: SAFE_REF.source },
|
|
1185
|
-
requester_timezone: { type: "string", minLength: 1, maxLength: 128, description: "IANA timezone observed for the requester on this Request." }
|
|
1236
|
+
requester_timezone: { type: "string", minLength: 1, maxLength: 128, description: "IANA timezone observed for the requester on this Request." },
|
|
1237
|
+
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." }
|
|
1186
1238
|
},
|
|
1187
1239
|
required: CEAL_REQUEST_KEYS
|
|
1188
1240
|
};
|
|
@@ -1241,6 +1293,7 @@ var REQUEST_EXECUTION_PREPARE_REFUSAL_PROPERTIES = {
|
|
|
1241
1293
|
retryable: { type: "boolean" },
|
|
1242
1294
|
source: { type: "string", enum: ["provider", "gateway"] },
|
|
1243
1295
|
retry_after_ms: { type: "integer", minimum: 0, maximum: CEAL_RETRY_AFTER_MAX_MS },
|
|
1296
|
+
write_request_ref: { type: "string", pattern: CEAL_GATEWAY_WRITE_REQUEST_REF_PATTERN.source, maxLength: 128 },
|
|
1244
1297
|
excluded_targets: cealExcludedTargetCoverageJsonSchema(),
|
|
1245
1298
|
repaired_input: {},
|
|
1246
1299
|
unrepaired_pointers: { type: "array", maxItems: 128, items: { type: "string", minLength: 1, maxLength: 256 } },
|
|
@@ -1305,12 +1358,49 @@ function cealHostObservationRef(producer, digest2) {
|
|
|
1305
1358
|
if (!HOST_OBSERVATION_PRODUCER.test(producer) || !HOST_OBSERVATION_DIGEST.test(digest2)) invalid("Ceal Host observation reference parts are invalid.");
|
|
1306
1359
|
return `${CEAL_HOST_OBSERVATION_REF_PREFIX}${producer}-${digest2}`;
|
|
1307
1360
|
}
|
|
1361
|
+
var HOST_TOKEN_OBSERVATION_BREAKDOWN_KEYS = vocabulary(["cached_input", "cache_write_input", "reasoning_output"]);
|
|
1362
|
+
function createHostTokenObservationValue(counts, source) {
|
|
1363
|
+
const core = validTokenCore(counts);
|
|
1364
|
+
if (typeof source !== "string" || source.length === 0 || source.length > 256) invalid("Ceal Host token observation source is invalid.");
|
|
1365
|
+
const value = { status: "available", unit: "tokens", input: core.input, output: core.output, total: core.total, source };
|
|
1366
|
+
for (const breakdown of validTokenBreakdowns(counts)) value[breakdown.key] = breakdown.tokens;
|
|
1367
|
+
assertHostObservationValue(value, "token");
|
|
1368
|
+
return value;
|
|
1369
|
+
}
|
|
1370
|
+
function validTokenCore(counts) {
|
|
1371
|
+
if (!isOperationRecord(counts) || !safeCount(counts.input) || !safeCount(counts.output) || !safeCount(counts.total)) invalid("Ceal Host token observation counts are invalid.");
|
|
1372
|
+
if (counts.total !== counts.input + counts.output) invalid("Ceal Host token observation counts are invalid.");
|
|
1373
|
+
return { input: counts.input, output: counts.output, total: counts.total };
|
|
1374
|
+
}
|
|
1375
|
+
function validTokenBreakdowns(counts) {
|
|
1376
|
+
const breakdowns = [
|
|
1377
|
+
{ key: "cached_input", tokens: counts.cached_input },
|
|
1378
|
+
{ key: "cache_write_input", tokens: counts.cache_write_input },
|
|
1379
|
+
{ key: "reasoning_output", tokens: counts.reasoning_output }
|
|
1380
|
+
];
|
|
1381
|
+
const stated = breakdowns.filter((breakdown) => breakdown.tokens !== void 0).map((breakdown) => ({ key: breakdown.key, tokens: Number(breakdown.tokens) }));
|
|
1382
|
+
for (const breakdown of stated) {
|
|
1383
|
+
if (!safeCount(breakdown.tokens)) invalid("Ceal Host token observation counts are invalid.");
|
|
1384
|
+
}
|
|
1385
|
+
const cached = counts.cached_input ?? 0;
|
|
1386
|
+
const created = counts.cache_write_input ?? 0;
|
|
1387
|
+
const reasoning = counts.reasoning_output ?? 0;
|
|
1388
|
+
if (cached + created > counts.input || reasoning > counts.output) invalid("Ceal Host token observation counts are invalid.");
|
|
1389
|
+
return stated;
|
|
1390
|
+
}
|
|
1391
|
+
function hostObservationNotRecorded(reason) {
|
|
1392
|
+
if (typeof reason !== "string" || reason.length === 0 || reason.length > 256) invalid("Ceal Host observation gap reason is invalid.");
|
|
1393
|
+
return { status: "not_recorded", reason };
|
|
1394
|
+
}
|
|
1395
|
+
function safeCount(value) {
|
|
1396
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
1397
|
+
}
|
|
1308
1398
|
function validateCealHostObservationDraft(value) {
|
|
1309
|
-
const
|
|
1310
|
-
requirePrefixedRef(
|
|
1311
|
-
requireClosedValue(
|
|
1312
|
-
requireIsoTimestamp(
|
|
1313
|
-
assertHostObservationValue(
|
|
1399
|
+
const record6 = operationRecord(value, "Ceal Host observation draft is invalid.");
|
|
1400
|
+
requirePrefixedRef(record6.observation_ref, CEAL_HOST_OBSERVATION_REF_PREFIX, "Ceal Host observation reference is invalid.");
|
|
1401
|
+
requireClosedValue(record6.observation_kind, CEAL_HOST_OBSERVATION_KINDS, "Ceal Host observation kind is invalid.");
|
|
1402
|
+
requireIsoTimestamp(record6.observed_at, "Ceal Host observation time is invalid.");
|
|
1403
|
+
assertHostObservationValue(record6.value, record6.observation_kind);
|
|
1314
1404
|
}
|
|
1315
1405
|
var CEAL_REQUEST_MAX_OBSERVATION_VALUE_BYTES = 64 * 1024;
|
|
1316
1406
|
var CEAL_HOST_OBSERVATION_KINDS = vocabulary(["prompt", "session", "turn", "model", "token", "cost", "timing"]);
|
|
@@ -1446,12 +1536,94 @@ var CEAL_HOST_INVOCATION_AUTHENTICATION_RESULT_MAX_BYTES = new TextEncoder().enc
|
|
|
1446
1536
|
expires_at: (/* @__PURE__ */ new Date("+010000-01-01T00:00:00.000Z")).toISOString()
|
|
1447
1537
|
})}
|
|
1448
1538
|
`).byteLength;
|
|
1539
|
+
var CEAL_HOST_MINT_REQUEST_CONTEXT_REFUSAL_REASONS = vocabulary([
|
|
1540
|
+
"request_authority_unavailable",
|
|
1541
|
+
"request_missing",
|
|
1542
|
+
"authority_mismatch",
|
|
1543
|
+
"host_source_mismatch",
|
|
1544
|
+
"host_session_mismatch",
|
|
1545
|
+
"host_turn_mismatch",
|
|
1546
|
+
"host_input_mismatch",
|
|
1547
|
+
"host_prompt_mismatch"
|
|
1548
|
+
]);
|
|
1549
|
+
var CEAL_HOST_AUTHENTICATION_CAUSES = vocabulary([
|
|
1550
|
+
"invalid_arguments",
|
|
1551
|
+
"options_invalid",
|
|
1552
|
+
"session_unavailable",
|
|
1553
|
+
"context_unavailable",
|
|
1554
|
+
"context_replaced",
|
|
1555
|
+
"no_binding",
|
|
1556
|
+
"context_stale",
|
|
1557
|
+
"context_expired",
|
|
1558
|
+
"host_source_refused",
|
|
1559
|
+
"host_credential_refused",
|
|
1560
|
+
"gateway_unavailable",
|
|
1561
|
+
"gateway_rate_limited",
|
|
1562
|
+
"gateway_refused",
|
|
1563
|
+
"gateway_response_invalid",
|
|
1564
|
+
"producer_frame_invalid",
|
|
1565
|
+
"relay_permission_denied",
|
|
1566
|
+
"relay_unavailable",
|
|
1567
|
+
"transient_retryable",
|
|
1568
|
+
"mint_failed",
|
|
1569
|
+
"relay_failed",
|
|
1570
|
+
"host_refusal_invalid"
|
|
1571
|
+
]);
|
|
1572
|
+
var CEAL_HOST_AUTHENTICATION_POLICY = Object.freeze({
|
|
1573
|
+
invalid_arguments: Object.freeze({ classification: "host_arguments_invalid", next_action: "answer_with_gap" }),
|
|
1574
|
+
options_invalid: Object.freeze({ classification: "host_configuration_invalid", next_action: "answer_with_gap" }),
|
|
1575
|
+
session_unavailable: Object.freeze({ classification: "host_session_unavailable", next_action: "answer_with_gap" }),
|
|
1576
|
+
context_unavailable: Object.freeze({ classification: "host_context_unavailable", next_action: "refresh_host_invocation_context", remedy: "the Host has no invocation context from the current transcript turn; run `ceal-host install` to repair the Host hook registration, then retry this command on a fresh Host turn" }),
|
|
1577
|
+
context_replaced: Object.freeze({ classification: "host_invocation_context_stale", next_action: "refresh_host_invocation_context" }),
|
|
1578
|
+
no_binding: Object.freeze({ classification: "host_binding_unavailable", next_action: "refresh_host_invocation_context" }),
|
|
1579
|
+
context_stale: Object.freeze({ classification: "host_invocation_context_stale", next_action: "refresh_host_invocation_context" }),
|
|
1580
|
+
context_expired: Object.freeze({ classification: "host_invocation_context_expired", next_action: "refresh_host_invocation_context" }),
|
|
1581
|
+
host_source_refused: Object.freeze({ classification: "host_source_refused", next_action: "answer_with_gap" }),
|
|
1582
|
+
host_credential_refused: Object.freeze({ classification: "host_registration_credential_refused", next_action: "ask_admin" }),
|
|
1583
|
+
gateway_unavailable: Object.freeze({ classification: "host_mint_gateway_unavailable", next_action: "ask_admin" }),
|
|
1584
|
+
gateway_rate_limited: Object.freeze({ classification: "host_mint_gateway_rate_limited", next_action: "answer_with_gap" }),
|
|
1585
|
+
gateway_refused: Object.freeze({ classification: "host_mint_gateway_refused", next_action: "answer_with_gap" }),
|
|
1586
|
+
gateway_response_invalid: Object.freeze({ classification: "host_mint_gateway_response_invalid", next_action: "answer_with_gap" }),
|
|
1587
|
+
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" }),
|
|
1588
|
+
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" }),
|
|
1589
|
+
relay_unavailable: Object.freeze({ classification: "host_relay_unavailable", next_action: "answer_with_gap" }),
|
|
1590
|
+
transient_retryable: Object.freeze({ classification: "host_mint_transient_retryable", next_action: "retry" }),
|
|
1591
|
+
mint_failed: Object.freeze({ classification: "host_mint_failed", next_action: "answer_with_gap" }),
|
|
1592
|
+
relay_failed: Object.freeze({ classification: "host_relay_failed", next_action: "answer_with_gap" }),
|
|
1593
|
+
host_refusal_invalid: Object.freeze({ classification: "host_refusal_invalid", next_action: "answer_with_gap" })
|
|
1594
|
+
});
|
|
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
|
+
"activation_recorded",
|
|
1607
|
+
"activation_failed",
|
|
1608
|
+
"watcher_scan_failed",
|
|
1609
|
+
"activation_wait_skipped",
|
|
1610
|
+
"activation_wait_engaged",
|
|
1611
|
+
"activation_wait_completed",
|
|
1612
|
+
"activation_wait_exhausted"
|
|
1613
|
+
]);
|
|
1614
|
+
var CEAL_HOST_SERVICE_DIAGNOSTIC_FAILURE_KINDS = vocabulary([
|
|
1615
|
+
"malformed_line",
|
|
1616
|
+
"other",
|
|
1617
|
+
"payload_rejected",
|
|
1618
|
+
"read_error",
|
|
1619
|
+
"size_budget",
|
|
1620
|
+
"symlink"
|
|
1621
|
+
]);
|
|
1449
1622
|
|
|
1450
1623
|
// packages/ceal-client-protocol/src/client-result-materialization.ts
|
|
1451
1624
|
var CEAL_UNREAD_REASONS = vocabulary(["blocked", "unavailable", "too_large", "unsupported", "download_failed", "permission_denied"]);
|
|
1452
|
-
var
|
|
1453
|
-
var
|
|
1454
|
-
var FIELDS = /* @__PURE__ */ new Set([...REQUIRED_FIELDS2, "read_argv", "local_result_root", "continue_argv", "model_disclosure", "item_read_argv"]);
|
|
1625
|
+
var REQUIRED_FIELDS2 = Object.freeze(["schema_version", "operation_id", "path", "sha256"]);
|
|
1626
|
+
var FIELDS = /* @__PURE__ */ new Set([...REQUIRED_FIELDS2, "items_path", "row_count", "result_complete", "coverage", "local_result_root", "model_disclosure", "continue_argv", "how_to_read"]);
|
|
1455
1627
|
|
|
1456
1628
|
// packages/ceal-client-protocol/src/session-start-affordance.ts
|
|
1457
1629
|
var CEAL_SESSION_ENTRY_INVOCATION = "ceal";
|
|
@@ -1484,14 +1656,6 @@ var CEAL_GATEWAY_ANNOUNCEMENT_POLICY_NON_CLAIMS = vocabulary([
|
|
|
1484
1656
|
"target_specific_scope_not_declared"
|
|
1485
1657
|
]);
|
|
1486
1658
|
|
|
1487
|
-
// packages/ceal-client-protocol/src/gateway-write-identity.ts
|
|
1488
|
-
var CEAL_GATEWAY_WRITE_IDENTITY_ROLES = Object.freeze(["replay_identity", "lookup_handle", "collision_evidence"]);
|
|
1489
|
-
var CEAL_GATEWAY_WRITE_IDENTITY_FIELDS = Object.freeze({
|
|
1490
|
-
replay_identity: "idempotency_claim_sha256",
|
|
1491
|
-
lookup_handle: "write_request_sha256",
|
|
1492
|
-
collision_evidence: "normalized_mutation_sha256"
|
|
1493
|
-
});
|
|
1494
|
-
|
|
1495
1659
|
// packages/ceal-client-protocol/src/gateway-scoped-identity-projection-validation.ts
|
|
1496
1660
|
var PROJECTION_REVISION_KEYS = Object.freeze(["graph_revision", "subject_key_revision", "projection_revision"]);
|
|
1497
1661
|
|
|
@@ -1504,6 +1668,18 @@ var CEAL_SUPPORTED_GATEWAY_PROTOCOL_RANGE = Object.freeze({
|
|
|
1504
1668
|
// packages/ceal-client-protocol/src/admin-projection-vocabulary.ts
|
|
1505
1669
|
var ADMIN_PROJECTION_STATES = vocabulary(["ready", "empty", "partial", "unknown"]);
|
|
1506
1670
|
var ADMIN_VIEW_KINDS = vocabulary(["request_detail", "resource_access_timeline", "weekly_usage"]);
|
|
1671
|
+
var ADMIN_RECORD_PROJECTION_KINDS = vocabulary([
|
|
1672
|
+
"request_list",
|
|
1673
|
+
"usage_series",
|
|
1674
|
+
"connector_health",
|
|
1675
|
+
"person_connections",
|
|
1676
|
+
"client_sessions",
|
|
1677
|
+
"knowledge_base",
|
|
1678
|
+
"change_history",
|
|
1679
|
+
"approval_queue",
|
|
1680
|
+
"access_group_list",
|
|
1681
|
+
"capability_catalogue"
|
|
1682
|
+
]);
|
|
1507
1683
|
var ADMIN_GAP_KINDS = vocabulary(["unknown", "unavailable", "partial"]);
|
|
1508
1684
|
var ADMIN_UNAVAILABLE_REASONS = vocabulary([
|
|
1509
1685
|
"not_recorded",
|
|
@@ -1527,7 +1703,11 @@ var ADMIN_SOURCE_OWNERS = vocabulary([
|
|
|
1527
1703
|
"person",
|
|
1528
1704
|
"resource",
|
|
1529
1705
|
"prompt",
|
|
1530
|
-
"usage"
|
|
1706
|
+
"usage",
|
|
1707
|
+
"connector",
|
|
1708
|
+
"session",
|
|
1709
|
+
"knowledge",
|
|
1710
|
+
"policy"
|
|
1531
1711
|
]);
|
|
1532
1712
|
var ADMIN_COMPLETENESS_STATES = vocabulary(["complete", "partial", "unknown"]);
|
|
1533
1713
|
var ADMIN_OPERATION_OUTCOMES = vocabulary(["completed", "failed", "denied", "cancelled", "unknown"]);
|
|
@@ -1554,10 +1734,7 @@ var FAILURE_RESPONSE_KEYS = Object.freeze(["error", "ok", "proof_ref_or_unavaila
|
|
|
1554
1734
|
|
|
1555
1735
|
// host/claude/src/claude-decoder.ts
|
|
1556
1736
|
var MAX_HOOK_BYTES = 256 * 1024;
|
|
1557
|
-
var MAX_TEXT = 128 * 1024;
|
|
1558
1737
|
var SESSION_SOURCES = ["startup", "resume", "clear", "compact", "fork"];
|
|
1559
|
-
var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
1560
|
-
var STOP_FAILURES = ["rate_limit", "overloaded", "authentication_failed", "oauth_org_not_allowed", "account_on_hold", "billing_error", "invalid_request", "model_not_found", "server_error", "max_output_tokens", "unknown"];
|
|
1561
1738
|
function decodeClaudeHookInput(event, raw) {
|
|
1562
1739
|
const rawBytes = Buffer.byteLength(raw, "utf8");
|
|
1563
1740
|
if (rawBytes > MAX_HOOK_BYTES) throw invalid2(`Claude hook input is too large (${refusalFact("bytes", rawBytes, `at most ${MAX_HOOK_BYTES} UTF-8 bytes`)}).`);
|
|
@@ -1568,70 +1745,33 @@ function decodeClaudeHookInput(event, raw) {
|
|
|
1568
1745
|
throw invalid2("Claude hook input is not valid JSON.");
|
|
1569
1746
|
}
|
|
1570
1747
|
if (!record(value) || value.hook_event_name !== event) throw invalid2(`Claude hook event identity does not match the configured event (${refusalFact("hook_event_name", record(value) ? value.hook_event_name : value, `expected ${event}`)}).`);
|
|
1571
|
-
const common =
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1748
|
+
const common = {
|
|
1749
|
+
session_id: requiredText(value.session_id, 256, "Claude session identity is invalid."),
|
|
1750
|
+
transcript_path: requiredAbsolutePath(value.transcript_path, "Claude transcript path is invalid."),
|
|
1751
|
+
cwd: requiredAbsolutePath(value.cwd, "Claude working directory is invalid.")
|
|
1752
|
+
};
|
|
1753
|
+
if (event === "UserPromptSubmit") {
|
|
1754
|
+
return { event, ...common, hook_event_name: event, prompt: requiredText(value.prompt, 256 * 1024, "Claude submitted prompt is invalid.") };
|
|
1755
|
+
}
|
|
1578
1756
|
if (!isSessionSource(value.source)) throw invalid2("Claude SessionStart source is invalid.");
|
|
1579
1757
|
const model = optionalText(value.model, 256);
|
|
1580
|
-
return { ...common, hook_event_name: "SessionStart", source: value.source, ...model === void 0 ? {} : { model } };
|
|
1581
|
-
}
|
|
1582
|
-
function decodeUserPrompt(value, common, promptId) {
|
|
1583
|
-
return { ...common, hook_event_name: "UserPromptSubmit", prompt_id: promptId, prompt: requiredText(value.prompt, MAX_TEXT, "Claude prompt is invalid.") };
|
|
1584
|
-
}
|
|
1585
|
-
function decodeStop(value, common, promptId) {
|
|
1586
|
-
if (typeof value.stop_hook_active !== "boolean") throw invalid2("Claude Stop state is invalid.");
|
|
1587
|
-
return { ...common, hook_event_name: "Stop", prompt_id: promptId, stop_hook_active: value.stop_hook_active, last_assistant_message: requiredText(value.last_assistant_message, MAX_TEXT, "Claude Stop response is invalid.") };
|
|
1588
|
-
}
|
|
1589
|
-
function decodeStopFailure(value, common, promptId) {
|
|
1590
|
-
if (!isStopFailure(value.error)) throw invalid2("Claude StopFailure error is invalid.");
|
|
1591
|
-
const errorDetails = optionalText(value.error_details, 4096);
|
|
1592
|
-
const lastMessage = optionalText(value.last_assistant_message, MAX_TEXT);
|
|
1593
|
-
return { ...common, hook_event_name: "StopFailure", prompt_id: promptId, error: value.error, ...errorDetails === void 0 ? {} : { error_details: errorDetails }, ...lastMessage === void 0 ? {} : { last_assistant_message: lastMessage } };
|
|
1758
|
+
return { event, ...common, hook_event_name: "SessionStart", source: value.source, ...model === void 0 ? {} : { model } };
|
|
1594
1759
|
}
|
|
1595
1760
|
function claudeLocalSessionRef(sessionId) {
|
|
1596
1761
|
return `claude-session:${digest(sessionId)}`;
|
|
1597
1762
|
}
|
|
1598
|
-
function claudeLocalTurnRef(sessionId,
|
|
1599
|
-
return `claude-turn:${cealRequestSha256({ session_id: sessionId, prompt_id:
|
|
1763
|
+
function claudeLocalTurnRef(sessionId, turnId) {
|
|
1764
|
+
return `claude-turn:${cealRequestSha256({ session_id: sessionId, prompt_id: turnId })}`;
|
|
1600
1765
|
}
|
|
1601
|
-
function claudeLocalInputRef(sessionId,
|
|
1602
|
-
return `claude-input:${cealRequestSha256({ session_id: sessionId, prompt_id:
|
|
1766
|
+
function claudeLocalInputRef(sessionId, turnId) {
|
|
1767
|
+
return `claude-input:${cealRequestSha256({ session_id: sessionId, prompt_id: turnId })}`;
|
|
1603
1768
|
}
|
|
1604
1769
|
function claudeWorkspaceRef(cwd) {
|
|
1605
1770
|
return `workspace:${digest(cwd)}`;
|
|
1606
1771
|
}
|
|
1607
|
-
function decodeCommon(value, event) {
|
|
1608
|
-
const effort = decodeEffort(value.effort);
|
|
1609
|
-
const promptId = optionalText(value.prompt_id, 256);
|
|
1610
|
-
const permissionMode = optionalText(value.permission_mode, 64);
|
|
1611
|
-
return {
|
|
1612
|
-
session_id: requiredText(value.session_id, 256, "Claude session identity is invalid."),
|
|
1613
|
-
...promptId === void 0 ? {} : { prompt_id: promptId },
|
|
1614
|
-
transcript_path: requiredAbsolutePath(value.transcript_path, "Claude transcript path is invalid."),
|
|
1615
|
-
cwd: requiredAbsolutePath(value.cwd, "Claude working directory is invalid."),
|
|
1616
|
-
...permissionMode === void 0 ? {} : { permission_mode: permissionMode },
|
|
1617
|
-
...effort === void 0 ? {} : { effort },
|
|
1618
|
-
hook_event_name: event
|
|
1619
|
-
};
|
|
1620
|
-
}
|
|
1621
|
-
function decodeEffort(value) {
|
|
1622
|
-
if (value === void 0) return void 0;
|
|
1623
|
-
if (!record(value) || !isEffortLevel(value.level)) throw invalid2("Claude effort is invalid.");
|
|
1624
|
-
return { level: value.level };
|
|
1625
|
-
}
|
|
1626
1772
|
function isSessionSource(value) {
|
|
1627
1773
|
return typeof value === "string" && SESSION_SOURCES.some((source) => source === value);
|
|
1628
1774
|
}
|
|
1629
|
-
function isEffortLevel(value) {
|
|
1630
|
-
return typeof value === "string" && EFFORT_LEVELS.some((effort) => effort === value);
|
|
1631
|
-
}
|
|
1632
|
-
function isStopFailure(value) {
|
|
1633
|
-
return typeof value === "string" && STOP_FAILURES.some((failure) => failure === value);
|
|
1634
|
-
}
|
|
1635
1775
|
function requiredAbsolutePath(value, message) {
|
|
1636
1776
|
const text = requiredText(value, 4096, message);
|
|
1637
1777
|
if (!text.startsWith("/") || text.includes("\0")) throw invalid2(message);
|
|
@@ -1659,8 +1799,62 @@ function invalid2(message) {
|
|
|
1659
1799
|
}
|
|
1660
1800
|
|
|
1661
1801
|
// host/claude/src/claude-outbox.ts
|
|
1662
|
-
import {
|
|
1663
|
-
import { join as
|
|
1802
|
+
import { mkdir as mkdir3, readdir, readFile as readFile2, unlink as unlink2 } from "node:fs/promises";
|
|
1803
|
+
import { isAbsolute, join as join3 } from "node:path";
|
|
1804
|
+
|
|
1805
|
+
// host/invocation-context.ts
|
|
1806
|
+
var HOST_INVOCATION_IDENTITY_KEYS = ["host_session_ref", "host_turn_ref", "host_source_ref"];
|
|
1807
|
+
var HOST_INVOCATION_CONTEXT_REQUIRED_KEYS = ["schema_version", "identity", "prompt_ref", "expires_at"];
|
|
1808
|
+
function invocationContextKey(identity) {
|
|
1809
|
+
return cealRequestSha256({ host_source_ref: identity.host_source_ref, host_session_ref: identity.host_session_ref });
|
|
1810
|
+
}
|
|
1811
|
+
function hasHostInvocationIdentityKeys(value) {
|
|
1812
|
+
return exactKeys(value, HOST_INVOCATION_IDENTITY_KEYS);
|
|
1813
|
+
}
|
|
1814
|
+
function hasHostInvocationContextKeys(value, optional = []) {
|
|
1815
|
+
return requiredKeys(value, HOST_INVOCATION_CONTEXT_REQUIRED_KEYS, optional);
|
|
1816
|
+
}
|
|
1817
|
+
async function readLatestSessionProjection(file, hostSourceRef, hostSessionRef, readProjection) {
|
|
1818
|
+
const context = await readProjection(file);
|
|
1819
|
+
if (context === null || context.identity.host_source_ref !== hostSourceRef || context.identity.host_session_ref !== hostSessionRef) return null;
|
|
1820
|
+
return context;
|
|
1821
|
+
}
|
|
1822
|
+
function isLiveSessionProjection(context, now) {
|
|
1823
|
+
return context !== null && Date.parse(context.expires_at) > now.getTime();
|
|
1824
|
+
}
|
|
1825
|
+
function invocationContextFromTurn(identity, promptRef, expiresAt, descriptor) {
|
|
1826
|
+
const context = {
|
|
1827
|
+
schema_version: descriptor.schema_version,
|
|
1828
|
+
identity,
|
|
1829
|
+
prompt_ref: promptRef,
|
|
1830
|
+
expires_at: expiresAt,
|
|
1831
|
+
...descriptor.turn_generation_ref === void 0 ? {} : { turn_generation_ref: descriptor.turn_generation_ref },
|
|
1832
|
+
...descriptor.working_directory === void 0 ? {} : { working_directory: descriptor.working_directory }
|
|
1833
|
+
};
|
|
1834
|
+
descriptor.validate(context);
|
|
1835
|
+
return Object.freeze(context);
|
|
1836
|
+
}
|
|
1837
|
+
function isHostInvocationTurnSelection(value) {
|
|
1838
|
+
return isRecord(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) => safeRef(value[key]));
|
|
1839
|
+
}
|
|
1840
|
+
function invocationTurnSelectionMatches(context, selection) {
|
|
1841
|
+
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;
|
|
1842
|
+
}
|
|
1843
|
+
function exactKeys(value, keys) {
|
|
1844
|
+
const actual = Object.keys(value);
|
|
1845
|
+
return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key)) && actual.every((key) => keys.includes(key));
|
|
1846
|
+
}
|
|
1847
|
+
function requiredKeys(value, required, optional) {
|
|
1848
|
+
const actual = Object.keys(value);
|
|
1849
|
+
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
1850
|
+
return actual.length >= required.length && actual.length <= allowed.size && required.every((key) => Object.hasOwn(value, key)) && actual.every((key) => allowed.has(key));
|
|
1851
|
+
}
|
|
1852
|
+
function isRecord(value) {
|
|
1853
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1854
|
+
}
|
|
1855
|
+
function safeRef(value) {
|
|
1856
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value);
|
|
1857
|
+
}
|
|
1664
1858
|
|
|
1665
1859
|
// host/relay/src/producer-frame.ts
|
|
1666
1860
|
var HOST_PRODUCER_FRAME_SCHEMA = "ceal.host_producer_frame.v1";
|
|
@@ -1673,11 +1867,20 @@ var FRAME_KINDS = new Set(HOST_PRODUCER_FRAME_KINDS);
|
|
|
1673
1867
|
function isHostProducerFrameKind(value) {
|
|
1674
1868
|
return typeof value === "string" && FRAME_KINDS.has(value);
|
|
1675
1869
|
}
|
|
1870
|
+
var HOST_PRODUCER_FRAME_ERROR_CODES = [
|
|
1871
|
+
"invalid_frame_encoding",
|
|
1872
|
+
"invalid_frame_length",
|
|
1873
|
+
"frame_truncated",
|
|
1874
|
+
"unsupported_frame_schema",
|
|
1875
|
+
"invalid_frame_shape",
|
|
1876
|
+
"invalid_producer_identity",
|
|
1877
|
+
"frame_too_large"
|
|
1878
|
+
];
|
|
1676
1879
|
var HostProducerFrameError = class extends Error {
|
|
1677
1880
|
code;
|
|
1678
|
-
constructor(
|
|
1881
|
+
constructor(code2, message = code2) {
|
|
1679
1882
|
super(message);
|
|
1680
|
-
this.code =
|
|
1883
|
+
this.code = code2;
|
|
1681
1884
|
}
|
|
1682
1885
|
};
|
|
1683
1886
|
function encodeHostProducerFrame(frame) {
|
|
@@ -1707,11 +1910,14 @@ function decodeHostProducerFrame(input) {
|
|
|
1707
1910
|
validateFrame(value);
|
|
1708
1911
|
return value;
|
|
1709
1912
|
}
|
|
1913
|
+
function isHostSafeRef(value) {
|
|
1914
|
+
return safeRef2(value);
|
|
1915
|
+
}
|
|
1710
1916
|
function validateFrame(value) {
|
|
1711
1917
|
if (!record2(value)) throw new HostProducerFrameError("invalid_frame_shape");
|
|
1712
1918
|
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)}`);
|
|
1713
|
-
if (!
|
|
1714
|
-
if (!PRODUCERS.has(String(value.producer_id)) || !
|
|
1919
|
+
if (!exactKeys2(value, ["schema_version", "producer_id", "producer_installation_ref", "producer_epoch", "sequence", "frame_kind", "occurred_at", "payload"])) throw new HostProducerFrameError("invalid_frame_shape");
|
|
1920
|
+
if (!PRODUCERS.has(String(value.producer_id)) || !safeRef2(value.producer_installation_ref)) throw new HostProducerFrameError("invalid_producer_identity");
|
|
1715
1921
|
if (!positive(value.producer_epoch) || !positive(value.sequence) || !hostProducerFrameKind(value.frame_kind) || !timestamp(value.occurred_at) || !record2(value.payload)) throw new HostProducerFrameError("invalid_frame_shape");
|
|
1716
1922
|
validatePayload(value.frame_kind, value.payload);
|
|
1717
1923
|
}
|
|
@@ -1720,13 +1926,13 @@ function validatePayload(kind, value) {
|
|
|
1720
1926
|
if (!valid) throw new HostProducerFrameError("invalid_frame_shape");
|
|
1721
1927
|
}
|
|
1722
1928
|
function validSession(value) {
|
|
1723
|
-
return exactOptional(value, ["local_session_ref", "working_directory_ref"], ["model", "host_version", "observations"]) &&
|
|
1929
|
+
return exactOptional(value, ["local_session_ref", "working_directory_ref"], ["model", "host_version", "observations"]) && safeRef2(value.local_session_ref) && safeRef2(value.working_directory_ref) && optionalText2(value.model, 256) && optionalText2(value.host_version, 256) && validObservations(value.observations);
|
|
1724
1930
|
}
|
|
1725
1931
|
function validTurn(value) {
|
|
1726
|
-
return exactOptional(value, ["local_session_ref", "local_turn_ref"], ["model", "observations"]) &&
|
|
1932
|
+
return exactOptional(value, ["local_session_ref", "local_turn_ref"], ["model", "observations"]) && safeRef2(value.local_session_ref) && safeRef2(value.local_turn_ref) && optionalText2(value.model, 256) && validObservations(value.observations);
|
|
1727
1933
|
}
|
|
1728
1934
|
function validInput(value) {
|
|
1729
|
-
return exactOptional(value, ["local_session_ref", "local_turn_ref", "local_input_ref", "input_kind", "origin_input", "observations"], ["local_turn_aliases", "context_expires_at"]) &&
|
|
1935
|
+
return exactOptional(value, ["local_session_ref", "local_turn_ref", "local_input_ref", "input_kind", "origin_input", "observations"], ["local_turn_aliases", "context_expires_at"]) && safeRef2(value.local_session_ref) && safeRef2(value.local_turn_ref) && safeRef2(value.local_input_ref) && validInputObservations(value.observations) && isCealHostInputKind(value.input_kind) && typeof value.origin_input === "boolean" && validInputOptionals(value);
|
|
1730
1936
|
}
|
|
1731
1937
|
function validInputObservations(value) {
|
|
1732
1938
|
return validObservations(value) && Array.isArray(value) && value.filter((item) => record2(item) && item.observation_kind === "prompt").length === 1;
|
|
@@ -1735,13 +1941,13 @@ function validInputOptionals(value) {
|
|
|
1735
1941
|
return optionalRefs(value.local_turn_aliases) && (value.context_expires_at === void 0 || timestamp(value.context_expires_at));
|
|
1736
1942
|
}
|
|
1737
1943
|
function validTerminal(value) {
|
|
1738
|
-
return exactOptional(value, ["local_session_ref", "local_turn_ref", "terminal_state"], ["abort_reason", "duration_ms", "time_to_first_token_ms", "usage", "observations"]) &&
|
|
1944
|
+
return exactOptional(value, ["local_session_ref", "local_turn_ref", "terminal_state"], ["abort_reason", "duration_ms", "time_to_first_token_ms", "usage", "observations"]) && safeRef2(value.local_session_ref) && safeRef2(value.local_turn_ref) && validTerminalDisposition(value.terminal_state, value.abort_reason) && optionalNonNegative(value.duration_ms) && optionalNonNegative(value.time_to_first_token_ms) && (value.usage === void 0 || jsonValue(value.usage)) && validObservations(value.observations);
|
|
1739
1945
|
}
|
|
1740
1946
|
function validTerminalDisposition(state, reason) {
|
|
1741
|
-
return (state === "completed" || state === "aborted") && (reason === void 0 || state === "aborted" &&
|
|
1947
|
+
return (state === "completed" || state === "aborted") && (reason === void 0 || state === "aborted" && safeRef2(reason));
|
|
1742
1948
|
}
|
|
1743
1949
|
function validStatus(value) {
|
|
1744
|
-
return exactOptional(value, ["status", "reason_code"], ["detail"]) && ["ready", "degraded", "stopping"].includes(String(value.status)) &&
|
|
1950
|
+
return exactOptional(value, ["status", "reason_code"], ["detail"]) && ["ready", "degraded", "stopping"].includes(String(value.status)) && safeRef2(value.reason_code) && optionalText2(value.detail, 4096);
|
|
1745
1951
|
}
|
|
1746
1952
|
function optionalText2(value, maxLength) {
|
|
1747
1953
|
return value === void 0 || typeof value === "string" && value.length > 0 && value.length <= maxLength;
|
|
@@ -1750,7 +1956,7 @@ function optionalNonNegative(value) {
|
|
|
1750
1956
|
return value === void 0 || typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
1751
1957
|
}
|
|
1752
1958
|
function optionalRefs(value) {
|
|
1753
|
-
return value === void 0 || Array.isArray(value) && value.length <= 16 && value.every(
|
|
1959
|
+
return value === void 0 || Array.isArray(value) && value.length <= 16 && value.every(safeRef2);
|
|
1754
1960
|
}
|
|
1755
1961
|
function validObservations(value) {
|
|
1756
1962
|
if (value === void 0) return true;
|
|
@@ -1774,7 +1980,7 @@ function jsonValue(value, depth = 0) {
|
|
|
1774
1980
|
function record2(value) {
|
|
1775
1981
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1776
1982
|
}
|
|
1777
|
-
function
|
|
1983
|
+
function safeRef2(value) {
|
|
1778
1984
|
return typeof value === "string" && SAFE_REF2.test(value);
|
|
1779
1985
|
}
|
|
1780
1986
|
function positive(value) {
|
|
@@ -1786,7 +1992,7 @@ function timestamp(value) {
|
|
|
1786
1992
|
function hostProducerFrameKind(value) {
|
|
1787
1993
|
return isHostProducerFrameKind(value);
|
|
1788
1994
|
}
|
|
1789
|
-
function
|
|
1995
|
+
function exactKeys2(value, keys) {
|
|
1790
1996
|
return JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort());
|
|
1791
1997
|
}
|
|
1792
1998
|
function exactOptional(value, required, optional) {
|
|
@@ -1794,6 +2000,15 @@ function exactOptional(value, required, optional) {
|
|
|
1794
2000
|
return required.every((key) => keys.includes(key)) && keys.every((key) => required.includes(key) || optional.includes(key));
|
|
1795
2001
|
}
|
|
1796
2002
|
|
|
2003
|
+
// host/relay/src/host-producer-core.ts
|
|
2004
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2005
|
+
import { O_NOFOLLOW, O_RDONLY as O_RDONLY2 } from "node:constants";
|
|
2006
|
+
import { mkdir as mkdir2, open as open2, readFile, rename as rename2, writeFile } from "node:fs/promises";
|
|
2007
|
+
import { join as join2 } from "node:path";
|
|
2008
|
+
|
|
2009
|
+
// host/relay/src/relay-ipc.ts
|
|
2010
|
+
import { createConnection, createServer } from "node:net";
|
|
2011
|
+
|
|
1797
2012
|
// host/relay/src/durable-atomic-replacement.ts
|
|
1798
2013
|
import { O_RDONLY } from "node:constants";
|
|
1799
2014
|
import { lstat, mkdir, open, rename, unlink } from "node:fs/promises";
|
|
@@ -1882,17 +2097,6 @@ function errorCode(error) {
|
|
|
1882
2097
|
// host/relay/src/relay-request-binding-store.ts
|
|
1883
2098
|
var MAX_STATE_BYTES = 8 * 1024 * 1024;
|
|
1884
2099
|
|
|
1885
|
-
// host/relay/src/relay-quarantine-store.ts
|
|
1886
|
-
var RELAY_QUARANTINE_MAX_BYTES = 16 * 1024 * 1024;
|
|
1887
|
-
|
|
1888
|
-
// host/relay/src/relay-admission-store.ts
|
|
1889
|
-
var DEFAULT_LIMITS = { global_count: 8192, global_bytes: 64 * 1024 * 1024, producer_count: 4096, producer_bytes: 32 * 1024 * 1024 };
|
|
1890
|
-
var RETRY_SECONDS = [1, 2, 4, 8, 16, 32, 60];
|
|
1891
|
-
var MAX_RETRYABLE_ATTEMPTS = RETRY_SECONDS.length + 10;
|
|
1892
|
-
|
|
1893
|
-
// host/relay/src/relay-ipc.ts
|
|
1894
|
-
import { createConnection, createServer } from "node:net";
|
|
1895
|
-
|
|
1896
2100
|
// host/relay/src/current-prompt-custody.ts
|
|
1897
2101
|
var HOST_CURRENT_PROMPT_MAX_BYTES = 256 * 1024;
|
|
1898
2102
|
|
|
@@ -1925,10 +2129,31 @@ var HOST_RELAY_INVOCATION_MINT_REFUSAL_REASONS = vocabulary([
|
|
|
1925
2129
|
"gateway_unavailable",
|
|
1926
2130
|
"gateway_refused",
|
|
1927
2131
|
"gateway_response_invalid",
|
|
2132
|
+
"gateway_rate_limited",
|
|
2133
|
+
"producer_frame_invalid",
|
|
1928
2134
|
"deadline",
|
|
1929
2135
|
"cancelled",
|
|
1930
2136
|
"mint_failed",
|
|
1931
|
-
"relay_failed"
|
|
2137
|
+
"relay_failed",
|
|
2138
|
+
"request_authority_unavailable",
|
|
2139
|
+
"request_missing",
|
|
2140
|
+
"authority_mismatch",
|
|
2141
|
+
"host_source_mismatch",
|
|
2142
|
+
"host_session_mismatch",
|
|
2143
|
+
"host_turn_mismatch",
|
|
2144
|
+
"host_input_mismatch",
|
|
2145
|
+
"host_prompt_mismatch"
|
|
2146
|
+
]);
|
|
2147
|
+
var HOST_RELAY_DIRECT_WIRE_MINT_REASONS = vocabulary([
|
|
2148
|
+
"context_stale",
|
|
2149
|
+
"context_expired",
|
|
2150
|
+
"host_source_refused",
|
|
2151
|
+
"host_credential_refused",
|
|
2152
|
+
"gateway_unavailable",
|
|
2153
|
+
"gateway_rate_limited",
|
|
2154
|
+
"gateway_refused",
|
|
2155
|
+
"gateway_response_invalid",
|
|
2156
|
+
"producer_frame_invalid"
|
|
1932
2157
|
]);
|
|
1933
2158
|
var HOST_RELAY_INVOCATION_MINT_OUTCOMES = vocabulary(["completed", "caller_cancelled", "deadline", "failed", "no_binding"]);
|
|
1934
2159
|
var HOST_RELAY_EXCHANGE_FAILURES = vocabulary([
|
|
@@ -1945,12 +2170,15 @@ var HostRelayIpcError = class extends Error {
|
|
|
1945
2170
|
code;
|
|
1946
2171
|
/** Set only when the client-side socket exchange itself failed. */
|
|
1947
2172
|
failure;
|
|
1948
|
-
constructor(
|
|
2173
|
+
constructor(code2, message = code2, failure) {
|
|
1949
2174
|
super(message);
|
|
1950
|
-
this.code =
|
|
2175
|
+
this.code = code2;
|
|
1951
2176
|
this.failure = failure;
|
|
1952
2177
|
}
|
|
1953
2178
|
};
|
|
2179
|
+
function hostRelayExchangeFailure(error) {
|
|
2180
|
+
return error instanceof HostRelayIpcError ? error.failure : void 0;
|
|
2181
|
+
}
|
|
1954
2182
|
function socketErrorFailure(error) {
|
|
1955
2183
|
switch (errorCode2(error)) {
|
|
1956
2184
|
case "EACCES":
|
|
@@ -1994,7 +2222,7 @@ function submitHostProducerFrame(endpoint, frame, timeoutMs = 5e3) {
|
|
|
1994
2222
|
return exchange(endpoint, encodeHostProducerFrame(frame), timeoutMs, decodeResponseBuffer);
|
|
1995
2223
|
}
|
|
1996
2224
|
function exchange(endpoint, encoded, timeoutMs, decode, signal) {
|
|
1997
|
-
return new Promise((
|
|
2225
|
+
return new Promise((resolve2, reject) => {
|
|
1998
2226
|
const socket = createConnection(endpoint);
|
|
1999
2227
|
let buffer = Buffer.alloc(0);
|
|
2000
2228
|
let settled = false;
|
|
@@ -2005,7 +2233,7 @@ function exchange(endpoint, encoded, timeoutMs, decode, signal) {
|
|
|
2005
2233
|
signal?.removeEventListener("abort", aborted);
|
|
2006
2234
|
socket.destroy();
|
|
2007
2235
|
if (error !== null) reject(error);
|
|
2008
|
-
else if (response2 !== void 0)
|
|
2236
|
+
else if (response2 !== void 0) resolve2(response2);
|
|
2009
2237
|
};
|
|
2010
2238
|
if (timeoutMs !== void 0) socket.setTimeout(timeoutMs, () => finish(new HostRelayIpcError("relay_unavailable", "Host Relay IPC timed out.", "timed_out")));
|
|
2011
2239
|
if (signal?.aborted === true) {
|
|
@@ -2071,7 +2299,7 @@ function decodeResponseBuffer(buffer) {
|
|
|
2071
2299
|
function decodeCurrentPromptResponseBuffer(buffer) {
|
|
2072
2300
|
const value = decodeLengthPrefixedJson(buffer, HOST_RELAY_ADMISSION_RESPONSE_MAX_BYTES);
|
|
2073
2301
|
if (value === null) return null;
|
|
2074
|
-
if (!record3(value) || !
|
|
2302
|
+
if (!record3(value) || !exactKeys3(value, ["schema_version", "status"]) || value.schema_version !== HOST_RELAY_CURRENT_PROMPT_RESULT_SCHEMA || value.status !== "registered" && value.status !== "current_prompt_unavailable") throw new HostRelayIpcError("relay_unavailable");
|
|
2075
2303
|
return { schema_version: HOST_RELAY_CURRENT_PROMPT_RESULT_SCHEMA, status: value.status };
|
|
2076
2304
|
}
|
|
2077
2305
|
function decodeLengthPrefixedJson(input, maximum) {
|
|
@@ -2093,327 +2321,1490 @@ function encodeLengthPrefixed(value) {
|
|
|
2093
2321
|
body.copy(output, HOST_RELAY_IPC_LENGTH_BYTES);
|
|
2094
2322
|
return output;
|
|
2095
2323
|
}
|
|
2096
|
-
function
|
|
2097
|
-
|
|
2324
|
+
function exactKeys3(value, required, optional = []) {
|
|
2325
|
+
const keys = Object.keys(value);
|
|
2326
|
+
return required.every((key) => Object.hasOwn(value, key)) && keys.every((key) => required.includes(key) || optional.includes(key));
|
|
2098
2327
|
}
|
|
2099
2328
|
function errorCode2(error) {
|
|
2100
2329
|
return record3(error) && typeof error.code === "string" ? error.code : null;
|
|
2101
2330
|
}
|
|
2102
2331
|
|
|
2103
|
-
// host/
|
|
2104
|
-
var
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
function validateClaudeIdentity(value) {
|
|
2147
|
-
if (!validIdentity(value)) throw new ClaudeHostError("invalid_input", "Claude Host session identity is invalid.");
|
|
2148
|
-
}
|
|
2149
|
-
function contextShape(value) {
|
|
2150
|
-
return record4(value) && value.schema_version === CLAUDE_INVOCATION_CONTEXT_SCHEMA_VERSION && hasHostInvocationContextKeys(value, ["request_ref", "bound_at", "working_directory"]);
|
|
2332
|
+
// host/relay/src/host-producer-core.ts
|
|
2333
|
+
var HOST_TRANSCRIPT_CURSOR_REASONS = vocabulary(["transcript_replaced", "transcript_truncated", "transcript_rewritten", "transcript_malformed"]);
|
|
2334
|
+
function isHostTranscriptCursorReason(value) {
|
|
2335
|
+
return typeof value === "string" && HOST_TRANSCRIPT_CURSOR_REASONS.some((reason) => reason === value);
|
|
2336
|
+
}
|
|
2337
|
+
var TRANSCRIPT_MISSING_REASON = "transcript_missing";
|
|
2338
|
+
var HOST_TRANSCRIPT_READ_CHUNK_BYTES = 256 * 1024;
|
|
2339
|
+
async function readTranscriptCompleteChunk(handle, offset, size) {
|
|
2340
|
+
const available = Math.max(0, size - offset);
|
|
2341
|
+
if (available === 0) return "empty";
|
|
2342
|
+
const chunks = [];
|
|
2343
|
+
let position = offset;
|
|
2344
|
+
let total = 0;
|
|
2345
|
+
let lastNewline = -1;
|
|
2346
|
+
while (position < size) {
|
|
2347
|
+
const buffer = Buffer.alloc(Math.min(HOST_TRANSCRIPT_READ_CHUNK_BYTES, size - position));
|
|
2348
|
+
const read = await handle.read(buffer, 0, buffer.length, position);
|
|
2349
|
+
if (read.bytesRead === 0) break;
|
|
2350
|
+
const chunk = buffer.subarray(0, read.bytesRead);
|
|
2351
|
+
chunks.push(chunk);
|
|
2352
|
+
const newline = chunk.lastIndexOf(10);
|
|
2353
|
+
if (newline >= 0) lastNewline = total + newline;
|
|
2354
|
+
total += chunk.length;
|
|
2355
|
+
position += chunk.length;
|
|
2356
|
+
if (lastNewline >= 0) break;
|
|
2357
|
+
}
|
|
2358
|
+
if (lastNewline < 0) return "partial";
|
|
2359
|
+
return Buffer.concat(chunks, total).subarray(0, lastNewline + 1);
|
|
2360
|
+
}
|
|
2361
|
+
async function readTranscriptAnchor(handle, offset, anchorBytes) {
|
|
2362
|
+
const start = Math.max(0, offset - anchorBytes);
|
|
2363
|
+
const anchor = Buffer.alloc(offset - start);
|
|
2364
|
+
if (anchor.length > 0) await handle.read(anchor, 0, anchor.length, start);
|
|
2365
|
+
return { start, sha256: transcriptSha256(anchor) };
|
|
2366
|
+
}
|
|
2367
|
+
async function transcriptAnchorMatches(handle, cursor) {
|
|
2368
|
+
const buffer = Buffer.alloc(cursor.offset - cursor.anchor_start);
|
|
2369
|
+
if (buffer.length > 0) await handle.read(buffer, 0, buffer.length, cursor.anchor_start);
|
|
2370
|
+
return transcriptSha256(buffer) === cursor.anchor_sha256;
|
|
2371
|
+
}
|
|
2372
|
+
function transcriptCursorIdentityReason(cursor, transcriptPath, device, inode) {
|
|
2373
|
+
if (cursor.device !== device || cursor.inode !== inode || cursor.transcript_path !== transcriptPath) return "transcript_replaced";
|
|
2374
|
+
return null;
|
|
2151
2375
|
}
|
|
2152
|
-
function
|
|
2153
|
-
if (
|
|
2376
|
+
async function transcriptCursorBoundsReason(handle, cursor, size) {
|
|
2377
|
+
if (size < cursor.offset) return "transcript_truncated";
|
|
2378
|
+
if (cursor.offset > 0 && !await transcriptAnchorMatches(handle, cursor)) return "transcript_rewritten";
|
|
2379
|
+
return null;
|
|
2154
2380
|
}
|
|
2155
|
-
function
|
|
2156
|
-
|
|
2381
|
+
function transcriptSha256(value) {
|
|
2382
|
+
return createHash3("sha256").update(value).digest("hex");
|
|
2157
2383
|
}
|
|
2158
|
-
function
|
|
2159
|
-
|
|
2160
|
-
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)}`)}).`);
|
|
2384
|
+
function transcriptObjectSha256(value) {
|
|
2385
|
+
return createHash3("sha256").update(JSON.stringify(value), "utf8").digest("hex");
|
|
2161
2386
|
}
|
|
2162
|
-
function
|
|
2163
|
-
return
|
|
2387
|
+
function isTranscriptRecord(value) {
|
|
2388
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2164
2389
|
}
|
|
2165
|
-
function
|
|
2166
|
-
return
|
|
2390
|
+
function isMissingTranscriptError(error) {
|
|
2391
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2167
2392
|
}
|
|
2168
|
-
function
|
|
2169
|
-
|
|
2393
|
+
function transcriptTimestamp(now, fail) {
|
|
2394
|
+
const value = now();
|
|
2395
|
+
if (!Number.isFinite(value.getTime())) throw fail();
|
|
2396
|
+
return value.toISOString();
|
|
2170
2397
|
}
|
|
2171
|
-
function
|
|
2172
|
-
return
|
|
2398
|
+
function transcriptExpiration(now, retentionMs, fail) {
|
|
2399
|
+
return new Date(Date.parse(transcriptTimestamp(now, fail)) + retentionMs).toISOString();
|
|
2173
2400
|
}
|
|
2174
|
-
function
|
|
2175
|
-
|
|
2401
|
+
function createTranscriptWatcherBase(options, owner) {
|
|
2402
|
+
if (typeof options.outbox_dir !== "string" || options.outbox_dir.length === 0) throw owner.invalid_outbox();
|
|
2403
|
+
return {
|
|
2404
|
+
outboxRetentionMs: options.retention_ms ?? owner.default_retention_ms,
|
|
2405
|
+
now: options.now ?? (() => /* @__PURE__ */ new Date()),
|
|
2406
|
+
log: options.log ?? ((line) => {
|
|
2407
|
+
process.stderr.write(`${line}
|
|
2408
|
+
`);
|
|
2409
|
+
}),
|
|
2410
|
+
producerInstallationRef: options.producer_installation_ref
|
|
2411
|
+
};
|
|
2176
2412
|
}
|
|
2177
|
-
function
|
|
2178
|
-
|
|
2413
|
+
async function scanTranscriptRegistrations(registrations, scanOne) {
|
|
2414
|
+
let records = 0;
|
|
2415
|
+
let enqueued = 0;
|
|
2416
|
+
let quarantined = 0;
|
|
2417
|
+
for (const registration of registrations) {
|
|
2418
|
+
const result = await scanOne(registration);
|
|
2419
|
+
records += result.records;
|
|
2420
|
+
enqueued += result.enqueued;
|
|
2421
|
+
if (result.quarantined) quarantined += 1;
|
|
2422
|
+
}
|
|
2423
|
+
return { registrations: registrations.length, records, enqueued, quarantined };
|
|
2179
2424
|
}
|
|
2180
|
-
function
|
|
2181
|
-
return
|
|
2425
|
+
function formatRetiredTranscriptLog(producerLabel, transcriptPath) {
|
|
2426
|
+
return `${producerLabel} retired a transcript registration: transcript_path=${JSON.stringify(transcriptPath)}, reason=${TRANSCRIPT_MISSING_REASON}.`;
|
|
2182
2427
|
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
this.
|
|
2193
|
-
this.
|
|
2194
|
-
this.
|
|
2428
|
+
var HostTranscriptWatcher = class {
|
|
2429
|
+
outboxRetentionMs;
|
|
2430
|
+
now;
|
|
2431
|
+
log;
|
|
2432
|
+
producerInstallationRef;
|
|
2433
|
+
clockFailure;
|
|
2434
|
+
constructor(options, owner) {
|
|
2435
|
+
const base = createTranscriptWatcherBase(options, owner);
|
|
2436
|
+
this.outboxRetentionMs = base.outboxRetentionMs;
|
|
2437
|
+
this.now = base.now;
|
|
2438
|
+
this.log = base.log;
|
|
2439
|
+
this.producerInstallationRef = base.producerInstallationRef;
|
|
2440
|
+
this.clockFailure = owner.clock_failure;
|
|
2195
2441
|
}
|
|
2196
|
-
async
|
|
2197
|
-
await
|
|
2198
|
-
await mkdir2(this.activeContexts, { recursive: true, mode: 448 });
|
|
2442
|
+
async scanOnce() {
|
|
2443
|
+
return scanTranscriptRegistrations(await this.listRegistrations(), (registration) => this.scanRegistration(registration));
|
|
2199
2444
|
}
|
|
2200
|
-
async
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2445
|
+
async scanRegistration(registration) {
|
|
2446
|
+
return this.openTranscript(registration, (handle) => this.scanOpened(handle, registration), {
|
|
2447
|
+
missing: () => this.retireMissing(registration),
|
|
2448
|
+
failed: (handle, error) => this.scanFailed(handle, registration, error)
|
|
2449
|
+
});
|
|
2204
2450
|
}
|
|
2205
|
-
async
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2451
|
+
async openTranscript(registration, scan, handlers) {
|
|
2452
|
+
let handle;
|
|
2453
|
+
try {
|
|
2454
|
+
handle = await open2(this.transcriptPathOf(registration), O_RDONLY2 | O_NOFOLLOW);
|
|
2455
|
+
return await scan(handle);
|
|
2456
|
+
} catch (error) {
|
|
2457
|
+
if (handle === void 0 && isMissingTranscriptError(error)) return handlers.missing();
|
|
2458
|
+
if (hostRelayExchangeFailure(error) !== void 0) throw error;
|
|
2459
|
+
return handlers.failed(handle, error);
|
|
2460
|
+
} finally {
|
|
2461
|
+
await handle?.close();
|
|
2462
|
+
}
|
|
2209
2463
|
}
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2464
|
+
/**
|
|
2465
|
+
* A transcript the Host deleted never comes back, so retrying its
|
|
2466
|
+
* registration once per reconcile interval is a failure with no end.
|
|
2467
|
+
* Retire it at the scan that finds it missing and say so once. Only
|
|
2468
|
+
* ENOENT means gone; a permission or busy failure is about this moment,
|
|
2469
|
+
* not about the file, and keeps the registration for the next scan.
|
|
2470
|
+
*/
|
|
2471
|
+
async retireMissingTranscript(transcriptPath, producerLabel, retire) {
|
|
2472
|
+
await retire();
|
|
2473
|
+
this.log(formatRetiredTranscriptLog(producerLabel, transcriptPath));
|
|
2474
|
+
return { records: 0, enqueued: 0, quarantined: false };
|
|
2475
|
+
}
|
|
2476
|
+
stamp() {
|
|
2477
|
+
return transcriptTimestamp(this.now, this.clockFailure);
|
|
2478
|
+
}
|
|
2479
|
+
expire(retentionMs) {
|
|
2480
|
+
return transcriptExpiration(this.now, retentionMs, this.clockFailure);
|
|
2481
|
+
}
|
|
2482
|
+
sha(value) {
|
|
2483
|
+
return transcriptSha256(value);
|
|
2484
|
+
}
|
|
2485
|
+
objectSha(value) {
|
|
2486
|
+
return transcriptObjectSha256(value);
|
|
2487
|
+
}
|
|
2488
|
+
isRecord(value) {
|
|
2489
|
+
return isTranscriptRecord(value);
|
|
2490
|
+
}
|
|
2491
|
+
readChunk(handle, offset, size) {
|
|
2492
|
+
return readTranscriptCompleteChunk(handle, offset, size);
|
|
2493
|
+
}
|
|
2494
|
+
readAnchor(handle, offset, anchorBytes) {
|
|
2495
|
+
return readTranscriptAnchor(handle, offset, anchorBytes);
|
|
2496
|
+
}
|
|
2497
|
+
cursorIdentityReason(cursor, transcriptPath, device, inode) {
|
|
2498
|
+
return transcriptCursorIdentityReason(cursor, transcriptPath, device, inode);
|
|
2499
|
+
}
|
|
2500
|
+
cursorBoundsReason(handle, cursor, size) {
|
|
2501
|
+
return transcriptCursorBoundsReason(handle, cursor, size);
|
|
2502
|
+
}
|
|
2503
|
+
};
|
|
2504
|
+
var HOST_PRODUCER_STATE_IDENTITY = {
|
|
2505
|
+
codex: { file_name: "codex-producer-state.json", schema: "ceal.codex_producer_state.v1" },
|
|
2506
|
+
claude: { file_name: "claude-producer-state.json", schema: "ceal.claude_producer_state.v1" }
|
|
2507
|
+
};
|
|
2508
|
+
var HostProducerDrainCore = class {
|
|
2509
|
+
host;
|
|
2510
|
+
relay;
|
|
2511
|
+
statePath;
|
|
2512
|
+
stateSchema;
|
|
2513
|
+
installationRef;
|
|
2514
|
+
epoch;
|
|
2515
|
+
now;
|
|
2516
|
+
tail = Promise.resolve();
|
|
2517
|
+
constructor(host, runtime) {
|
|
2518
|
+
if (!isHostSafeRef(runtime.producer_installation_ref)) throw host.fail("configuration", `${host.producer_label} producer installation identity is invalid.`);
|
|
2519
|
+
const epoch = runtime.producer_epoch ?? 1;
|
|
2520
|
+
if (!Number.isSafeInteger(epoch) || epoch <= 0) throw host.fail("configuration", `${host.producer_label} producer epoch is invalid.`);
|
|
2521
|
+
this.host = host;
|
|
2522
|
+
this.relay = runtime.relay;
|
|
2523
|
+
this.statePath = join2(host.outbox_directory, HOST_PRODUCER_STATE_IDENTITY[host.producer_id].file_name);
|
|
2524
|
+
this.stateSchema = HOST_PRODUCER_STATE_IDENTITY[host.producer_id].schema;
|
|
2525
|
+
this.installationRef = runtime.producer_installation_ref;
|
|
2526
|
+
this.epoch = epoch;
|
|
2527
|
+
this.now = runtime.now ?? (() => /* @__PURE__ */ new Date());
|
|
2528
|
+
}
|
|
2529
|
+
drainOutbox() {
|
|
2530
|
+
const run = this.tail.then(() => this.drainLocked());
|
|
2531
|
+
this.tail = run.then(() => void 0, () => void 0);
|
|
2532
|
+
return run;
|
|
2533
|
+
}
|
|
2534
|
+
async drainLocked() {
|
|
2535
|
+
const read = await this.host.listEnvelopes();
|
|
2536
|
+
let admitted = 0;
|
|
2537
|
+
let expired = 0;
|
|
2538
|
+
let retained = 0;
|
|
2539
|
+
for (const envelope of read.entries) {
|
|
2540
|
+
const reserved = await this.hasReservation(this.host.submissionRefOf(envelope));
|
|
2541
|
+
if (!reserved && Date.parse(this.host.expiresAtOf(envelope)) <= this.now().getTime()) {
|
|
2542
|
+
await this.host.removeEnvelope(envelope);
|
|
2543
|
+
expired += 1;
|
|
2544
|
+
continue;
|
|
2545
|
+
}
|
|
2546
|
+
const frames = await this.reserve(envelope);
|
|
2547
|
+
let complete = true;
|
|
2548
|
+
for (const frame of frames) {
|
|
2549
|
+
const outcome = await this.relay.admit(frame);
|
|
2550
|
+
if (outcome.code !== "admitted" && outcome.code !== "already_admitted") {
|
|
2551
|
+
complete = false;
|
|
2552
|
+
break;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
if (!complete) {
|
|
2556
|
+
retained += 1;
|
|
2557
|
+
break;
|
|
2558
|
+
}
|
|
2559
|
+
await this.host.removeEnvelope(envelope);
|
|
2560
|
+
await this.release(this.host.submissionRefOf(envelope));
|
|
2561
|
+
admitted += 1;
|
|
2562
|
+
}
|
|
2563
|
+
return { admitted, retained: retained + Math.max(0, read.entries.length - admitted - expired - retained), expired, malformed: read.malformed };
|
|
2564
|
+
}
|
|
2565
|
+
async hasReservation(submissionRef2) {
|
|
2566
|
+
return (await this.readState()).pending[submissionRef2] !== void 0;
|
|
2567
|
+
}
|
|
2568
|
+
async reserve(envelope) {
|
|
2569
|
+
const state = await this.readState();
|
|
2570
|
+
const submissionRef2 = this.host.submissionRefOf(envelope);
|
|
2571
|
+
const existing = state.pending[submissionRef2];
|
|
2572
|
+
if (existing !== void 0) return existing;
|
|
2573
|
+
const drafts = await this.host.mapEnvelope(envelope);
|
|
2574
|
+
const frames = drafts.map((draft, offset) => ({
|
|
2575
|
+
schema_version: "ceal.host_producer_frame.v1",
|
|
2576
|
+
producer_id: this.host.producer_id,
|
|
2577
|
+
producer_installation_ref: this.installationRef,
|
|
2578
|
+
producer_epoch: this.epoch,
|
|
2579
|
+
sequence: state.next_sequence + offset,
|
|
2580
|
+
frame_kind: draft.frame_kind,
|
|
2581
|
+
occurred_at: this.host.createdAtOf(envelope),
|
|
2582
|
+
payload: draft.payload
|
|
2583
|
+
}));
|
|
2584
|
+
await this.writeState({ ...state, next_sequence: state.next_sequence + frames.length, pending: { ...state.pending, [submissionRef2]: frames } });
|
|
2585
|
+
return frames;
|
|
2586
|
+
}
|
|
2587
|
+
async release(submissionRef2) {
|
|
2588
|
+
const state = await this.readState();
|
|
2589
|
+
if (state.pending[submissionRef2] === void 0) return;
|
|
2590
|
+
const pending = { ...state.pending };
|
|
2591
|
+
delete pending[submissionRef2];
|
|
2592
|
+
await this.writeState({ ...state, pending });
|
|
2593
|
+
}
|
|
2594
|
+
async readState() {
|
|
2595
|
+
try {
|
|
2596
|
+
const value = JSON.parse(await readFile(this.statePath, "utf8"));
|
|
2597
|
+
return this.decodeState(value);
|
|
2598
|
+
} catch (error) {
|
|
2599
|
+
if (errorCode3(error) !== "ENOENT") throw error;
|
|
2600
|
+
return { schema_version: this.stateSchema, producer_installation_ref: this.installationRef, producer_epoch: this.epoch, next_sequence: 1, pending: {} };
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
async writeState(state) {
|
|
2604
|
+
await mkdir2(this.host.outbox_directory, { recursive: true, mode: 448 });
|
|
2605
|
+
const temporary = `${this.statePath}.${process.pid}.new`;
|
|
2606
|
+
await writeFile(temporary, JSON.stringify(state), { mode: 384 });
|
|
2607
|
+
await rename2(temporary, this.statePath);
|
|
2608
|
+
}
|
|
2609
|
+
decodeState(value) {
|
|
2610
|
+
const label = this.host.producer_label;
|
|
2611
|
+
const fail = (code2, message) => this.host.fail(code2, message);
|
|
2612
|
+
if (!isDrainStateHeader(value, this.stateSchema, this.installationRef)) {
|
|
2613
|
+
throw fail("storage", `${label} producer state header is malformed or belongs to another installation (${refusalFacts([
|
|
2614
|
+
{ name: "schema_version", value: isTranscriptRecord(value) ? value.schema_version : void 0, expected: this.stateSchema },
|
|
2615
|
+
{ name: "producer_installation_ref", value: isTranscriptRecord(value) ? value.producer_installation_ref : void 0, expected: this.installationRef }
|
|
2616
|
+
])}).`);
|
|
2617
|
+
}
|
|
2618
|
+
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}`)}).`);
|
|
2619
|
+
const pending = {};
|
|
2620
|
+
const storedEpoch = Number(value.producer_epoch);
|
|
2621
|
+
for (const [submissionRef2, frames] of Object.entries(value.pending)) {
|
|
2622
|
+
if (!isHostSafeRef(submissionRef2) || !Array.isArray(frames) || frames.length === 0) throw fail("storage", `${label} producer pending reservation is malformed.`);
|
|
2623
|
+
pending[submissionRef2] = frames.map((frame) => this.decodeStoredFrame(frame, storedEpoch));
|
|
2624
|
+
}
|
|
2625
|
+
return { schema_version: this.stateSchema, producer_installation_ref: this.installationRef, producer_epoch: this.epoch, next_sequence: Number(value.next_sequence), pending };
|
|
2626
|
+
}
|
|
2627
|
+
decodeStoredFrame(value, storedEpoch) {
|
|
2628
|
+
const label = this.host.producer_label;
|
|
2629
|
+
const json = Buffer.from(JSON.stringify(value), "utf8");
|
|
2630
|
+
const encoded = Buffer.allocUnsafe(json.length + 4);
|
|
2631
|
+
encoded.writeUInt32BE(json.length, 0);
|
|
2632
|
+
json.copy(encoded, 4);
|
|
2633
|
+
const frame = decodeHostProducerFrame(encoded);
|
|
2634
|
+
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([
|
|
2635
|
+
{ name: "producer_id", value: frame.producer_id, expected: this.host.producer_id },
|
|
2636
|
+
{ name: "producer_installation_ref", value: frame.producer_installation_ref, expected: this.installationRef },
|
|
2637
|
+
{ name: "producer_epoch", value: frame.producer_epoch, expected: `at most ${storedEpoch}` }
|
|
2638
|
+
])}).`);
|
|
2639
|
+
return frame;
|
|
2640
|
+
}
|
|
2641
|
+
};
|
|
2642
|
+
function isDrainStateHeader(value, schema, installationRef) {
|
|
2643
|
+
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);
|
|
2644
|
+
}
|
|
2645
|
+
function errorCode3(error) {
|
|
2646
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : null;
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// host/relay/src/relay-quarantine-store.ts
|
|
2650
|
+
var RELAY_QUARANTINE_MAX_BYTES = 16 * 1024 * 1024;
|
|
2651
|
+
var RELAY_QUARANTINE_FRAME_REASONS = ["frame_identity_conflict", "sequence_gap", "gateway_refused"];
|
|
2652
|
+
var RELAY_QUARANTINE_REASONS = [...RELAY_QUARANTINE_FRAME_REASONS, ...HOST_PRODUCER_FRAME_ERROR_CODES];
|
|
2653
|
+
|
|
2654
|
+
// host/relay/src/relay-admission-store.ts
|
|
2655
|
+
var DEFAULT_LIMITS = { global_count: 8192, global_bytes: 64 * 1024 * 1024, producer_count: 4096, producer_bytes: 32 * 1024 * 1024 };
|
|
2656
|
+
var RETRY_SECONDS = [1, 2, 4, 8, 16, 32, 60];
|
|
2657
|
+
var MAX_RETRYABLE_ATTEMPTS = RETRY_SECONDS.length + 10;
|
|
2658
|
+
|
|
2659
|
+
// host/claude/src/claude-invocation-context.ts
|
|
2660
|
+
function claudeInvocationContextFromTurn(identity, promptRef, expiresAt, workingDirectory, turnGenerationRef) {
|
|
2661
|
+
return invocationContextFromTurn(identity, promptRef, expiresAt, { schema_version: CLAUDE_INVOCATION_CONTEXT_SCHEMA_VERSION, validate: validateClaudeHostInvocationContext, working_directory: workingDirectory, turn_generation_ref: turnGenerationRef });
|
|
2662
|
+
}
|
|
2663
|
+
function validateClaudeHostInvocationContext(value) {
|
|
2664
|
+
if (!contextShape(value)) throw new ClaudeHostError("storage", "Claude Host invocation context is malformed.");
|
|
2665
|
+
validateIdentity(value.identity);
|
|
2666
|
+
validateContextReferences(value);
|
|
2667
|
+
validateContextTimes(value);
|
|
2668
|
+
}
|
|
2669
|
+
function contextShape(value) {
|
|
2670
|
+
return record4(value) && value.schema_version === CLAUDE_INVOCATION_CONTEXT_SCHEMA_VERSION && hasHostInvocationContextKeys(value, ["request_ref", "bound_at", "working_directory", "turn_generation_ref"]);
|
|
2671
|
+
}
|
|
2672
|
+
function validateIdentity(value) {
|
|
2673
|
+
if (!validIdentity(value)) throw new ClaudeHostError("storage", "Claude Host invocation identity is malformed.");
|
|
2674
|
+
}
|
|
2675
|
+
function validateContextReferences(value) {
|
|
2676
|
+
if (!safeRef3(value.prompt_ref) || !optionalSafeRef(value.request_ref) || !optionalSafeRef(value.turn_generation_ref)) throw new ClaudeHostError("storage", "Claude Host invocation context is malformed.");
|
|
2677
|
+
}
|
|
2678
|
+
function validateContextTimes(value) {
|
|
2679
|
+
if (!timestamp2(value.expires_at) || !optionalTimestamp(value.bound_at) || !optionalDirectory(value.working_directory)) throw new ClaudeHostError("storage", "Claude Host invocation context is malformed.");
|
|
2680
|
+
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)}`)}).`);
|
|
2681
|
+
}
|
|
2682
|
+
function validIdentity(value) {
|
|
2683
|
+
return record4(value) && hasHostInvocationIdentityKeys(value) && [value.host_session_ref, value.host_turn_ref, value.host_source_ref].every(safeRef3);
|
|
2684
|
+
}
|
|
2685
|
+
function optionalSafeRef(value) {
|
|
2686
|
+
return value === void 0 || safeRef3(value);
|
|
2687
|
+
}
|
|
2688
|
+
function optionalTimestamp(value) {
|
|
2689
|
+
return value === void 0 || timestamp2(value);
|
|
2690
|
+
}
|
|
2691
|
+
function optionalDirectory(value) {
|
|
2692
|
+
return value === void 0 || typeof value === "string" && value.startsWith("/") && !value.includes("\0") && value.length <= 4096;
|
|
2693
|
+
}
|
|
2694
|
+
function safeRef3(value) {
|
|
2695
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value);
|
|
2696
|
+
}
|
|
2697
|
+
function timestamp2(value) {
|
|
2698
|
+
return typeof value === "string" && value.length <= 64 && Number.isFinite(Date.parse(value));
|
|
2699
|
+
}
|
|
2700
|
+
function record4(value) {
|
|
2701
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
// host/claude/src/claude-outbox.ts
|
|
2705
|
+
var SAFE_REF3 = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
2706
|
+
var CLAUDE_TRANSCRIPT_PATH_MAX_CHARACTERS = 4096;
|
|
2707
|
+
var CLAUDE_TRANSCRIPT_SUBAGENT_NAME_MAX_CHARACTERS = 256;
|
|
2708
|
+
var ClaudeOutboxStore = class {
|
|
2709
|
+
envelopes;
|
|
2710
|
+
sessions;
|
|
2711
|
+
registrations;
|
|
2712
|
+
cursors;
|
|
2713
|
+
directory;
|
|
2714
|
+
activeContexts;
|
|
2715
|
+
turnSelections;
|
|
2716
|
+
constructor(directory) {
|
|
2717
|
+
if (!directory.startsWith("/")) throw new ClaudeHostError("configuration", "Claude outbox directory must be absolute.");
|
|
2718
|
+
this.directory = directory;
|
|
2719
|
+
this.envelopes = join3(directory, "envelopes");
|
|
2720
|
+
this.sessions = join3(directory, "sessions");
|
|
2721
|
+
this.registrations = join3(directory, "transcript-registrations");
|
|
2722
|
+
this.cursors = join3(directory, "transcript-cursors");
|
|
2723
|
+
this.activeContexts = join3(directory, "active-contexts");
|
|
2724
|
+
this.turnSelections = join3(directory, "turn-selections");
|
|
2725
|
+
}
|
|
2726
|
+
async prepare() {
|
|
2727
|
+
for (const path of [this.envelopes, this.sessions, this.registrations, this.cursors, this.activeContexts, this.turnSelections]) {
|
|
2728
|
+
await mkdir3(path, { recursive: true, mode: 448 });
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
async activateInvocationContext(identity, promptRef, expiresAt, workingDirectory, turnGenerationRef) {
|
|
2732
|
+
const context = claudeInvocationContextFromTurn(identity, promptRef, expiresAt, workingDirectory, turnGenerationRef);
|
|
2733
|
+
await this.prepare();
|
|
2734
|
+
await durableAtomicReplace(this.activeContextPath(identity), JSON.stringify(context));
|
|
2735
|
+
}
|
|
2736
|
+
async readInvocationContext(hostSourceRef, hostSessionRef, now = /* @__PURE__ */ new Date()) {
|
|
2737
|
+
validateInvocationContextReadArguments(hostSourceRef, hostSessionRef, now);
|
|
2738
|
+
const context = await this.readRawInvocationContext(hostSourceRef, hostSessionRef);
|
|
2739
|
+
return isLiveSessionProjection(context, now) ? context : null;
|
|
2740
|
+
}
|
|
2741
|
+
async writeInvocationTurnSelection(selection) {
|
|
2742
|
+
if (!isHostInvocationTurnSelection(selection)) throw new ClaudeHostError("invalid_input", "Claude Host turn selection is invalid.");
|
|
2743
|
+
await this.prepare();
|
|
2744
|
+
await durableAtomicReplace(this.turnSelectionPath(selection.host_source_ref, selection.host_session_ref), JSON.stringify(selection));
|
|
2745
|
+
}
|
|
2746
|
+
async attachInvocationTurnGeneration(identity, promptRef, turnGenerationRef) {
|
|
2747
|
+
const selection = await this.readInvocationTurnSelection(identity.host_source_ref, identity.host_session_ref);
|
|
2748
|
+
if (selection === null || selection.prompt_ref !== promptRef || selection.turn_generation_ref !== turnGenerationRef) return false;
|
|
2749
|
+
const path = this.activeContextPath(identity);
|
|
2750
|
+
const active = await readProjectionFile(path);
|
|
2751
|
+
if (active === null || active.prompt_ref !== promptRef || active.identity.host_turn_ref !== identity.host_turn_ref) return false;
|
|
2752
|
+
const updated = { ...active, turn_generation_ref: turnGenerationRef };
|
|
2753
|
+
validateClaudeHostInvocationContext(updated);
|
|
2754
|
+
await durableAtomicReplace(path, JSON.stringify(updated));
|
|
2755
|
+
return true;
|
|
2756
|
+
}
|
|
2757
|
+
/**
|
|
2758
|
+
* The latest Claude session projection even when past expiry, so the
|
|
2759
|
+
* invocation authenticator can report an expired context distinctly from
|
|
2760
|
+
* a missing one. Every other caller keeps the fail-closed read above.
|
|
2761
|
+
*/
|
|
2762
|
+
async readRawInvocationContext(hostSourceRef, hostSessionRef) {
|
|
2763
|
+
if (!SAFE_REF3.test(hostSourceRef) || !SAFE_REF3.test(hostSessionRef)) throw new ClaudeHostError("invalid_input", "Claude Host context identity or clock is invalid.");
|
|
2764
|
+
await this.prepare();
|
|
2765
|
+
const context = await readLatestSessionProjection(this.activeContextPath({ host_source_ref: hostSourceRef, host_session_ref: hostSessionRef }), hostSourceRef, hostSessionRef, readProjectionFile);
|
|
2766
|
+
return invocationTurnSelectionMatches(context, await this.readInvocationTurnSelection(hostSourceRef, hostSessionRef)) ? context : null;
|
|
2767
|
+
}
|
|
2768
|
+
async listInvocationContexts(now = /* @__PURE__ */ new Date()) {
|
|
2769
|
+
await this.prepare();
|
|
2770
|
+
const contexts = [];
|
|
2771
|
+
for (const name of await readdir(this.activeContexts)) {
|
|
2772
|
+
if (!name.endsWith(".json")) continue;
|
|
2773
|
+
try {
|
|
2774
|
+
const value = JSON.parse(await readFile2(join3(this.activeContexts, name), "utf8"));
|
|
2217
2775
|
validateClaudeHostInvocationContext(value);
|
|
2218
|
-
if (Date.parse(value.expires_at) > now.getTime()) contexts.push(value);
|
|
2776
|
+
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);
|
|
2219
2777
|
} catch {
|
|
2220
2778
|
}
|
|
2221
2779
|
}
|
|
2222
2780
|
return contexts;
|
|
2223
2781
|
}
|
|
2224
|
-
async
|
|
2225
|
-
|
|
2782
|
+
async putEnvelope(entry) {
|
|
2783
|
+
validateEnvelope(entry);
|
|
2226
2784
|
await this.prepare();
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2785
|
+
await durableAtomicReplace(this.envelopePath(entry.submission_ref), JSON.stringify(entry));
|
|
2786
|
+
}
|
|
2787
|
+
async listEnvelopes() {
|
|
2788
|
+
await this.prepare();
|
|
2789
|
+
const entries = [];
|
|
2790
|
+
let malformed = 0;
|
|
2791
|
+
for (const name of await readdir(this.envelopes)) {
|
|
2792
|
+
if (!name.endsWith(".json")) continue;
|
|
2230
2793
|
try {
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2794
|
+
const value = JSON.parse(await readFile2(join3(this.envelopes, name), "utf8"));
|
|
2795
|
+
validateEnvelope(value);
|
|
2796
|
+
entries.push(value);
|
|
2797
|
+
} catch {
|
|
2798
|
+
malformed += 1;
|
|
2235
2799
|
}
|
|
2800
|
+
}
|
|
2801
|
+
entries.sort((left, right) => left.created_at.localeCompare(right.created_at));
|
|
2802
|
+
return { entries, malformed };
|
|
2803
|
+
}
|
|
2804
|
+
async removeEnvelope(entry) {
|
|
2805
|
+
validateEnvelope(entry);
|
|
2806
|
+
try {
|
|
2807
|
+
await unlink2(this.envelopePath(entry.submission_ref));
|
|
2236
2808
|
} catch (error) {
|
|
2237
|
-
if (code(error) !== "
|
|
2238
|
-
const existing = JSON.parse(await readFile(path, "utf8"));
|
|
2239
|
-
validateBatch(existing);
|
|
2240
|
-
if (JSON.stringify(existing) !== JSON.stringify(batch)) throw new ClaudeHostError("storage", "Claude hook batch identity conflicts with different bytes.");
|
|
2809
|
+
if (code(error) !== "ENOENT") throw error;
|
|
2241
2810
|
}
|
|
2242
2811
|
}
|
|
2243
|
-
async
|
|
2812
|
+
async saveSession(snapshot) {
|
|
2813
|
+
validateSession(snapshot);
|
|
2244
2814
|
await this.prepare();
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2815
|
+
await durableAtomicReplace(join3(this.sessions, `${cealRequestSha256({ source: snapshot.host_source_ref, session: snapshot.host_session_ref })}.json`), JSON.stringify(snapshot));
|
|
2816
|
+
}
|
|
2817
|
+
async readSession(sourceRef, sessionRef) {
|
|
2818
|
+
if (!SAFE_REF3.test(sourceRef) || !SAFE_REF3.test(sessionRef)) throw new ClaudeHostError("invalid_input", "Claude session identity is invalid.");
|
|
2819
|
+
await this.prepare();
|
|
2820
|
+
try {
|
|
2821
|
+
const value = JSON.parse(await readFile2(join3(this.sessions, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`), "utf8"));
|
|
2822
|
+
validateSession(value);
|
|
2823
|
+
return value;
|
|
2824
|
+
} catch (error) {
|
|
2825
|
+
if (code(error) === "ENOENT") return null;
|
|
2826
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Claude session snapshot is malformed.");
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
async registerTranscript(registration) {
|
|
2830
|
+
validateRegistration(registration);
|
|
2831
|
+
await this.prepare();
|
|
2832
|
+
const path = this.registrationPath(registration.host_source_ref, registration.host_session_ref);
|
|
2833
|
+
const existing = await this.readRegistrationFile(path);
|
|
2834
|
+
if (existing !== null && (existing.claude_session_id !== registration.claude_session_id || existing.transcript_path !== registration.transcript_path)) {
|
|
2835
|
+
throw new ClaudeHostError("storage", "Claude transcript registration conflicts with the source-bound session.");
|
|
2836
|
+
}
|
|
2837
|
+
await durableAtomicReplace(path, JSON.stringify(registration));
|
|
2838
|
+
}
|
|
2839
|
+
/**
|
|
2840
|
+
* End a registration whose transcript the Host has deleted. The cursor is
|
|
2841
|
+
* keyed by the same session and describes byte offsets of a file that no
|
|
2842
|
+
* longer exists, so it leaves with the registration.
|
|
2843
|
+
*/
|
|
2844
|
+
async retireTranscriptRegistration(registration) {
|
|
2845
|
+
validateRegistration(registration);
|
|
2846
|
+
await this.prepare();
|
|
2847
|
+
await this.removeStateFile(this.registrationPath(registration.host_source_ref, registration.host_session_ref));
|
|
2848
|
+
await this.removeStateFile(this.cursorPath(registration.host_source_ref, registration.host_session_ref));
|
|
2849
|
+
}
|
|
2850
|
+
async listTranscriptRegistrations() {
|
|
2851
|
+
await this.prepare();
|
|
2852
|
+
const registrations = [];
|
|
2853
|
+
for (const name of await readdir(this.registrations)) {
|
|
2248
2854
|
if (!name.endsWith(".json")) continue;
|
|
2249
2855
|
try {
|
|
2250
|
-
const value = JSON.parse(await
|
|
2251
|
-
|
|
2252
|
-
|
|
2856
|
+
const value = JSON.parse(await readFile2(join3(this.registrations, name), "utf8"));
|
|
2857
|
+
validateRegistration(value);
|
|
2858
|
+
registrations.push(value);
|
|
2253
2859
|
} catch {
|
|
2254
|
-
malformed += 1;
|
|
2255
2860
|
}
|
|
2256
2861
|
}
|
|
2257
|
-
|
|
2258
|
-
return { entries: batches, malformed };
|
|
2862
|
+
return registrations.sort((left, right) => left.registered_at.localeCompare(right.registered_at));
|
|
2259
2863
|
}
|
|
2260
|
-
async
|
|
2864
|
+
async readInvocationTurnSelection(hostSourceRef, hostSessionRef) {
|
|
2865
|
+
if (!SAFE_REF3.test(hostSourceRef) || !SAFE_REF3.test(hostSessionRef)) throw new ClaudeHostError("invalid_input", "Claude Host turn selection identity is invalid.");
|
|
2866
|
+
await this.prepare();
|
|
2261
2867
|
try {
|
|
2262
|
-
await
|
|
2868
|
+
const value = JSON.parse(await readFile2(this.turnSelectionPath(hostSourceRef, hostSessionRef), "utf8"));
|
|
2869
|
+
if (!isHostInvocationTurnSelection(value) || value.host_source_ref !== hostSourceRef || value.host_session_ref !== hostSessionRef) throw new ClaudeHostError("storage", "Claude Host turn selection is malformed.");
|
|
2870
|
+
return value;
|
|
2263
2871
|
} catch (error) {
|
|
2264
|
-
if (code(error)
|
|
2872
|
+
if (code(error) === "ENOENT") return null;
|
|
2873
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Unable to read the Claude Host turn selection.");
|
|
2265
2874
|
}
|
|
2266
2875
|
}
|
|
2267
|
-
|
|
2268
|
-
|
|
2876
|
+
async readTranscriptCursor(registration) {
|
|
2877
|
+
validateRegistration(registration);
|
|
2878
|
+
await this.prepare();
|
|
2879
|
+
try {
|
|
2880
|
+
const value = JSON.parse(await readFile2(this.cursorPath(registration.host_source_ref, registration.host_session_ref), "utf8"));
|
|
2881
|
+
validateCursor(value);
|
|
2882
|
+
return value;
|
|
2883
|
+
} catch (error) {
|
|
2884
|
+
if (code(error) === "ENOENT") return null;
|
|
2885
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Claude transcript cursor is malformed.");
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
async writeTranscriptCursor(cursor) {
|
|
2889
|
+
validateCursor(cursor);
|
|
2890
|
+
await this.prepare();
|
|
2891
|
+
await durableAtomicReplace(this.cursorPath(cursor.host_source_ref, cursor.host_session_ref), JSON.stringify(cursor));
|
|
2892
|
+
}
|
|
2893
|
+
envelopePath(ref) {
|
|
2894
|
+
return join3(this.envelopes, `${cealRequestSha256(ref)}.json`);
|
|
2895
|
+
}
|
|
2896
|
+
registrationPath(sourceRef, sessionRef) {
|
|
2897
|
+
return join3(this.registrations, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`);
|
|
2898
|
+
}
|
|
2899
|
+
turnSelectionPath(sourceRef, sessionRef) {
|
|
2900
|
+
return join3(this.turnSelections, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`);
|
|
2901
|
+
}
|
|
2902
|
+
cursorPath(sourceRef, sessionRef) {
|
|
2903
|
+
return join3(this.cursors, `${cealRequestSha256({ source: sourceRef, session: sessionRef })}.json`);
|
|
2904
|
+
}
|
|
2905
|
+
async readRegistrationFile(path) {
|
|
2906
|
+
try {
|
|
2907
|
+
const value = JSON.parse(await readFile2(path, "utf8"));
|
|
2908
|
+
validateRegistration(value);
|
|
2909
|
+
return value;
|
|
2910
|
+
} catch (error) {
|
|
2911
|
+
if (code(error) === "ENOENT") return null;
|
|
2912
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("storage", "Claude transcript registration is malformed.");
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
async removeStateFile(path) {
|
|
2916
|
+
try {
|
|
2917
|
+
await unlink2(path);
|
|
2918
|
+
} catch (error) {
|
|
2919
|
+
if (code(error) !== "ENOENT") throw error;
|
|
2920
|
+
}
|
|
2269
2921
|
}
|
|
2270
2922
|
activeContextPath(identity) {
|
|
2271
|
-
return
|
|
2923
|
+
return join3(this.activeContexts, `${invocationContextKey(identity)}.json`);
|
|
2272
2924
|
}
|
|
2273
2925
|
};
|
|
2274
2926
|
function validateInvocationContextReadArguments(hostSourceRef, hostSessionRef, now) {
|
|
2275
2927
|
if (!SAFE_REF3.test(hostSourceRef) || !SAFE_REF3.test(hostSessionRef) || !(now instanceof Date) || !Number.isFinite(now.getTime())) throw new ClaudeHostError("invalid_input", "Claude Host context identity or clock is invalid.");
|
|
2276
2928
|
}
|
|
2277
|
-
async function
|
|
2929
|
+
async function readProjectionFile(file) {
|
|
2278
2930
|
try {
|
|
2279
|
-
const context = JSON.parse(await
|
|
2931
|
+
const context = JSON.parse(await readFile2(file, "utf8"));
|
|
2280
2932
|
validateClaudeHostInvocationContext(context);
|
|
2281
|
-
return
|
|
2933
|
+
return context;
|
|
2282
2934
|
} catch (error) {
|
|
2283
2935
|
if (code(error) === "ENOENT") return null;
|
|
2284
2936
|
if (error instanceof ClaudeHostError) throw error;
|
|
2285
2937
|
throw new ClaudeHostError("storage", "Unable to read the Claude Host invocation context.");
|
|
2286
2938
|
}
|
|
2287
2939
|
}
|
|
2288
|
-
function
|
|
2289
|
-
return
|
|
2940
|
+
function safeRef4(value) {
|
|
2941
|
+
return typeof value === "string" && SAFE_REF3.test(value);
|
|
2290
2942
|
}
|
|
2291
|
-
function
|
|
2292
|
-
return
|
|
2943
|
+
function timestamp3(value) {
|
|
2944
|
+
return typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
2293
2945
|
}
|
|
2294
|
-
function
|
|
2295
|
-
return
|
|
2946
|
+
function record5(value) {
|
|
2947
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2948
|
+
}
|
|
2949
|
+
function code(error) {
|
|
2950
|
+
return record5(error) && typeof error.code === "string" ? error.code : null;
|
|
2296
2951
|
}
|
|
2297
|
-
function
|
|
2298
|
-
if (!
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2952
|
+
function validateEnvelope(value) {
|
|
2953
|
+
if (!record5(value)) throw new ClaudeHostError("storage", "Claude outbox entry is malformed.");
|
|
2954
|
+
if (!sameKeys(value, ["schema_version", "identity", "host_input_ref", "input_kind", "prompt_ref", "observations", "origin_input", "submission_ref", "created_at", "expires_at"], ["request_ref"])) {
|
|
2955
|
+
throw new ClaudeHostError("storage", `Claude outbox entry key set is malformed (${refusalFact("keys", Object.keys(value), "exactly the outbox entry key set")}).`);
|
|
2956
|
+
}
|
|
2957
|
+
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}`)}).`);
|
|
2958
|
+
validateEnvelopeIdentity(value);
|
|
2959
|
+
validateEnvelopeObservations(value.observations);
|
|
2960
|
+
if (typeof value.origin_input !== "boolean") throw new ClaudeHostError("storage", "Claude outbox origin disposition is malformed.");
|
|
2961
|
+
if (!timestamp3(value.created_at) || !timestamp3(value.expires_at)) throw new ClaudeHostError("storage", "Claude outbox timestamps are malformed.");
|
|
2962
|
+
}
|
|
2963
|
+
function validateEnvelopeIdentity(value) {
|
|
2964
|
+
validateIdentity2(value.identity);
|
|
2965
|
+
if (!safeRef4(value.host_input_ref)) throw new ClaudeHostError("storage", "Claude outbox input reference is malformed.");
|
|
2966
|
+
if (!isCealHostInputKind(value.input_kind)) throw new ClaudeHostError("storage", "Claude outbox input provenance is malformed.");
|
|
2967
|
+
if (!safeRef4(value.prompt_ref) || !safeRef4(value.submission_ref)) throw new ClaudeHostError("storage", "Claude outbox references are malformed.");
|
|
2968
|
+
if (value.request_ref !== void 0 && !safeRef4(value.request_ref)) throw new ClaudeHostError("storage", "Claude outbox Request reference is malformed.");
|
|
2969
|
+
}
|
|
2970
|
+
function validateEnvelopeObservations(value) {
|
|
2971
|
+
if (!Array.isArray(value)) throw new ClaudeHostError("storage", "Claude outbox observations are malformed.");
|
|
2972
|
+
try {
|
|
2973
|
+
for (const observation2 of value) validateCealHostObservationDraft(observation2);
|
|
2974
|
+
} catch {
|
|
2975
|
+
throw new ClaudeHostError("storage", "Claude outbox observations are malformed.");
|
|
2302
2976
|
}
|
|
2303
2977
|
}
|
|
2304
|
-
function
|
|
2305
|
-
|
|
2978
|
+
function validateSession(value) {
|
|
2979
|
+
if (!record5(value)) throw new ClaudeHostError("storage", "Claude session snapshot is malformed.");
|
|
2980
|
+
if (!sameKeys(value, ["schema_version", "host_session_ref", "host_source_ref", "observed_at", "value", "expires_at"])) {
|
|
2981
|
+
throw new ClaudeHostError("storage", `Claude session snapshot key set is malformed (${refusalFact("keys", Object.keys(value), "exactly the session snapshot key set")}).`);
|
|
2982
|
+
}
|
|
2983
|
+
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}`)}).`);
|
|
2984
|
+
if (!safeRef4(value.host_session_ref) || !safeRef4(value.host_source_ref) || !timestamp3(value.observed_at) || !timestamp3(value.expires_at)) {
|
|
2985
|
+
throw new ClaudeHostError("storage", "Claude session snapshot fields are malformed.");
|
|
2986
|
+
}
|
|
2306
2987
|
}
|
|
2307
|
-
function
|
|
2308
|
-
|
|
2988
|
+
function validateRegistration(value) {
|
|
2989
|
+
if (!record5(value)) throw new ClaudeHostError("storage", "Claude transcript registration is malformed.");
|
|
2990
|
+
if (!sameKeys(value, ["schema_version", "host_source_ref", "host_session_ref", "claude_session_id", "transcript_path", "registered_at", "expires_at"], ["cwd"])) {
|
|
2991
|
+
throw new ClaudeHostError("storage", `Claude transcript registration key set is malformed (${refusalFact("keys", Object.keys(value), "exactly the registration key set")}).`);
|
|
2992
|
+
}
|
|
2993
|
+
if (value.schema_version !== CLAUDE_TRANSCRIPT_REGISTRATION_SCHEMA_VERSION) throw new ClaudeHostError("storage", "Claude transcript registration is malformed.");
|
|
2994
|
+
if (![value.host_source_ref, value.host_session_ref, value.claude_session_id].every((candidate) => typeof candidate === "string" && SAFE_REF3.test(candidate))) {
|
|
2995
|
+
throw new ClaudeHostError("storage", "Claude transcript registration references are malformed.");
|
|
2996
|
+
}
|
|
2997
|
+
validateRegistrationPaths(value.cwd, value.transcript_path);
|
|
2998
|
+
if (!timestamp3(value.registered_at) || !timestamp3(value.expires_at)) throw new ClaudeHostError("storage", "Claude transcript registration timestamps are malformed.");
|
|
2309
2999
|
}
|
|
2310
|
-
function
|
|
2311
|
-
|
|
3000
|
+
function validateRegistrationPaths(cwd, transcriptPath) {
|
|
3001
|
+
if (cwd !== void 0 && (typeof cwd !== "string" || !isAbsolute(cwd) || cwd.length > CLAUDE_TRANSCRIPT_PATH_MAX_CHARACTERS || cwd.includes("\0"))) {
|
|
3002
|
+
throw new ClaudeHostError("storage", "Claude transcript registration working directory is malformed.");
|
|
3003
|
+
}
|
|
3004
|
+
if (typeof transcriptPath !== "string" || transcriptPath.length === 0 || transcriptPath.length > CLAUDE_TRANSCRIPT_PATH_MAX_CHARACTERS) {
|
|
3005
|
+
throw new ClaudeHostError("storage", "Claude transcript registration path is malformed.");
|
|
3006
|
+
}
|
|
2312
3007
|
}
|
|
2313
|
-
function
|
|
2314
|
-
|
|
3008
|
+
function validateCursor(value) {
|
|
3009
|
+
if (!record5(value)) throw new ClaudeHostError("storage", "Claude transcript cursor is malformed.");
|
|
3010
|
+
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"])) {
|
|
3011
|
+
throw new ClaudeHostError("storage", "Claude transcript cursor key set is malformed.");
|
|
3012
|
+
}
|
|
3013
|
+
if (value.schema_version !== CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION) throw new ClaudeHostError("storage", "Claude transcript cursor is malformed.");
|
|
3014
|
+
if (![value.host_source_ref, value.host_session_ref].every((candidate) => typeof candidate === "string" && SAFE_REF3.test(candidate))) throw new ClaudeHostError("storage", "Claude transcript cursor references are malformed.");
|
|
3015
|
+
if (typeof value.transcript_path !== "string" || value.transcript_path.length === 0) throw new ClaudeHostError("storage", "Claude transcript cursor path is malformed.");
|
|
3016
|
+
validateCursorPosition(value);
|
|
3017
|
+
validateCursorTurn(value.open_turn, value.unattributed, value.session_observed);
|
|
3018
|
+
validateSubagentCursors(value.subagents);
|
|
3019
|
+
if (value.reason !== void 0 && !isHostTranscriptCursorReason(value.reason)) throw new ClaudeHostError("storage", "Claude transcript cursor reason is malformed.");
|
|
3020
|
+
}
|
|
3021
|
+
function validateCursorPosition(value) {
|
|
3022
|
+
if (!validCursorPosition(value)) throw new ClaudeHostError("storage", "Claude transcript cursor position is malformed.");
|
|
3023
|
+
if (value.device !== void 0 && typeof value.device !== "string") throw new ClaudeHostError("storage", "Claude transcript cursor device is malformed.");
|
|
3024
|
+
if (value.inode !== void 0 && typeof value.inode !== "string") throw new ClaudeHostError("storage", "Claude transcript cursor inode is malformed.");
|
|
3025
|
+
if (value.device === void 0 && value.offset !== 0) throw new ClaudeHostError("storage", "Claude transcript cursor is unpinned past its start.");
|
|
3026
|
+
if (value.status !== "active" && value.status !== "quarantined" || !timestamp3(value.updated_at)) throw new ClaudeHostError("storage", "Claude transcript cursor status is malformed.");
|
|
3027
|
+
}
|
|
3028
|
+
function validateCursorTurn(openTurn, unattributed, sessionObserved) {
|
|
3029
|
+
if (openTurn !== void 0 && !validOpenTurn(openTurn)) throw new ClaudeHostError("storage", "Claude transcript turn cursor is malformed.");
|
|
3030
|
+
if (unattributed !== void 0 && !validUnattributed(unattributed)) throw new ClaudeHostError("storage", "Claude transcript unattributed cursor is malformed.");
|
|
3031
|
+
if (sessionObserved !== void 0 && typeof sessionObserved !== "boolean") throw new ClaudeHostError("storage", "Claude transcript cursor session marker is malformed.");
|
|
3032
|
+
}
|
|
3033
|
+
function validateSubagentCursors(value) {
|
|
3034
|
+
if (!record5(value)) throw new ClaudeHostError("storage", "Claude transcript subagent cursors are malformed.");
|
|
3035
|
+
for (const [name, entry] of Object.entries(value)) validateSubagentEntry(name, entry);
|
|
3036
|
+
}
|
|
3037
|
+
function validateSubagentEntry(name, entry) {
|
|
3038
|
+
if (name.length === 0 || name.length > CLAUDE_TRANSCRIPT_SUBAGENT_NAME_MAX_CHARACTERS || name.includes("/")) throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3039
|
+
if (name.includes("\0")) throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3040
|
+
if (!record5(entry) || !sameKeys(entry, ["offset", "anchor_start", "anchor_sha256"], ["device", "inode"]) || !nonNegativeInteger(entry.offset)) throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3041
|
+
validateSubagentIdentity(entry);
|
|
3042
|
+
}
|
|
3043
|
+
function validateSubagentIdentity(entry) {
|
|
3044
|
+
if (entry.device !== void 0 && typeof entry.device !== "string" || entry.inode !== void 0 && typeof entry.inode !== "string") {
|
|
3045
|
+
throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3046
|
+
}
|
|
3047
|
+
if (!nonNegativeInteger(entry.anchor_start) || typeof entry.anchor_sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(entry.anchor_sha256)) {
|
|
3048
|
+
throw new ClaudeHostError("storage", "Claude transcript subagent cursor is malformed.");
|
|
3049
|
+
}
|
|
3050
|
+
if (entry.device === void 0 && Number(entry.offset) !== 0) throw new ClaudeHostError("storage", "Claude transcript subagent cursor is unpinned past its start.");
|
|
3051
|
+
}
|
|
3052
|
+
function validCursorPosition(value) {
|
|
3053
|
+
return nonNegativeInteger(value.offset) && nonNegativeInteger(value.anchor_start) && typeof value.anchor_sha256 === "string" && /^[a-f0-9]{64}$/u.test(value.anchor_sha256);
|
|
3054
|
+
}
|
|
3055
|
+
function validOpenTurn(value) {
|
|
3056
|
+
if (!record5(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;
|
|
3057
|
+
if (![value.turn_key, value.user_uuid, value.prompt_ref, value.host_input_ref].every((candidate) => typeof candidate === "string" && SAFE_REF3.test(candidate))) return false;
|
|
3058
|
+
if (!timestamp3(value.started_at) || !timestamp3(value.last_observed_at)) return false;
|
|
3059
|
+
if (!validMessageIds(value.message_ids)) return false;
|
|
3060
|
+
if (!validTurnCounts(value.counts)) return false;
|
|
3061
|
+
return value.last_terminal_digest === void 0 || typeof value.last_terminal_digest === "string" && /^[a-f0-9]{64}$/u.test(value.last_terminal_digest);
|
|
3062
|
+
}
|
|
3063
|
+
function validMessageIds(value) {
|
|
3064
|
+
if (!Array.isArray(value) || value.length > CLAUDE_TRANSCRIPT_MAX_TURN_MESSAGES) return false;
|
|
3065
|
+
if (!value.every((candidate) => typeof candidate === "string" && candidate.length > 0 && candidate.length <= 256)) return false;
|
|
3066
|
+
return new Set(value).size === value.length;
|
|
3067
|
+
}
|
|
3068
|
+
function validUnattributed(value) {
|
|
3069
|
+
if (!record5(value) || !sameKeys(value, ["input", "cached_input", "cache_write_input", "output", "message_ids", "last_observed_at"])) return false;
|
|
3070
|
+
if (!validMessageIds(value.message_ids) || value.message_ids.length === 0) return false;
|
|
3071
|
+
if (!timestamp3(value.last_observed_at)) return false;
|
|
3072
|
+
return validTurnCounts(value);
|
|
3073
|
+
}
|
|
3074
|
+
function validTurnCounts(value) {
|
|
3075
|
+
if (!record5(value) || !sameKeys(value, ["input", "cached_input", "cache_write_input", "output"])) return false;
|
|
3076
|
+
return [value.input, value.cached_input, value.cache_write_input, value.output].every(nonNegativeInteger);
|
|
3077
|
+
}
|
|
3078
|
+
function validateIdentity2(value) {
|
|
3079
|
+
if (!record5(value) || ![value.host_session_ref, value.host_turn_ref, value.host_source_ref].every((candidate) => typeof candidate === "string" && SAFE_REF3.test(candidate))) {
|
|
3080
|
+
throw new ClaudeHostError("storage", "Claude Host identity is malformed.");
|
|
3081
|
+
}
|
|
2315
3082
|
}
|
|
2316
|
-
function
|
|
2317
|
-
return value
|
|
3083
|
+
function nonNegativeInteger(value) {
|
|
3084
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
2318
3085
|
}
|
|
2319
|
-
function
|
|
2320
|
-
return
|
|
3086
|
+
function sameKeys(value, required, optional = []) {
|
|
3087
|
+
return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
|
|
2321
3088
|
}
|
|
2322
|
-
|
|
2323
|
-
|
|
3089
|
+
|
|
3090
|
+
// host/claude/src/claude-transcript-watcher.ts
|
|
3091
|
+
import { O_NOFOLLOW as O_NOFOLLOW2, O_RDONLY as O_RDONLY3 } from "node:constants";
|
|
3092
|
+
import { open as open3, readdir as readdir2 } from "node:fs/promises";
|
|
3093
|
+
import { join as join4 } from "node:path";
|
|
3094
|
+
var TOKEN_OBSERVATION_SOURCE = "claude_transcript.message.usage";
|
|
3095
|
+
var ClaudeTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
3096
|
+
store;
|
|
3097
|
+
registerCurrentPrompt;
|
|
3098
|
+
diagnostic;
|
|
3099
|
+
constructor(options) {
|
|
3100
|
+
super(options, {
|
|
3101
|
+
default_retention_ms: CLAUDE_DEFAULT_RETENTION_MS,
|
|
3102
|
+
invalid_outbox: () => new ClaudeHostError("configuration", "Claude transcript watcher outbox is invalid."),
|
|
3103
|
+
clock_failure: () => new ClaudeHostError("storage", "Claude watcher clock is invalid.")
|
|
3104
|
+
});
|
|
3105
|
+
this.store = new ClaudeOutboxStore(options.outbox_dir);
|
|
3106
|
+
this.registerCurrentPrompt = options.register_current_prompt;
|
|
3107
|
+
this.diagnostic = options.diagnostic ?? (() => void 0);
|
|
3108
|
+
}
|
|
3109
|
+
transcriptPathOf(registration) {
|
|
3110
|
+
return registration.transcript_path;
|
|
3111
|
+
}
|
|
3112
|
+
async listRegistrations() {
|
|
3113
|
+
return this.store.listTranscriptRegistrations();
|
|
3114
|
+
}
|
|
3115
|
+
async scanOpened(handle, registration) {
|
|
3116
|
+
const { size, device, inode } = await ownedClaudeTranscriptFile(handle);
|
|
3117
|
+
const cursor = await this.preflight(registration, handle, size, device, inode);
|
|
3118
|
+
if (cursor === null) return { records: 0, enqueued: 0, quarantined: true };
|
|
3119
|
+
if (cursor.status === "quarantined") return { records: 0, enqueued: 0, quarantined: true };
|
|
3120
|
+
const draft = mutableCursor(cursor);
|
|
3121
|
+
await this.scanOpenedTranscript(registration, handle, draft, size, device, inode);
|
|
3122
|
+
return { records: draft.consumed, enqueued: draft.enqueued, quarantined: false };
|
|
3123
|
+
}
|
|
3124
|
+
async scanFailed(handle, registration, error) {
|
|
3125
|
+
this.diagnostic("watcher_scan_failed", claudeTranscriptScanFailureKind(error));
|
|
3126
|
+
if (handle === void 0 && isSymlinkTranscript(error)) return await this.quarantineUnopened(registration, "transcript_malformed");
|
|
3127
|
+
if (error instanceof ClaudeHostError && error.code === "payload_rejected") return await this.quarantineDraft(await this.preservedCursor(registration));
|
|
3128
|
+
await this.quarantineFailure(registration, handle);
|
|
3129
|
+
return { records: 0, enqueued: 0, quarantined: true };
|
|
3130
|
+
}
|
|
3131
|
+
async retireMissing(registration) {
|
|
3132
|
+
return this.retireMissingTranscript(registration.transcript_path, "Claude", () => this.store.retireTranscriptRegistration(registration));
|
|
3133
|
+
}
|
|
3134
|
+
initialCursor(registration) {
|
|
3135
|
+
return {
|
|
3136
|
+
schema_version: CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION,
|
|
3137
|
+
host_source_ref: registration.host_source_ref,
|
|
3138
|
+
host_session_ref: registration.host_session_ref,
|
|
3139
|
+
transcript_path: registration.transcript_path,
|
|
3140
|
+
offset: 0,
|
|
3141
|
+
anchor_start: 0,
|
|
3142
|
+
anchor_sha256: this.sha(Buffer.alloc(0)),
|
|
3143
|
+
subagents: {},
|
|
3144
|
+
status: "active",
|
|
3145
|
+
updated_at: this.stamp()
|
|
3146
|
+
};
|
|
3147
|
+
}
|
|
3148
|
+
async scanOpenedTranscript(registration, handle, draft, size, device, inode) {
|
|
3149
|
+
const main2 = await this.readChunk(handle, draft.offset, size);
|
|
3150
|
+
if (main2 !== "empty" && main2 !== "partial") {
|
|
3151
|
+
await this.consumeMainChunk(registration, main2, draft);
|
|
3152
|
+
draft.offset += main2.length;
|
|
3153
|
+
}
|
|
3154
|
+
draft.consumed += await this.consumeSubagents(registration, draft);
|
|
3155
|
+
await this.emitOpenTerminal(registration, draft);
|
|
3156
|
+
await this.commitCursor(registration, handle, draft, device, inode);
|
|
3157
|
+
}
|
|
3158
|
+
async preflight(registration, handle, size, device, inode) {
|
|
3159
|
+
const existing = await this.store.readTranscriptCursor(registration);
|
|
3160
|
+
if (existing?.status === "quarantined") return existing;
|
|
3161
|
+
const cursor = existing ?? this.initialCursor(registration);
|
|
3162
|
+
if (cursor.device === void 0) {
|
|
3163
|
+
if (cursor.offset !== 0) return await this.quarantineStored(cursor, device, inode, "transcript_replaced");
|
|
3164
|
+
return { ...cursor, device, inode };
|
|
3165
|
+
}
|
|
3166
|
+
const reason = this.cursorIdentityReason(cursor, registration.transcript_path, device, inode) ?? await this.cursorBoundsReason(handle, cursor, size);
|
|
3167
|
+
if (reason !== null) return await this.quarantineStored(cursor, device, inode, reason);
|
|
3168
|
+
return cursor;
|
|
3169
|
+
}
|
|
3170
|
+
async consumeMainChunk(registration, complete, draft) {
|
|
3171
|
+
for (const line of complete.toString("utf8").split("\n").filter((value) => value.length > 0)) {
|
|
3172
|
+
draft.consumed += 1;
|
|
3173
|
+
const record6 = parseTranscriptLine(line);
|
|
3174
|
+
if (record6 === null) throw new ClaudeHostError("payload_rejected", "Claude transcript line is not valid JSON.", "malformed_line");
|
|
3175
|
+
if (isUserTurnLine(record6)) await this.openTurn(registration, draft, record6);
|
|
3176
|
+
else if (isAssistantUsageLine(record6)) this.accumulateUsage(draft, toUsage(record6.uuid, record6.timestamp, record6.message.usage));
|
|
3177
|
+
else if (isAssistantLine(record6) && record6.message.usage !== void 0) throw new ClaudeHostError("payload_rejected", "Claude transcript usage is malformed.");
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
async openTurn(registration, draft, record6) {
|
|
3181
|
+
this.diagnostic("watcher_turn_observed");
|
|
3182
|
+
await this.closeTurn(registration, draft, record6.timestamp);
|
|
3183
|
+
const sessionRef = registration.host_session_ref;
|
|
3184
|
+
const turnRef = claudeLocalTurnRef(registration.claude_session_id, record6.uuid);
|
|
3185
|
+
const inputRef = claudeLocalInputRef(registration.claude_session_id, record6.uuid);
|
|
3186
|
+
const promptRef = hostProducerPromptRef(this.sha(record6.message.content));
|
|
3187
|
+
const identity = { host_source_ref: registration.host_source_ref, host_session_ref: sessionRef, host_turn_ref: turnRef };
|
|
3188
|
+
const observations = [
|
|
3189
|
+
{ observation_ref: observationRef("transcript_user_text", inputRef, record6.message.content), observation_kind: "prompt", observed_at: record6.timestamp, value: { body_sha256: this.sha(record6.message.content) } },
|
|
3190
|
+
{ observation_ref: observationRef("transcript_turn_started", turnRef, record6.uuid), observation_kind: "turn", observed_at: record6.timestamp, value: { status: "started", host_session_id_sha256: this.sha(registration.claude_session_id), host_turn_ref: turnRef } }
|
|
3191
|
+
];
|
|
3192
|
+
if (!draft.session_observed) {
|
|
3193
|
+
const session = await this.store.readSession(registration.host_source_ref, sessionRef);
|
|
3194
|
+
if (session !== null) observations.push({ observation_ref: observationRef("transcript_session", sessionRef, session.value), observation_kind: "session", observed_at: record6.timestamp, value: session.value });
|
|
3195
|
+
draft.session_observed = true;
|
|
3196
|
+
}
|
|
3197
|
+
await this.store.putEnvelope({
|
|
3198
|
+
schema_version: "ceal.claude_observation_outbox.v1",
|
|
3199
|
+
identity,
|
|
3200
|
+
host_input_ref: inputRef,
|
|
3201
|
+
input_kind: "human",
|
|
3202
|
+
prompt_ref: promptRef,
|
|
3203
|
+
observations,
|
|
3204
|
+
origin_input: true,
|
|
3205
|
+
submission_ref: submissionRef("input", turnRef, inputRef, promptRef),
|
|
3206
|
+
created_at: record6.timestamp,
|
|
3207
|
+
expires_at: this.expire(this.outboxRetentionMs)
|
|
3208
|
+
});
|
|
3209
|
+
draft.enqueued += 1;
|
|
3210
|
+
await this.registerPrompt(sessionRef, turnRef, promptRef, record6.message.content);
|
|
3211
|
+
const selection = await this.store.readInvocationTurnSelection(identity.host_source_ref, identity.host_session_ref);
|
|
3212
|
+
const turnGenerationRef = selection?.prompt_ref === promptRef ? selection.turn_generation_ref : void 0;
|
|
3213
|
+
await this.activateContext(identity, promptRef, turnGenerationRef);
|
|
3214
|
+
const folded = foldUnattributed(draft.unattributed);
|
|
3215
|
+
draft.open_turn = {
|
|
3216
|
+
turn_key: turnRef,
|
|
3217
|
+
user_uuid: record6.uuid,
|
|
3218
|
+
prompt_ref: promptRef,
|
|
3219
|
+
host_input_ref: inputRef,
|
|
3220
|
+
started_at: record6.timestamp,
|
|
3221
|
+
last_observed_at: folded?.last_observed_at ?? record6.timestamp,
|
|
3222
|
+
message_ids: folded?.message_ids ?? [],
|
|
3223
|
+
counts: folded?.counts ?? zeroCounts()
|
|
3224
|
+
};
|
|
3225
|
+
draft.unattributed = void 0;
|
|
3226
|
+
}
|
|
3227
|
+
async closeTurn(registration, draft, at) {
|
|
3228
|
+
const turn = draft.open_turn;
|
|
3229
|
+
draft.open_turn = void 0;
|
|
3230
|
+
if (turn === void 0) return;
|
|
3231
|
+
const digest2 = terminalDigest(turn);
|
|
3232
|
+
if (digest2 === turn.last_terminal_digest) return;
|
|
3233
|
+
await this.putTerminalEnvelope(registration, turn, digest2, turn.message_ids.length === 0 ? at : void 0);
|
|
3234
|
+
draft.enqueued += 1;
|
|
3235
|
+
}
|
|
3236
|
+
accumulateUsage(draft, usage) {
|
|
3237
|
+
const turn = draft.open_turn;
|
|
3238
|
+
if (turn === void 0) {
|
|
3239
|
+
draft.unattributed = accumulateUnattributed(draft.unattributed, usage);
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
if (turn.message_ids.includes(usage.uuid)) return;
|
|
3243
|
+
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");
|
|
3244
|
+
draft.open_turn = {
|
|
3245
|
+
...turn,
|
|
3246
|
+
last_observed_at: usage.observed_at > turn.last_observed_at ? usage.observed_at : turn.last_observed_at,
|
|
3247
|
+
message_ids: [...turn.message_ids, usage.uuid],
|
|
3248
|
+
counts: addCounts(turn.counts, usage)
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
/**
|
|
3252
|
+
* Subagent transcripts live beside the session transcript at
|
|
3253
|
+
* `<session-id>/subagents/*.jsonl` with a `.meta.json` sibling the scan
|
|
3254
|
+
* ignores. Their usage belongs to the turn open when the scan runs: a
|
|
3255
|
+
* subagent only runs while its parent turn is in flight. A straggler line
|
|
3256
|
+
* that lands after the next user message therefore attributes to the open
|
|
3257
|
+
* turn, never to a closed one.
|
|
3258
|
+
*/
|
|
3259
|
+
async consumeSubagents(registration, draft) {
|
|
3260
|
+
const directory = subagentsDirectory(registration.transcript_path);
|
|
3261
|
+
let names;
|
|
3262
|
+
try {
|
|
3263
|
+
names = (await readdir2(directory)).filter((name) => name.endsWith(".jsonl") && !name.endsWith(".meta.json")).sort();
|
|
3264
|
+
} catch (error) {
|
|
3265
|
+
if (isMissingTranscriptError(error) || isNotDirectory(error)) return 0;
|
|
3266
|
+
throw new ClaudeHostError("payload_rejected", "Claude subagent transcript directory is unavailable.");
|
|
3267
|
+
}
|
|
3268
|
+
if (names.length > CLAUDE_TRANSCRIPT_MAX_SUBAGENT_FILES) {
|
|
3269
|
+
this.log(`Claude ignored subagent transcripts past its file bound: transcript_path=${JSON.stringify(registration.transcript_path)}, files=${names.length}.`);
|
|
3270
|
+
names = names.slice(0, CLAUDE_TRANSCRIPT_MAX_SUBAGENT_FILES);
|
|
3271
|
+
}
|
|
3272
|
+
let consumed = 0;
|
|
3273
|
+
for (const name of names) {
|
|
3274
|
+
consumed += await this.consumeSubagentFile(draft, directory, name);
|
|
3275
|
+
}
|
|
3276
|
+
return consumed;
|
|
3277
|
+
}
|
|
3278
|
+
async consumeSubagentFile(draft, directory, name) {
|
|
3279
|
+
const path = join4(directory, name);
|
|
3280
|
+
let handle;
|
|
3281
|
+
try {
|
|
3282
|
+
handle = await open3(path, O_RDONLY3 | O_NOFOLLOW2);
|
|
3283
|
+
const { size, device, inode } = await ownedClaudeTranscriptFile(handle);
|
|
3284
|
+
const entry = draft.subagents[name] ?? { offset: 0, anchor_start: 0, anchor_sha256: this.sha(Buffer.alloc(0)) };
|
|
3285
|
+
const pinned = await this.preflightSubagent(entry, size, device, inode);
|
|
3286
|
+
const chunk = await this.readChunk(handle, pinned.offset, size);
|
|
3287
|
+
if (chunk === "empty" || chunk === "partial") {
|
|
3288
|
+
draft.subagents[name] = pinned;
|
|
3289
|
+
return 0;
|
|
3290
|
+
}
|
|
3291
|
+
const consumed = this.consumeSubagentLines(draft, chunk);
|
|
3292
|
+
const offset = pinned.offset + chunk.length;
|
|
3293
|
+
const anchor = await this.readAnchor(handle, offset, CLAUDE_TRANSCRIPT_ANCHOR_BYTES);
|
|
3294
|
+
draft.subagents[name] = { device, inode, offset, anchor_start: anchor.start, anchor_sha256: anchor.sha256 };
|
|
3295
|
+
return consumed;
|
|
3296
|
+
} catch (error) {
|
|
3297
|
+
if (isMissingTranscriptError(error)) {
|
|
3298
|
+
delete draft.subagents[name];
|
|
3299
|
+
return 0;
|
|
3300
|
+
}
|
|
3301
|
+
throw error instanceof ClaudeHostError ? error : new ClaudeHostError("payload_rejected", "Claude subagent transcript is unavailable.");
|
|
3302
|
+
} finally {
|
|
3303
|
+
await handle?.close();
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
consumeSubagentLines(draft, chunk) {
|
|
3307
|
+
let consumed = 0;
|
|
3308
|
+
for (const line of chunk.toString("utf8").split("\n").filter((value) => value.length > 0)) {
|
|
3309
|
+
consumed += 1;
|
|
3310
|
+
const record6 = parseTranscriptLine(line);
|
|
3311
|
+
if (record6 === null) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript line is not valid JSON.", "malformed_line");
|
|
3312
|
+
if (isAssistantUsageLine(record6)) this.accumulateUsage(draft, toUsage(record6.uuid, record6.timestamp, record6.message.usage));
|
|
3313
|
+
else if (isAssistantLine(record6) && record6.message.usage !== void 0) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript usage is malformed.");
|
|
3314
|
+
}
|
|
3315
|
+
return consumed;
|
|
3316
|
+
}
|
|
3317
|
+
async preflightSubagent(entry, size, device, inode) {
|
|
3318
|
+
if (entry.device === void 0) {
|
|
3319
|
+
if (entry.offset !== 0) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript cursor is unpinned past its start.");
|
|
3320
|
+
return { device, inode, offset: 0, anchor_start: 0, anchor_sha256: this.sha(Buffer.alloc(0)) };
|
|
3321
|
+
}
|
|
3322
|
+
if (entry.device !== device || entry.inode !== inode) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript was replaced.");
|
|
3323
|
+
if (size < entry.offset) throw new ClaudeHostError("payload_rejected", "Claude subagent transcript was truncated.");
|
|
3324
|
+
return { device, inode, offset: entry.offset, anchor_start: entry.anchor_start, anchor_sha256: entry.anchor_sha256 };
|
|
3325
|
+
}
|
|
3326
|
+
/**
|
|
3327
|
+
* The open turn's terminal is emitted optimistically on every scan whose
|
|
3328
|
+
* new lines changed its totals, so usage is visible before the next user
|
|
3329
|
+
* message closes the turn. The emission digest changes with the content,
|
|
3330
|
+
* so a repeated scan over unchanged state emits nothing.
|
|
3331
|
+
*/
|
|
3332
|
+
async emitOpenTerminal(registration, draft) {
|
|
3333
|
+
const turn = draft.open_turn;
|
|
3334
|
+
if (turn === void 0 || turn.message_ids.length === 0) return;
|
|
3335
|
+
const digest2 = terminalDigest(turn);
|
|
3336
|
+
if (digest2 === turn.last_terminal_digest) return;
|
|
3337
|
+
await this.putTerminalEnvelope(registration, turn, digest2, void 0);
|
|
3338
|
+
draft.enqueued += 1;
|
|
3339
|
+
draft.open_turn = { ...turn, last_terminal_digest: digest2 };
|
|
3340
|
+
}
|
|
3341
|
+
async putTerminalEnvelope(registration, turn, digest2, gapAt) {
|
|
3342
|
+
const observedAt = gapAt ?? turn.last_observed_at;
|
|
3343
|
+
const tokenValue = turn.message_ids.length === 0 ? hostObservationNotRecorded("claude_turn_usage_not_observed") : availableTokenValue(turn.counts);
|
|
3344
|
+
const identity = { host_source_ref: registration.host_source_ref, host_session_ref: registration.host_session_ref, host_turn_ref: turn.turn_key };
|
|
3345
|
+
await this.store.putEnvelope({
|
|
3346
|
+
schema_version: "ceal.claude_observation_outbox.v1",
|
|
3347
|
+
identity,
|
|
3348
|
+
host_input_ref: turn.host_input_ref,
|
|
3349
|
+
input_kind: "host_automatic",
|
|
3350
|
+
prompt_ref: turn.prompt_ref,
|
|
3351
|
+
observations: [
|
|
3352
|
+
{ 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 } },
|
|
3353
|
+
{ observation_ref: observationRef("transcript_token", turn.turn_key, digest2), observation_kind: "token", observed_at: observedAt, value: tokenValue },
|
|
3354
|
+
{ observation_ref: observationRef("transcript_cost", turn.turn_key, digest2), observation_kind: "cost", observed_at: observedAt, value: hostObservationNotRecorded("provider_cost_not_observed") }
|
|
3355
|
+
],
|
|
3356
|
+
origin_input: false,
|
|
3357
|
+
submission_ref: submissionRef("terminal", turn.turn_key, digest2, turn.prompt_ref),
|
|
3358
|
+
created_at: observedAt,
|
|
3359
|
+
expires_at: this.expire(this.outboxRetentionMs)
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
async commitCursor(registration, handle, draft, device, inode) {
|
|
3363
|
+
const anchor = await this.readAnchor(handle, draft.offset, CLAUDE_TRANSCRIPT_ANCHOR_BYTES);
|
|
3364
|
+
await this.store.writeTranscriptCursor({
|
|
3365
|
+
schema_version: CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION,
|
|
3366
|
+
host_source_ref: registration.host_source_ref,
|
|
3367
|
+
host_session_ref: registration.host_session_ref,
|
|
3368
|
+
transcript_path: registration.transcript_path,
|
|
3369
|
+
device,
|
|
3370
|
+
inode,
|
|
3371
|
+
offset: draft.offset,
|
|
3372
|
+
anchor_start: anchor.start,
|
|
3373
|
+
anchor_sha256: anchor.sha256,
|
|
3374
|
+
subagents: draft.subagents,
|
|
3375
|
+
...draft.open_turn === void 0 ? {} : { open_turn: draft.open_turn },
|
|
3376
|
+
...draft.unattributed === void 0 ? {} : { unattributed: draft.unattributed },
|
|
3377
|
+
...draft.session_observed ? { session_observed: true } : {},
|
|
3378
|
+
status: "active",
|
|
3379
|
+
updated_at: this.stamp()
|
|
3380
|
+
});
|
|
3381
|
+
}
|
|
3382
|
+
async registerPrompt(sessionRef, turnRef, promptRef, prompt) {
|
|
3383
|
+
if (this.producerInstallationRef === void 0 || this.registerCurrentPrompt === void 0) return;
|
|
3384
|
+
try {
|
|
3385
|
+
await this.registerCurrentPrompt({
|
|
3386
|
+
producer_id: "claude",
|
|
3387
|
+
producer_installation_ref: this.producerInstallationRef,
|
|
3388
|
+
local_session_ref: sessionRef,
|
|
3389
|
+
local_turn_ref: turnRef,
|
|
3390
|
+
prompt_ref: promptRef,
|
|
3391
|
+
current_prompt: prompt,
|
|
3392
|
+
context_expires_at: this.expire(this.outboxRetentionMs)
|
|
3393
|
+
});
|
|
3394
|
+
this.diagnostic("current_prompt_registered");
|
|
3395
|
+
} catch (error) {
|
|
3396
|
+
this.diagnostic("current_prompt_registration_failed");
|
|
3397
|
+
throw error;
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
async activateContext(identity, promptRef, turnGenerationRef) {
|
|
3401
|
+
try {
|
|
3402
|
+
await this.store.activateInvocationContext(identity, promptRef, this.expire(this.outboxRetentionMs), void 0, turnGenerationRef);
|
|
3403
|
+
this.diagnostic("activation_recorded");
|
|
3404
|
+
} catch (error) {
|
|
3405
|
+
this.diagnostic("activation_failed");
|
|
3406
|
+
throw error;
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
async quarantineStored(cursor, device, inode, reason) {
|
|
3410
|
+
await this.store.writeTranscriptCursor({ ...cursor, device, inode, status: "quarantined", reason, updated_at: this.stamp() });
|
|
3411
|
+
return null;
|
|
3412
|
+
}
|
|
3413
|
+
async quarantineUnopened(registration, reason) {
|
|
3414
|
+
const existing = await this.store.readTranscriptCursor(registration).catch(() => null);
|
|
3415
|
+
await this.store.writeTranscriptCursor({ ...existing ?? this.initialCursor(registration), transcript_path: registration.transcript_path, status: "quarantined", reason, updated_at: this.stamp() });
|
|
3416
|
+
return { records: 0, enqueued: 0, quarantined: true };
|
|
3417
|
+
}
|
|
3418
|
+
async quarantineDraft(cursor) {
|
|
3419
|
+
await this.store.writeTranscriptCursor({ ...cursor, status: "quarantined", reason: "transcript_malformed", updated_at: this.stamp() });
|
|
3420
|
+
return { records: 0, enqueued: 0, quarantined: true };
|
|
3421
|
+
}
|
|
3422
|
+
async quarantineFailure(registration, handle) {
|
|
3423
|
+
if (handle === void 0) return;
|
|
3424
|
+
const stats = await handle.stat().catch(() => null);
|
|
3425
|
+
if (stats === null) return;
|
|
3426
|
+
const existing = await this.store.readTranscriptCursor(registration).catch(() => null);
|
|
3427
|
+
const cursor = existing ?? this.initialCursor(registration);
|
|
3428
|
+
await this.store.writeTranscriptCursor({ ...cursor, device: String(stats.dev), inode: String(stats.ino), status: "quarantined", reason: "transcript_malformed", updated_at: this.stamp() });
|
|
3429
|
+
}
|
|
3430
|
+
async preservedCursor(registration) {
|
|
3431
|
+
return await this.store.readTranscriptCursor(registration).catch(() => null) ?? this.initialCursor(registration);
|
|
3432
|
+
}
|
|
3433
|
+
};
|
|
3434
|
+
function mutableCursor(cursor) {
|
|
3435
|
+
return {
|
|
3436
|
+
offset: cursor.offset,
|
|
3437
|
+
...cursor.open_turn === void 0 ? {} : { open_turn: cursor.open_turn },
|
|
3438
|
+
...cursor.unattributed === void 0 ? {} : { unattributed: cursor.unattributed },
|
|
3439
|
+
subagents: Object.fromEntries(Object.entries(cursor.subagents).map(([name, entry]) => [name, { ...entry }])),
|
|
3440
|
+
session_observed: cursor.session_observed ?? false,
|
|
3441
|
+
consumed: 0,
|
|
3442
|
+
enqueued: 0
|
|
3443
|
+
};
|
|
2324
3444
|
}
|
|
2325
|
-
function
|
|
2326
|
-
return
|
|
3445
|
+
function zeroCounts() {
|
|
3446
|
+
return { input: 0, cached_input: 0, cache_write_input: 0, output: 0 };
|
|
3447
|
+
}
|
|
3448
|
+
function terminalDigest(turn) {
|
|
3449
|
+
return transcriptObjectSha256({ turn: turn.turn_key, messages: turn.message_ids, counts: turn.counts });
|
|
3450
|
+
}
|
|
3451
|
+
function availableTokenValue(counts) {
|
|
3452
|
+
const input = counts.input;
|
|
3453
|
+
const output = counts.output;
|
|
3454
|
+
if (!Number.isSafeInteger(input + output)) throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
3455
|
+
return createHostTokenObservationValue({
|
|
3456
|
+
input,
|
|
3457
|
+
output,
|
|
3458
|
+
total: input + output,
|
|
3459
|
+
cached_input: counts.cached_input,
|
|
3460
|
+
cache_write_input: counts.cache_write_input
|
|
3461
|
+
}, TOKEN_OBSERVATION_SOURCE);
|
|
3462
|
+
}
|
|
3463
|
+
function addCounts(current, next) {
|
|
3464
|
+
const counts = {
|
|
3465
|
+
input: current.input + next.input,
|
|
3466
|
+
cached_input: current.cached_input + next.cached_input,
|
|
3467
|
+
cache_write_input: current.cache_write_input + next.cache_write_input,
|
|
3468
|
+
output: current.output + next.output
|
|
3469
|
+
};
|
|
3470
|
+
if (![counts.input, counts.cached_input, counts.cache_write_input, counts.output].every((value) => Number.isSafeInteger(value) && value >= 0)) {
|
|
3471
|
+
throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
3472
|
+
}
|
|
3473
|
+
return counts;
|
|
3474
|
+
}
|
|
3475
|
+
function accumulateUnattributed(current, usage) {
|
|
3476
|
+
const base = current ?? { input: 0, cached_input: 0, cache_write_input: 0, output: 0, message_ids: [], last_observed_at: usage.observed_at };
|
|
3477
|
+
if (base.message_ids.includes(usage.uuid)) return base;
|
|
3478
|
+
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");
|
|
3479
|
+
const counts = addCounts(base, usage);
|
|
3480
|
+
return {
|
|
3481
|
+
...counts,
|
|
3482
|
+
message_ids: [...base.message_ids, usage.uuid],
|
|
3483
|
+
last_observed_at: usage.observed_at > base.last_observed_at ? usage.observed_at : base.last_observed_at
|
|
3484
|
+
};
|
|
3485
|
+
}
|
|
3486
|
+
function foldUnattributed(bucket) {
|
|
3487
|
+
if (bucket === void 0) return void 0;
|
|
3488
|
+
if (![bucket.input, bucket.cached_input, bucket.cache_write_input, bucket.output].every((value) => Number.isSafeInteger(value) && value >= 0)) {
|
|
3489
|
+
throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
3490
|
+
}
|
|
3491
|
+
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 };
|
|
3492
|
+
}
|
|
3493
|
+
function toUsage(uuid, observedAt, usage) {
|
|
3494
|
+
const input = usageNumber(usage.input_tokens);
|
|
3495
|
+
const cached = usageNumber(usage.cache_read_input_tokens ?? 0);
|
|
3496
|
+
const created = usageNumber(usage.cache_creation_input_tokens ?? 0);
|
|
3497
|
+
const output = usageNumber(usage.output_tokens);
|
|
3498
|
+
if (input === null || cached === null || created === null || output === null) throw new ClaudeHostError("payload_rejected", "Claude transcript usage is malformed.");
|
|
3499
|
+
const total = input + cached + created;
|
|
3500
|
+
if (![total, output].every((value) => Number.isSafeInteger(value) && value >= 0)) throw new ClaudeHostError("payload_rejected", "Claude transcript usage exceeds the supported range.");
|
|
3501
|
+
return { uuid, observed_at: observedAt, input: total, cached_input: cached, cache_write_input: created, output };
|
|
3502
|
+
}
|
|
3503
|
+
function usageNumber(value) {
|
|
3504
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
3505
|
+
}
|
|
3506
|
+
function parseTranscriptLine(line) {
|
|
3507
|
+
try {
|
|
3508
|
+
return JSON.parse(line);
|
|
3509
|
+
} catch {
|
|
3510
|
+
return null;
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
function lineIdentity(record6) {
|
|
3514
|
+
if (typeof record6.uuid !== "string" || record6.uuid.length === 0 || record6.uuid.length > 256) return null;
|
|
3515
|
+
if (typeof record6.timestamp !== "string" || !Number.isFinite(Date.parse(record6.timestamp))) return null;
|
|
3516
|
+
return { uuid: record6.uuid, timestamp: record6.timestamp };
|
|
3517
|
+
}
|
|
3518
|
+
function isUserTurnLine(record6) {
|
|
3519
|
+
if (!isTranscriptRecord(record6) || record6.type !== "user") return false;
|
|
3520
|
+
if (!isTranscriptRecord(record6.message) || typeof record6.message.content !== "string") return false;
|
|
3521
|
+
if (record6.isMeta === true && (!isTranscriptRecord(record6.origin) || typeof record6.origin.kind !== "string")) return false;
|
|
3522
|
+
const identity = lineIdentity(record6);
|
|
3523
|
+
if (identity === null) throw new ClaudeHostError("payload_rejected", "Claude transcript user line is malformed.");
|
|
3524
|
+
return true;
|
|
3525
|
+
}
|
|
3526
|
+
function claudeTranscriptScanFailureKind(error) {
|
|
3527
|
+
if (isSymlinkTranscript(error)) return "symlink";
|
|
3528
|
+
if (error instanceof ClaudeHostError) {
|
|
3529
|
+
if (error.diagnostic_kind !== void 0) return error.diagnostic_kind;
|
|
3530
|
+
if (error.code === "payload_rejected") return "payload_rejected";
|
|
3531
|
+
if (error.code === "storage") return "read_error";
|
|
3532
|
+
}
|
|
3533
|
+
const code2 = transcriptErrorCode(error);
|
|
3534
|
+
if (code2 === "EFBIG") return "size_budget";
|
|
3535
|
+
if (["EBADF", "EACCES", "EIO", "EMFILE", "ENFILE", "ENOENT", "ENOTDIR", "EPERM"].includes(code2 ?? "")) return "read_error";
|
|
3536
|
+
return "other";
|
|
3537
|
+
}
|
|
3538
|
+
function isAssistantLine(record6) {
|
|
3539
|
+
return isTranscriptRecord(record6) && record6.type === "assistant" && isTranscriptRecord(record6.message);
|
|
3540
|
+
}
|
|
3541
|
+
function isAssistantUsageLine(record6) {
|
|
3542
|
+
if (!isTranscriptRecord(record6)) return false;
|
|
3543
|
+
if (!isAssistantLine(record6) || record6.message.usage === void 0) return false;
|
|
3544
|
+
const identity = lineIdentity(record6);
|
|
3545
|
+
if (identity === null || !isTranscriptRecord(record6.message.usage)) throw new ClaudeHostError("payload_rejected", "Claude transcript usage line is malformed.");
|
|
3546
|
+
return true;
|
|
3547
|
+
}
|
|
3548
|
+
function subagentsDirectory(transcriptPath) {
|
|
3549
|
+
const stem = transcriptPath.endsWith(".jsonl") ? transcriptPath.slice(0, -".jsonl".length) : transcriptPath;
|
|
3550
|
+
return join4(stem, "subagents");
|
|
3551
|
+
}
|
|
3552
|
+
function observationRef(event, identity, value) {
|
|
3553
|
+
return cealHostObservationRef("claude", transcriptObjectSha256({ event, identity, value }));
|
|
3554
|
+
}
|
|
3555
|
+
function submissionRef(kind, turn, digest2, prompt) {
|
|
3556
|
+
return `claude-submission:${transcriptObjectSha256({ kind, turn, digest: digest2, prompt })}`;
|
|
3557
|
+
}
|
|
3558
|
+
async function ownedClaudeTranscriptFile(handle) {
|
|
3559
|
+
const stats = await handle.stat();
|
|
3560
|
+
const uid = typeof process.geteuid === "function" ? process.geteuid() : stats.uid;
|
|
3561
|
+
if (!stats.isFile()) throw new ClaudeHostError("payload_rejected", "Claude transcript source is not a regular file.");
|
|
3562
|
+
if (stats.uid !== uid) throw new ClaudeHostError("payload_rejected", "Claude transcript source is owned by another user.");
|
|
3563
|
+
return { size: stats.size, device: String(stats.dev), inode: String(stats.ino) };
|
|
3564
|
+
}
|
|
3565
|
+
function isNotDirectory(error) {
|
|
3566
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOTDIR";
|
|
3567
|
+
}
|
|
3568
|
+
function isSymlinkTranscript(error) {
|
|
3569
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ELOOP";
|
|
3570
|
+
}
|
|
3571
|
+
function transcriptErrorCode(error) {
|
|
3572
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
2327
3573
|
}
|
|
2328
3574
|
|
|
2329
3575
|
// host/claude/src/claude-adapter.ts
|
|
2330
|
-
import { createHash as
|
|
3576
|
+
import { createHash as createHash4, randomUUID } from "node:crypto";
|
|
3577
|
+
import { O_NOFOLLOW as O_NOFOLLOW3, O_RDONLY as O_RDONLY4 } from "node:constants";
|
|
3578
|
+
import { open as open4 } from "node:fs/promises";
|
|
3579
|
+
import { homedir } from "node:os";
|
|
3580
|
+
import { isAbsolute as isAbsolute2, join as join5, relative, resolve } from "node:path";
|
|
2331
3581
|
var ClaudeHookAdapter = class {
|
|
2332
3582
|
store;
|
|
2333
3583
|
now;
|
|
3584
|
+
optionsSourceRef;
|
|
3585
|
+
transcriptsRoot;
|
|
3586
|
+
randomUuid;
|
|
2334
3587
|
constructor(options) {
|
|
2335
3588
|
this.store = new ClaudeOutboxStore(options.outbox_dir);
|
|
2336
3589
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2337
3590
|
this.optionsSourceRef = options.host_source_ref ?? "claude-host:local";
|
|
2338
|
-
this.
|
|
2339
|
-
this.
|
|
3591
|
+
this.transcriptsRoot = options.claude_transcripts_root;
|
|
3592
|
+
this.randomUuid = options.random_uuid ?? randomUUID;
|
|
2340
3593
|
}
|
|
2341
3594
|
async handle(event, raw) {
|
|
2342
3595
|
let input;
|
|
2343
3596
|
try {
|
|
2344
3597
|
input = decodeClaudeHookInput(event, raw);
|
|
2345
3598
|
} catch (error) {
|
|
2346
|
-
return { event, disposition: "rejected", reason: error instanceof Error ?
|
|
3599
|
+
return { event, disposition: "rejected", reason: error instanceof Error ? error.message.slice(0, 128) : "invalid_claude_hook_input" };
|
|
2347
3600
|
}
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
if (input.hook_event_name === "UserPromptSubmit" && turnRef !== void 0 && this.producerInstallationRef !== void 0 && this.registerCurrentPrompt !== void 0) {
|
|
2353
|
-
const promptRef = hostProducerPromptRef(createHash3("sha256").update(input.prompt, "utf8").digest("hex"));
|
|
2354
|
-
await this.registerCurrentPrompt({ producer_id: "claude", producer_installation_ref: this.producerInstallationRef, local_session_ref: sessionRef, local_turn_ref: turnRef, prompt_ref: promptRef, current_prompt: input.prompt, context_expires_at: new Date(Date.parse(occurredAt) + 24 * 60 * 60 * 1e3).toISOString() });
|
|
3601
|
+
try {
|
|
3602
|
+
return input.event === "SessionStart" ? await this.registerSession(input) : await this.registerCurrentTurn(input);
|
|
3603
|
+
} catch (error) {
|
|
3604
|
+
return { event, disposition: "rejected", reason: error instanceof Error ? error.message.slice(0, 128) : "claude_hook_failed" };
|
|
2355
3605
|
}
|
|
2356
|
-
const batchRef = `claude-batch:${cealRequestSha256({ event, session_ref: sessionRef, turn_ref: turnRef ?? null, occurred_at: occurredAt, frames })}`;
|
|
2357
|
-
const batch = { schema_version: CLAUDE_OUTBOX_SCHEMA, batch_ref: batchRef, hook_event: event, local_session_ref: sessionRef, ...turnRef === void 0 ? {} : { local_turn_ref: turnRef }, occurred_at: occurredAt, frames };
|
|
2358
|
-
await this.store.put(batch);
|
|
2359
|
-
await this.activatePromptContext(input, frames, sessionRef, turnRef);
|
|
2360
|
-
return { event, disposition: "queued", batch_ref: batchRef, local_session_ref: sessionRef, ...turnRef === void 0 ? {} : { local_turn_ref: turnRef } };
|
|
2361
|
-
}
|
|
2362
|
-
async activatePromptContext(input, frames, sessionRef, turnRef) {
|
|
2363
|
-
if (input.hook_event_name !== "UserPromptSubmit" || turnRef === void 0) return;
|
|
2364
|
-
const identity = { host_source_ref: this.optionsSourceRef, host_session_ref: sessionRef, host_turn_ref: turnRef };
|
|
2365
|
-
validateClaudeIdentity(identity);
|
|
2366
|
-
const inputFrame = frames.find((frame) => frame.frame_kind === "user_input");
|
|
2367
|
-
const expiresAt = inputFrame?.payload.context_expires_at;
|
|
2368
|
-
if (typeof expiresAt !== "string") throw new ClaudeHostError("storage", "Claude prompt context expiry is unavailable.");
|
|
2369
|
-
const observations = inputFrame?.payload.observations;
|
|
2370
|
-
const prompt = Array.isArray(observations) ? observations.find((observation) => isRecord(observation) && observation.observation_kind === "prompt") : void 0;
|
|
2371
|
-
const promptValue = isRecord(prompt) ? prompt.value : void 0;
|
|
2372
|
-
if (!isCealHostPromptObservationValue(promptValue)) throw new ClaudeHostError("storage", "Claude prompt observation is unavailable.");
|
|
2373
|
-
await this.store.activateInvocationContext(identity, hostProducerPromptRef(promptValue.body_sha256), expiresAt, input.cwd);
|
|
2374
3606
|
}
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
3607
|
+
async registerSession(input) {
|
|
3608
|
+
const defaultRoot = join5(homedir(), ".claude", "projects");
|
|
3609
|
+
const transcriptPath = sourceBoundTranscriptPath(input.transcript_path, this.transcriptsRoot ?? defaultRoot);
|
|
3610
|
+
const hostSessionRef = claudeLocalSessionRef(input.session_id);
|
|
3611
|
+
const observedAt = timestamp4(this.now);
|
|
3612
|
+
const expiresAt = new Date(Date.parse(observedAt) + CLAUDE_DEFAULT_RETENTION_MS).toISOString();
|
|
3613
|
+
const registration = {
|
|
3614
|
+
schema_version: "ceal.claude_transcript_registration.v1",
|
|
3615
|
+
host_source_ref: this.optionsSourceRef,
|
|
3616
|
+
host_session_ref: hostSessionRef,
|
|
3617
|
+
claude_session_id: input.session_id,
|
|
3618
|
+
cwd: input.cwd,
|
|
3619
|
+
transcript_path: transcriptPath,
|
|
3620
|
+
registered_at: observedAt,
|
|
3621
|
+
expires_at: expiresAt
|
|
3622
|
+
};
|
|
3623
|
+
const existing = (await this.store.listTranscriptRegistrations()).find((candidate) => candidate.host_source_ref === registration.host_source_ref && candidate.host_session_ref === registration.host_session_ref);
|
|
3624
|
+
if (existing === void 0) {
|
|
3625
|
+
await this.store.writeTranscriptCursor((await captureClaudeTranscriptBaseline(registration, this.now)).cursor);
|
|
3626
|
+
}
|
|
3627
|
+
await this.store.registerTranscript(registration);
|
|
3628
|
+
const snapshot = {
|
|
3629
|
+
schema_version: "ceal.claude_session.v1",
|
|
3630
|
+
host_session_ref: hostSessionRef,
|
|
3631
|
+
host_source_ref: this.optionsSourceRef,
|
|
3632
|
+
observed_at: observedAt,
|
|
3633
|
+
value: sessionObservationValue(input),
|
|
3634
|
+
expires_at: expiresAt
|
|
3635
|
+
};
|
|
3636
|
+
await this.store.saveSession(snapshot);
|
|
3637
|
+
return { event: input.event, disposition: "ignored", reason: "transcript_registered", local_session_ref: hostSessionRef };
|
|
3638
|
+
}
|
|
3639
|
+
async registerCurrentTurn(input) {
|
|
3640
|
+
const transcriptPath = sourceBoundTranscriptPath(input.transcript_path, this.transcriptsRoot ?? join5(homedir(), ".claude", "projects"));
|
|
3641
|
+
const hostSessionRef = claudeLocalSessionRef(input.session_id);
|
|
3642
|
+
const observedAt = timestamp4(this.now);
|
|
3643
|
+
const expiresAt = new Date(Date.parse(observedAt) + CLAUDE_DEFAULT_RETENTION_MS).toISOString();
|
|
3644
|
+
const registration = {
|
|
3645
|
+
schema_version: "ceal.claude_transcript_registration.v1",
|
|
3646
|
+
host_source_ref: this.optionsSourceRef,
|
|
3647
|
+
host_session_ref: hostSessionRef,
|
|
3648
|
+
claude_session_id: input.session_id,
|
|
3649
|
+
cwd: input.cwd,
|
|
3650
|
+
transcript_path: transcriptPath,
|
|
3651
|
+
registered_at: observedAt,
|
|
3652
|
+
expires_at: expiresAt
|
|
3653
|
+
};
|
|
3654
|
+
const promptRef = hostProducerPromptRef(createHash4("sha256").update(input.prompt, "utf8").digest("hex"));
|
|
3655
|
+
const turnGenerationRef = this.randomUuid();
|
|
3656
|
+
const selection = { schema_version: "ceal.host_invocation_turn_selection.v1", host_source_ref: this.optionsSourceRef, host_session_ref: hostSessionRef, prompt_ref: promptRef, turn_generation_ref: turnGenerationRef };
|
|
3657
|
+
await this.store.writeInvocationTurnSelection(selection);
|
|
3658
|
+
const existing = (await this.store.listTranscriptRegistrations()).find((candidate) => candidate.host_source_ref === registration.host_source_ref && candidate.host_session_ref === registration.host_session_ref);
|
|
3659
|
+
const baseline = await captureClaudeTranscriptBaseline(registration, this.now, input.prompt);
|
|
3660
|
+
const transcriptTurnRef = baseline.current_turn_ref;
|
|
3661
|
+
if (existing === void 0) await this.store.writeTranscriptCursor(baseline.cursor);
|
|
3662
|
+
else await refreshCurrentTurnCursor(this.store, registration, baseline, promptRef, turnGenerationRef, transcriptTurnRef);
|
|
3663
|
+
await this.store.registerTranscript(registration);
|
|
3664
|
+
return { event: input.event, disposition: "ignored", reason: "transcript_registered", local_session_ref: hostSessionRef, ...transcriptTurnRef === void 0 ? {} : { local_turn_ref: transcriptTurnRef } };
|
|
3665
|
+
}
|
|
2378
3666
|
};
|
|
2379
|
-
function
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
const
|
|
2386
|
-
const
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
const turnRef = claudeLocalTurnRef(input.session_id, input.prompt_id);
|
|
2392
|
-
const promptDigest = createHash3("sha256").update(input.prompt, "utf8").digest("hex");
|
|
2393
|
-
const turn = obs("turn", turnRef, at, { status: "started", prompt_id_sha256: createHash3("sha256").update(input.prompt_id, "utf8").digest("hex"), effort: input.effort?.level ?? "unavailable" });
|
|
2394
|
-
const prompt = obs("prompt", claudeLocalInputRef(input.session_id, input.prompt_id), at, { body_sha256: promptDigest });
|
|
2395
|
-
return [{ frame_kind: "turn_started", payload: { local_session_ref: sessionRef, local_turn_ref: turnRef, observations: [turn] } }, { frame_kind: "user_input", payload: compact({ local_session_ref: sessionRef, local_turn_ref: turnRef, local_input_ref: claudeLocalInputRef(input.session_id, input.prompt_id), input_kind: "human", origin_input: true, local_turn_aliases: [safeAlias(input.prompt_id)], context_expires_at: new Date(Date.parse(at) + 24 * 60 * 60 * 1e3).toISOString(), observations: [prompt] }) }];
|
|
2396
|
-
}
|
|
2397
|
-
function terminalFrames(input, at) {
|
|
2398
|
-
const sessionRef = claudeLocalSessionRef(input.session_id);
|
|
2399
|
-
const turnRef = claudeLocalTurnRef(input.session_id, input.prompt_id);
|
|
2400
|
-
const aborted = input.hook_event_name === "StopFailure";
|
|
2401
|
-
const reason = aborted ? `api_${input.error}` : void 0;
|
|
2402
|
-
const terminal = obs("turn", `${turnRef}:${aborted ? "failed" : "stopped"}`, at, compact({ status: aborted ? "aborted" : "completed", reason }));
|
|
2403
|
-
const timing = obs("timing", `${turnRef}:timing`, at, { status: "unavailable", reason: "claude_hook_does_not_expose_timing_or_usage" });
|
|
2404
|
-
return [{ frame_kind: "turn_terminal", payload: compact({ local_session_ref: sessionRef, local_turn_ref: turnRef, terminal_state: aborted ? "aborted" : "completed", abort_reason: reason, observations: [terminal, timing] }) }];
|
|
2405
|
-
}
|
|
2406
|
-
function obs(kind, identity, at, value) {
|
|
2407
|
-
return { observation_ref: cealHostObservationRef("claude", cealRequestSha256({ kind, identity, at, value })), observation_kind: kind, observed_at: at, value };
|
|
2408
|
-
}
|
|
2409
|
-
function safeAlias(value) {
|
|
2410
|
-
return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value) ? value : `prompt:${createHash3("sha256").update(value).digest("hex")}`;
|
|
3667
|
+
function sessionObservationValue(input) {
|
|
3668
|
+
return { status: "started", host_kind: "claude", source: input.source, model: input.model ?? "unavailable", decoder_evidence_version: "claude-code-2.1.258" };
|
|
3669
|
+
}
|
|
3670
|
+
function sourceBoundTranscriptPath(path, transcriptsRoot) {
|
|
3671
|
+
if (!isAbsolute2(path) || !isAbsolute2(transcriptsRoot)) throw new ClaudeHostError("configuration", "Claude transcript registration paths must be absolute.");
|
|
3672
|
+
const transcript = resolve(path);
|
|
3673
|
+
const root = resolve(transcriptsRoot);
|
|
3674
|
+
const child = relative(root, transcript);
|
|
3675
|
+
if (child === "" || child === ".." || child.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute2(child)) {
|
|
3676
|
+
throw new ClaudeHostError("invalid_input", "Claude transcript path is outside the configured transcripts root.");
|
|
3677
|
+
}
|
|
3678
|
+
return transcript;
|
|
2411
3679
|
}
|
|
2412
|
-
function
|
|
3680
|
+
async function refreshCurrentTurnCursor(store, registration, baseline, promptRef, turnGenerationRef, transcriptTurnRef) {
|
|
3681
|
+
const cursor = await store.readTranscriptCursor(registration);
|
|
3682
|
+
if (cursor?.status !== "active") {
|
|
3683
|
+
await store.writeTranscriptCursor(baseline.cursor);
|
|
3684
|
+
return;
|
|
3685
|
+
}
|
|
3686
|
+
if (baseline.current_line_start === void 0 || baseline.current_user_uuid === void 0 || transcriptTurnRef === void 0 || cursor.offset <= baseline.current_line_start || cursor.open_turn?.user_uuid !== baseline.current_user_uuid) return;
|
|
3687
|
+
await store.attachInvocationTurnGeneration({
|
|
3688
|
+
host_source_ref: registration.host_source_ref,
|
|
3689
|
+
host_session_ref: registration.host_session_ref,
|
|
3690
|
+
host_turn_ref: transcriptTurnRef
|
|
3691
|
+
}, promptRef, turnGenerationRef);
|
|
3692
|
+
}
|
|
3693
|
+
async function captureClaudeTranscriptBaseline(registration, now, currentPrompt) {
|
|
3694
|
+
let handle;
|
|
3695
|
+
try {
|
|
3696
|
+
handle = await open4(registration.transcript_path, O_RDONLY4 | O_NOFOLLOW3);
|
|
3697
|
+
} catch (error) {
|
|
3698
|
+
if (isNotFound(error)) return { cursor: initialCursor(registration, now) };
|
|
3699
|
+
throw error;
|
|
3700
|
+
}
|
|
3701
|
+
try {
|
|
3702
|
+
const file = await inspectClaudeTranscript(handle);
|
|
3703
|
+
const current = currentPrompt === void 0 ? void 0 : await currentClaudeUserLine(handle, file.size, registration.claude_session_id, currentPrompt);
|
|
3704
|
+
const offset = current?.offset ?? file.size;
|
|
3705
|
+
const cursor = await makeClaudeBaselineCursor(handle, registration, now, file, offset);
|
|
3706
|
+
return {
|
|
3707
|
+
cursor,
|
|
3708
|
+
...current?.user === void 0 ? {} : {
|
|
3709
|
+
current_turn_ref: claudeLocalTurnRef(registration.claude_session_id, current.user.uuid),
|
|
3710
|
+
current_user_uuid: current.user.uuid,
|
|
3711
|
+
current_line_start: current.offset
|
|
3712
|
+
}
|
|
3713
|
+
};
|
|
3714
|
+
} finally {
|
|
3715
|
+
await handle?.close();
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
async function inspectClaudeTranscript(handle) {
|
|
3719
|
+
const stats = await handle.stat();
|
|
3720
|
+
const uid = typeof process.geteuid === "function" ? process.geteuid() : stats.uid;
|
|
3721
|
+
if (!stats.isFile()) throw new ClaudeHostError("storage", "Claude transcript source is not a regular file.");
|
|
3722
|
+
if (stats.uid !== uid) throw new ClaudeHostError("storage", "Claude transcript source is owned by another user.");
|
|
3723
|
+
return { size: stats.size, device: String(stats.dev), inode: String(stats.ino) };
|
|
3724
|
+
}
|
|
3725
|
+
async function currentClaudeUserLine(handle, size, sessionId, prompt) {
|
|
3726
|
+
const lastLine = await readLastClaudeTranscriptLine(handle, size);
|
|
3727
|
+
if (lastLine === null) return void 0;
|
|
3728
|
+
const user = parseClaudeUserLine(lastLine.bytes.toString("utf8"), sessionId);
|
|
3729
|
+
if (user?.content === prompt) return { offset: lastLine.start, user };
|
|
3730
|
+
return { offset: lastLine.complete ? size : lastLine.start };
|
|
3731
|
+
}
|
|
3732
|
+
async function makeClaudeBaselineCursor(handle, registration, now, file, offset) {
|
|
3733
|
+
const anchorStart = Math.max(0, offset - CLAUDE_TRANSCRIPT_ANCHOR_BYTES);
|
|
3734
|
+
const anchor = Buffer.alloc(offset - anchorStart);
|
|
3735
|
+
if (anchor.length > 0) await handle.read(anchor, 0, anchor.length, anchorStart);
|
|
3736
|
+
return {
|
|
3737
|
+
schema_version: CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION,
|
|
3738
|
+
host_source_ref: registration.host_source_ref,
|
|
3739
|
+
host_session_ref: registration.host_session_ref,
|
|
3740
|
+
transcript_path: registration.transcript_path,
|
|
3741
|
+
device: file.device,
|
|
3742
|
+
inode: file.inode,
|
|
3743
|
+
offset,
|
|
3744
|
+
anchor_start: anchorStart,
|
|
3745
|
+
anchor_sha256: createHash4("sha256").update(anchor).digest("hex"),
|
|
3746
|
+
subagents: {},
|
|
3747
|
+
status: "active",
|
|
3748
|
+
updated_at: timestamp4(now)
|
|
3749
|
+
};
|
|
3750
|
+
}
|
|
3751
|
+
async function readLastClaudeTranscriptLine(handle, size) {
|
|
3752
|
+
if (size === 0) return null;
|
|
3753
|
+
const last = Buffer.alloc(1);
|
|
3754
|
+
await handle.read(last, 0, 1, size - 1);
|
|
3755
|
+
const complete = last[0] === 10;
|
|
3756
|
+
let end = complete ? size - 1 : size;
|
|
3757
|
+
if (end === 0) return null;
|
|
3758
|
+
let position = end;
|
|
3759
|
+
const chunks = [];
|
|
3760
|
+
let total = 0;
|
|
3761
|
+
const maximumBytes = 512 * 1024;
|
|
3762
|
+
while (position > 0 && total <= maximumBytes) {
|
|
3763
|
+
const start = Math.max(0, position - 64 * 1024);
|
|
3764
|
+
const buffer = Buffer.alloc(position - start);
|
|
3765
|
+
const read = await handle.read(buffer, 0, buffer.length, start);
|
|
3766
|
+
const chunk = buffer.subarray(0, read.bytesRead);
|
|
3767
|
+
const newline = chunk.lastIndexOf(10);
|
|
3768
|
+
if (newline >= 0) return { start: start + newline + 1, bytes: Buffer.concat([chunk.subarray(newline + 1), ...chunks]), complete };
|
|
3769
|
+
chunks.unshift(chunk);
|
|
3770
|
+
total += chunk.length;
|
|
3771
|
+
position = start;
|
|
3772
|
+
}
|
|
3773
|
+
if (position === 0) return { start: 0, bytes: Buffer.concat(chunks), complete };
|
|
3774
|
+
return null;
|
|
3775
|
+
}
|
|
3776
|
+
function parseClaudeUserLine(line, sessionId) {
|
|
3777
|
+
const row = parseClaudeJsonObject(line);
|
|
3778
|
+
if (row === null || row.type !== "user" || row.sessionId !== sessionId || typeof row.uuid !== "string" || row.uuid.length === 0 || !isRecord2(row.message)) return null;
|
|
3779
|
+
return typeof row.message.content === "string" ? { uuid: row.uuid, content: row.message.content } : null;
|
|
3780
|
+
}
|
|
3781
|
+
function parseClaudeJsonObject(line) {
|
|
3782
|
+
try {
|
|
3783
|
+
const value = JSON.parse(line);
|
|
3784
|
+
return isRecord2(value) ? value : null;
|
|
3785
|
+
} catch {
|
|
3786
|
+
return null;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
function isRecord2(value) {
|
|
2413
3790
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2414
3791
|
}
|
|
2415
|
-
function
|
|
2416
|
-
return
|
|
3792
|
+
function isNotFound(error) {
|
|
3793
|
+
return isRecord2(error) && error.code === "ENOENT";
|
|
3794
|
+
}
|
|
3795
|
+
function initialCursor(registration, now) {
|
|
3796
|
+
return {
|
|
3797
|
+
schema_version: CLAUDE_TRANSCRIPT_CURSOR_SCHEMA_VERSION,
|
|
3798
|
+
host_source_ref: registration.host_source_ref,
|
|
3799
|
+
host_session_ref: registration.host_session_ref,
|
|
3800
|
+
transcript_path: registration.transcript_path,
|
|
3801
|
+
offset: 0,
|
|
3802
|
+
anchor_start: 0,
|
|
3803
|
+
anchor_sha256: createHash4("sha256").update(Buffer.alloc(0)).digest("hex"),
|
|
3804
|
+
subagents: {},
|
|
3805
|
+
status: "active",
|
|
3806
|
+
updated_at: timestamp4(now)
|
|
3807
|
+
};
|
|
2417
3808
|
}
|
|
2418
3809
|
function timestamp4(now) {
|
|
2419
3810
|
const value = now();
|
|
@@ -2422,134 +3813,154 @@ function timestamp4(now) {
|
|
|
2422
3813
|
}
|
|
2423
3814
|
|
|
2424
3815
|
// host/claude/src/claude-producer.ts
|
|
2425
|
-
import { mkdir as mkdir3, readFile as readFile2, rename as rename2, writeFile } from "node:fs/promises";
|
|
2426
|
-
import { join as join3 } from "node:path";
|
|
2427
|
-
var STATE_SCHEMA = "ceal.claude_producer_state.v1";
|
|
2428
|
-
var SAFE_REF4 = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
2429
3816
|
var ClaudeProducer = class {
|
|
3817
|
+
producer_id = "claude";
|
|
3818
|
+
producer_label = "Claude";
|
|
3819
|
+
outbox_directory;
|
|
2430
3820
|
store;
|
|
2431
|
-
|
|
2432
|
-
installation;
|
|
2433
|
-
epoch;
|
|
2434
|
-
options;
|
|
2435
|
-
tail = Promise.resolve();
|
|
3821
|
+
core;
|
|
2436
3822
|
constructor(options) {
|
|
2437
|
-
if (!SAFE_REF4.test(options.producer_installation_ref)) throw new ClaudeHostError("configuration", "Claude producer installation identity is invalid.");
|
|
2438
|
-
this.options = options;
|
|
2439
|
-
this.epoch = options.producer_epoch ?? 1;
|
|
2440
|
-
if (!Number.isSafeInteger(this.epoch) || this.epoch <= 0) throw new ClaudeHostError("configuration", "Claude producer epoch is invalid.");
|
|
2441
3823
|
this.store = new ClaudeOutboxStore(options.outbox_dir);
|
|
2442
|
-
this.
|
|
2443
|
-
this.
|
|
3824
|
+
this.outbox_directory = this.store.directory;
|
|
3825
|
+
this.core = new HostProducerDrainCore(this, options);
|
|
2444
3826
|
}
|
|
2445
3827
|
drainOutbox() {
|
|
2446
|
-
|
|
2447
|
-
this.tail = run.then(() => void 0, () => void 0);
|
|
2448
|
-
return run;
|
|
3828
|
+
return this.core.drainOutbox();
|
|
2449
3829
|
}
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
let admitted = 0;
|
|
2453
|
-
let retained = 0;
|
|
2454
|
-
for (const batch of read.entries) {
|
|
2455
|
-
const frames = await this.reserve(batch);
|
|
2456
|
-
let complete = true;
|
|
2457
|
-
for (const frame of frames) {
|
|
2458
|
-
const outcome = await this.options.relay.admit(frame);
|
|
2459
|
-
if (outcome.code !== "admitted" && outcome.code !== "already_admitted") {
|
|
2460
|
-
complete = false;
|
|
2461
|
-
break;
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2464
|
-
if (!complete) {
|
|
2465
|
-
retained += 1;
|
|
2466
|
-
break;
|
|
2467
|
-
}
|
|
2468
|
-
await this.store.remove(batch);
|
|
2469
|
-
await this.release(batch.batch_ref);
|
|
2470
|
-
admitted += 1;
|
|
2471
|
-
}
|
|
2472
|
-
return { admitted, retained: retained + Math.max(0, read.entries.length - admitted - retained), malformed: read.malformed };
|
|
3830
|
+
fail(code2, message) {
|
|
3831
|
+
return new ClaudeHostError(code2, message);
|
|
2473
3832
|
}
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
const existing = state.pending[batch.batch_ref];
|
|
2477
|
-
if (existing !== void 0) return existing;
|
|
2478
|
-
const frames = batch.frames.map((draft, offset) => ({ schema_version: "ceal.host_producer_frame.v1", producer_id: "claude", producer_installation_ref: this.installation, producer_epoch: this.epoch, sequence: state.next_sequence + offset, frame_kind: draft.frame_kind, occurred_at: batch.occurred_at, payload: draft.payload }));
|
|
2479
|
-
await this.writeState({ ...state, next_sequence: state.next_sequence + frames.length, pending: { ...state.pending, [batch.batch_ref]: frames } });
|
|
2480
|
-
return frames;
|
|
3833
|
+
listEnvelopes() {
|
|
3834
|
+
return this.store.listEnvelopes();
|
|
2481
3835
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
if (state.pending[ref] === void 0) return;
|
|
2485
|
-
const pending = { ...state.pending };
|
|
2486
|
-
delete pending[ref];
|
|
2487
|
-
await this.writeState({ ...state, pending });
|
|
3836
|
+
removeEnvelope(envelope) {
|
|
3837
|
+
return this.store.removeEnvelope(envelope);
|
|
2488
3838
|
}
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
return decodeState(JSON.parse(await readFile2(this.statePath, "utf8")), this.installation, this.epoch);
|
|
2492
|
-
} catch (error) {
|
|
2493
|
-
if (code2(error) !== "ENOENT") throw error;
|
|
2494
|
-
return { schema_version: STATE_SCHEMA, producer_installation_ref: this.installation, producer_epoch: this.epoch, next_sequence: 1, pending: {} };
|
|
2495
|
-
}
|
|
3839
|
+
submissionRefOf(envelope) {
|
|
3840
|
+
return envelope.submission_ref;
|
|
2496
3841
|
}
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
3842
|
+
expiresAtOf(envelope) {
|
|
3843
|
+
return envelope.expires_at;
|
|
3844
|
+
}
|
|
3845
|
+
createdAtOf(envelope) {
|
|
3846
|
+
return envelope.created_at;
|
|
3847
|
+
}
|
|
3848
|
+
async mapEnvelope(envelope) {
|
|
3849
|
+
const prompt = observation(envelope, "prompt");
|
|
3850
|
+
if (prompt !== void 0) return this.mapInput(envelope, prompt);
|
|
3851
|
+
return [mapTerminal(envelope)];
|
|
3852
|
+
}
|
|
3853
|
+
async mapInput(envelope, prompt) {
|
|
3854
|
+
const drafts = [];
|
|
3855
|
+
const session = observation(envelope, "session");
|
|
3856
|
+
if (session !== void 0) {
|
|
3857
|
+
const registrations = await this.store.listTranscriptRegistrations();
|
|
3858
|
+
const registration = registrations.find((item) => item.host_source_ref === envelope.identity.host_source_ref && item.host_session_ref === envelope.identity.host_session_ref);
|
|
3859
|
+
const directoryIdentity = registration?.cwd ?? envelope.identity.host_session_ref;
|
|
3860
|
+
drafts.push({
|
|
3861
|
+
frame_kind: "session_started",
|
|
3862
|
+
payload: compact({
|
|
3863
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3864
|
+
working_directory_ref: claudeWorkspaceRef(directoryIdentity),
|
|
3865
|
+
model: recordText(session.value, "model"),
|
|
3866
|
+
observations: [session]
|
|
3867
|
+
})
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
3870
|
+
const turn = observation(envelope, "turn");
|
|
3871
|
+
if (turn !== void 0) drafts.push({
|
|
3872
|
+
frame_kind: "turn_started",
|
|
3873
|
+
payload: compact({
|
|
3874
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3875
|
+
local_turn_ref: localTurnRef(envelope),
|
|
3876
|
+
observations: [turn]
|
|
3877
|
+
})
|
|
3878
|
+
});
|
|
3879
|
+
if (!isCealHostPromptObservationValue(prompt.value)) throw new ClaudeHostError("storage", "Claude prompt observation is malformed.");
|
|
3880
|
+
drafts.push({
|
|
3881
|
+
frame_kind: "user_input",
|
|
3882
|
+
payload: compact({
|
|
3883
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3884
|
+
local_turn_ref: localTurnRef(envelope),
|
|
3885
|
+
local_input_ref: envelope.host_input_ref,
|
|
3886
|
+
input_kind: envelope.input_kind,
|
|
3887
|
+
origin_input: envelope.origin_input,
|
|
3888
|
+
observations: [prompt],
|
|
3889
|
+
context_expires_at: envelope.expires_at
|
|
3890
|
+
})
|
|
3891
|
+
});
|
|
3892
|
+
return drafts;
|
|
2502
3893
|
}
|
|
2503
3894
|
};
|
|
2504
|
-
function
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
}
|
|
2517
|
-
function
|
|
2518
|
-
return
|
|
2519
|
-
}
|
|
2520
|
-
function
|
|
2521
|
-
return
|
|
3895
|
+
function mapTerminal(envelope) {
|
|
3896
|
+
const usage = envelope.observations.filter((item) => ["token", "cost"].includes(item.observation_kind));
|
|
3897
|
+
return {
|
|
3898
|
+
frame_kind: "turn_terminal",
|
|
3899
|
+
payload: compact({
|
|
3900
|
+
local_session_ref: envelope.identity.host_session_ref,
|
|
3901
|
+
local_turn_ref: localTurnRef(envelope),
|
|
3902
|
+
terminal_state: "completed",
|
|
3903
|
+
usage: usage.length === 0 ? void 0 : Object.fromEntries(usage.map((item) => [item.observation_kind, item.value])),
|
|
3904
|
+
observations: envelope.observations
|
|
3905
|
+
})
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
function observation(envelope, kind) {
|
|
3909
|
+
return envelope.observations.find((item) => item.observation_kind === kind);
|
|
3910
|
+
}
|
|
3911
|
+
function localTurnRef(envelope) {
|
|
3912
|
+
return envelope.identity.host_turn_ref;
|
|
3913
|
+
}
|
|
3914
|
+
function compact(value) {
|
|
3915
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== void 0));
|
|
2522
3916
|
}
|
|
2523
|
-
function
|
|
2524
|
-
|
|
2525
|
-
const encoded = Buffer.allocUnsafe(json.length + 4);
|
|
2526
|
-
encoded.writeUInt32BE(json.length, 0);
|
|
2527
|
-
json.copy(encoded, 4);
|
|
2528
|
-
const frame = decodeHostProducerFrame(encoded);
|
|
2529
|
-
if (frame.producer_id !== "claude" || frame.producer_installation_ref !== installation || frame.producer_epoch > epoch) throw new ClaudeHostError("storage", `Claude producer frame identity is malformed (${refusalFacts([{ name: "producer_id", value: frame.producer_id, expected: "claude" }, { name: "producer_installation_ref", value: frame.producer_installation_ref, expected: installation }, { name: "producer_epoch", value: frame.producer_epoch, expected: `at most ${epoch}` }])}).`);
|
|
2530
|
-
return frame;
|
|
3917
|
+
function asRecord(value) {
|
|
3918
|
+
return isRecord3(value) ? value : void 0;
|
|
2531
3919
|
}
|
|
2532
|
-
function
|
|
3920
|
+
function recordText(value, key) {
|
|
3921
|
+
const record6 = asRecord(value);
|
|
3922
|
+
return typeof record6?.[key] === "string" ? record6[key] : void 0;
|
|
3923
|
+
}
|
|
3924
|
+
function isRecord3(value) {
|
|
2533
3925
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2534
3926
|
}
|
|
2535
|
-
|
|
2536
|
-
|
|
3927
|
+
|
|
3928
|
+
// host/service-log.ts
|
|
3929
|
+
function writeHostServiceDiagnostic(producer_id, event, failure_kind) {
|
|
3930
|
+
try {
|
|
3931
|
+
process.stderr.write(`${JSON.stringify({ schema_version: CEAL_HOST_SERVICE_DIAGNOSTIC_SCHEMA, producer_id, event, ...event === "watcher_scan_failed" && failure_kind !== void 0 ? { failure_kind } : {} })}
|
|
3932
|
+
`);
|
|
3933
|
+
} catch {
|
|
3934
|
+
}
|
|
2537
3935
|
}
|
|
3936
|
+
var HOST_SERVICE_LOG_MAX_BYTES = 1024 * 1024;
|
|
3937
|
+
var HOST_SERVICE_LOG_TAIL_BYTES = 64 * 1024;
|
|
2538
3938
|
|
|
2539
3939
|
// host/claude/src/claude-producer-runtime.ts
|
|
2540
3940
|
async function drainClaudeProducerOnce(options) {
|
|
2541
|
-
|
|
3941
|
+
const promptPort = new HostRelayCurrentPromptPort(options.relay_endpoint, options.timeout_ms);
|
|
3942
|
+
const scan = await new ClaudeTranscriptWatcher({
|
|
3943
|
+
outbox_dir: options.outbox_dir,
|
|
3944
|
+
producer_installation_ref: options.producer_installation_ref,
|
|
3945
|
+
register_current_prompt: (input) => promptPort.register(input),
|
|
3946
|
+
diagnostic: (event, failure_kind) => writeHostServiceDiagnostic("claude", event, failure_kind)
|
|
3947
|
+
}).scanOnce().catch((error) => {
|
|
3948
|
+
writeHostServiceDiagnostic("claude", "watcher_scan_failed", claudeTranscriptScanFailureKind(error));
|
|
3949
|
+
throw error;
|
|
3950
|
+
});
|
|
3951
|
+
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 } });
|
|
3952
|
+
return { scanned: scan.enqueued, ...await producer.drainOutbox() };
|
|
2542
3953
|
}
|
|
2543
3954
|
|
|
2544
3955
|
// host/claude/src/claude-hook-runner.ts
|
|
2545
|
-
import { homedir } from "node:os";
|
|
2546
|
-
import { join as
|
|
3956
|
+
import { homedir as homedir2 } from "node:os";
|
|
3957
|
+
import { join as join6 } from "node:path";
|
|
2547
3958
|
|
|
2548
3959
|
// host/producer-service.ts
|
|
2549
3960
|
import { posix as posix3, win32 as win323 } from "node:path";
|
|
2550
3961
|
|
|
2551
3962
|
// host/runtime-source-paths.ts
|
|
2552
|
-
import { createHash as
|
|
3963
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
2553
3964
|
import { posix as posix2, win32 as win322 } from "node:path";
|
|
2554
3965
|
|
|
2555
3966
|
// host/absolute-path.ts
|
|
@@ -2571,7 +3982,7 @@ function hostRuntimeSourcePaths(stateRoot, sourceRef, platform = process.platfor
|
|
|
2571
3982
|
const sourcesRoot = path.join(stateRoot, "sources");
|
|
2572
3983
|
const sourceRoot = path.join(sourcesRoot, sourceId);
|
|
2573
3984
|
const runtimeRoot = path.join(stateRoot, "run");
|
|
2574
|
-
const endpoint = platform === "win32" ? `\\\\.\\pipe\\ceal-host-${
|
|
3985
|
+
const endpoint = platform === "win32" ? `\\\\.\\pipe\\ceal-host-${createHash5("sha256").update(`${stateRoot}\0${sourceRef}`).digest("hex").slice(0, 24)}` : path.join(runtimeRoot, `${sourceId}.sock`);
|
|
2575
3986
|
assertHostRuntimeIpcLength(endpoint, platform);
|
|
2576
3987
|
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 };
|
|
2577
3988
|
}
|
|
@@ -2587,19 +3998,24 @@ function assertHostProducerSourcePaths(stateRoot, config, producer, platform = p
|
|
|
2587
3998
|
|
|
2588
3999
|
// host/producer-service.ts
|
|
2589
4000
|
import { constants } from "node:fs";
|
|
2590
|
-
import { lstat as lstat2, open as
|
|
2591
|
-
var
|
|
4001
|
+
import { lstat as lstat2, open as open5 } from "node:fs/promises";
|
|
4002
|
+
var SAFE_REF4 = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
2592
4003
|
var HOST_PRODUCER_CONFIG_FLAG = "--ceal-host-config";
|
|
2593
4004
|
var HostProducerServiceError = class extends Error {
|
|
2594
4005
|
code;
|
|
2595
|
-
constructor(
|
|
2596
|
-
super(message ===
|
|
4006
|
+
constructor(code2, message = code2) {
|
|
4007
|
+
super(message === code2 ? code2 : `${code2}: ${message}`);
|
|
2597
4008
|
this.name = "HostProducerServiceError";
|
|
2598
|
-
this.code =
|
|
4009
|
+
this.code = code2;
|
|
2599
4010
|
}
|
|
2600
4011
|
};
|
|
2601
4012
|
var HOST_PRODUCER_CONFIG_KEYS = ["schema_version", "host_source_ref", "producer_installation_ref", "producer_epoch", "outbox_dir", "relay_endpoint"];
|
|
2602
4013
|
var HOST_PRODUCER_TIMING_DEFAULTS = Object.freeze({ reconcile_interval_ms: 250, retry_backoff_ms: 1e3, ipc_timeout_ms: 5e3 });
|
|
4014
|
+
function hostProducerExtraPath(extras, key) {
|
|
4015
|
+
const value = extras[key];
|
|
4016
|
+
if (value === void 0) throw hostProducerError("invalid_config");
|
|
4017
|
+
return value;
|
|
4018
|
+
}
|
|
2603
4019
|
function parseHostProducerConfigFlag(args, platform = process.platform) {
|
|
2604
4020
|
return parseProducerHostConfigFlag(args, HOST_PRODUCER_CONFIG_FLAG, platform, hostProducerError);
|
|
2605
4021
|
}
|
|
@@ -2620,8 +4036,8 @@ function decodeHostProducerConfigText(descriptor, text, platform = process.platf
|
|
|
2620
4036
|
const { extra_paths: extras, ...core } = decoded;
|
|
2621
4037
|
return Object.freeze(descriptor.config({ schema_version: descriptor.schema, ...core }, extras));
|
|
2622
4038
|
}
|
|
2623
|
-
function hostProducerError(
|
|
2624
|
-
return new HostProducerServiceError(
|
|
4039
|
+
function hostProducerError(code2, message = code2) {
|
|
4040
|
+
return new HostProducerServiceError(code2, message);
|
|
2625
4041
|
}
|
|
2626
4042
|
function parseProducerHostConfigFlag(args, flag, platform, error) {
|
|
2627
4043
|
const prefix = `${flag}=`;
|
|
@@ -2648,10 +4064,10 @@ async function readProducerConfigText(configPath, platform, error) {
|
|
|
2648
4064
|
}
|
|
2649
4065
|
function decodeProducerConfigCore(text, options) {
|
|
2650
4066
|
const value = parseCanonicalJson(text, options.error);
|
|
2651
|
-
if (!
|
|
4067
|
+
if (!isRecord4(value) || !hasConfigKeys(value, options.keys) || value.schema_version !== options.schema) throw options.error("invalid_config");
|
|
2652
4068
|
const hostSourceRef = typeof value.host_source_ref === "string" ? value.host_source_ref : "";
|
|
2653
4069
|
const installationRef = typeof value.producer_installation_ref === "string" ? value.producer_installation_ref : "";
|
|
2654
|
-
const producerEpoch =
|
|
4070
|
+
const producerEpoch = validateIdentity3(hostSourceRef, installationRef, value.producer_epoch, options.error);
|
|
2655
4071
|
assertProducerAbsolutePath(value.outbox_dir, options.platform, "runtime", options.error);
|
|
2656
4072
|
assertProducerRelayEndpoint(value.relay_endpoint, options.platform, options.error);
|
|
2657
4073
|
const extraPaths = {};
|
|
@@ -2693,7 +4109,7 @@ function validateConfigFile(value, platform, error) {
|
|
|
2693
4109
|
}
|
|
2694
4110
|
async function openConfigFile(configPath, flags, error) {
|
|
2695
4111
|
try {
|
|
2696
|
-
return await
|
|
4112
|
+
return await open5(configPath, flags);
|
|
2697
4113
|
} catch {
|
|
2698
4114
|
throw error("config_unavailable");
|
|
2699
4115
|
}
|
|
@@ -2732,8 +4148,8 @@ function firstDifferenceIndex(left, right) {
|
|
|
2732
4148
|
for (let index = 0; index < shared; index += 1) if (left[index] !== right[index]) return index;
|
|
2733
4149
|
return shared;
|
|
2734
4150
|
}
|
|
2735
|
-
function
|
|
2736
|
-
if (!
|
|
4151
|
+
function validateIdentity3(sourceRef, installationRef, epoch, error) {
|
|
4152
|
+
if (!SAFE_REF4.test(sourceRef) || !SAFE_REF4.test(installationRef) || !positive2(epoch, 2147483647)) throw error("invalid_config");
|
|
2737
4153
|
return epoch;
|
|
2738
4154
|
}
|
|
2739
4155
|
function validateIntervals(reconcile, retry, timeout, error) {
|
|
@@ -2743,7 +4159,7 @@ function validateIntervals(reconcile, retry, timeout, error) {
|
|
|
2743
4159
|
function hasConfigKeys(value, required) {
|
|
2744
4160
|
return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => required.includes(key) || Object.hasOwn(HOST_PRODUCER_TIMING_DEFAULTS, key));
|
|
2745
4161
|
}
|
|
2746
|
-
function
|
|
4162
|
+
function isRecord4(value) {
|
|
2747
4163
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2748
4164
|
}
|
|
2749
4165
|
function positive2(value, maximum) {
|
|
@@ -2755,10 +4171,12 @@ function boundedMilliseconds(value) {
|
|
|
2755
4171
|
|
|
2756
4172
|
// host/claude/src/claude-producer-service.ts
|
|
2757
4173
|
var CLAUDE_PRODUCER_CONFIG_SCHEMA = "ceal.claude_producer_config.v1";
|
|
4174
|
+
var CLAUDE_TRANSCRIPTS_ROOT = "claude_transcripts_root";
|
|
2758
4175
|
var CLAUDE_PRODUCER_DESCRIPTOR = {
|
|
2759
4176
|
schema: CLAUDE_PRODUCER_CONFIG_SCHEMA,
|
|
2760
4177
|
producer_id: "claude",
|
|
2761
|
-
|
|
4178
|
+
extra_paths: [CLAUDE_TRANSCRIPTS_ROOT],
|
|
4179
|
+
config: (shared, extras) => ({ ...shared, claude_transcripts_root: hostProducerExtraPath(extras, CLAUDE_TRANSCRIPTS_ROOT) }),
|
|
2762
4180
|
runtime_options: (config) => Object.freeze({
|
|
2763
4181
|
outbox_dir: config.outbox_dir,
|
|
2764
4182
|
producer_installation_ref: config.producer_installation_ref,
|
|
@@ -2778,39 +4196,38 @@ function claudeHookEventFromArgs(args) {
|
|
|
2778
4196
|
const value = args.find((arg) => arg.startsWith("--ceal-hook-event="))?.slice("--ceal-hook-event=".length);
|
|
2779
4197
|
return isClaudeHookEvent(value) ? value : null;
|
|
2780
4198
|
}
|
|
2781
|
-
async function
|
|
2782
|
-
const event = claudeHookEventFromArgs(args);
|
|
2783
|
-
if (event === null) return 0;
|
|
4199
|
+
async function runClaudeHook(event, stdin, options) {
|
|
2784
4200
|
try {
|
|
2785
4201
|
const result = await runClaudeHookFromStdin(event, stdin, options);
|
|
2786
|
-
if (result.disposition === "rejected")
|
|
4202
|
+
if (result.disposition === "rejected") writeHostServiceDiagnostic("claude", "hook_failed");
|
|
4203
|
+
else if (result.reason === "transcript_registered") writeHostServiceDiagnostic("claude", "registration_written");
|
|
2787
4204
|
return 0;
|
|
2788
|
-
} catch
|
|
2789
|
-
|
|
4205
|
+
} catch {
|
|
4206
|
+
writeHostServiceDiagnostic("claude", "hook_failed");
|
|
2790
4207
|
return 0;
|
|
2791
4208
|
}
|
|
2792
4209
|
}
|
|
2793
|
-
function
|
|
4210
|
+
async function runClaudeHookCli() {
|
|
4211
|
+
const args = process.argv.slice(2);
|
|
4212
|
+
const event = claudeHookEventFromArgs(args);
|
|
4213
|
+
if (event === null) return 0;
|
|
4214
|
+
writeHostServiceDiagnostic("claude", "hook_invoked");
|
|
2794
4215
|
try {
|
|
2795
|
-
|
|
2796
|
-
|
|
4216
|
+
const options = hasHostConfigArgument(args) ? await installedClaudeHookRunnerOptions(args) : standaloneClaudeHookRunnerOptions();
|
|
4217
|
+
return runClaudeHook(event, process.stdin, options);
|
|
2797
4218
|
} catch {
|
|
4219
|
+
writeHostServiceDiagnostic("claude", "hook_failed");
|
|
4220
|
+
return 0;
|
|
2798
4221
|
}
|
|
2799
4222
|
}
|
|
2800
|
-
async function runClaudeHookCli() {
|
|
2801
|
-
const args = process.argv.slice(2);
|
|
2802
|
-
const options = hasHostConfigArgument(args) ? await installedClaudeHookRunnerOptions(args) : standaloneClaudeHookRunnerOptions();
|
|
2803
|
-
return runClaudeHookProcess(args, process.stdin, options);
|
|
2804
|
-
}
|
|
2805
4223
|
async function installedClaudeHookRunnerOptions(args) {
|
|
2806
4224
|
const config = await readHostProducerConfig(CLAUDE_PRODUCER_DESCRIPTOR, parseHostProducerConfigFlag(args));
|
|
2807
|
-
const
|
|
2808
|
-
const options = { outbox_dir: config.outbox_dir, producer_installation_ref: config.producer_installation_ref, register_current_prompt: (input) => port.register(input) };
|
|
4225
|
+
const options = { outbox_dir: config.outbox_dir, claude_transcripts_root: config.claude_transcripts_root };
|
|
2809
4226
|
Object.defineProperty(options, "host_source_ref", { value: config.host_source_ref, enumerable: false });
|
|
2810
4227
|
return options;
|
|
2811
4228
|
}
|
|
2812
4229
|
function standaloneClaudeHookRunnerOptions() {
|
|
2813
|
-
return { outbox_dir: process.env.CEAL_CLAUDE_OUTBOX_DIR ??
|
|
4230
|
+
return { outbox_dir: process.env.CEAL_CLAUDE_OUTBOX_DIR ?? join6(homedir2(), ".claude", ".ceal", "claude-observation-outbox"), claude_transcripts_root: process.env.CLAUDE_CONFIG_DIR !== void 0 ? join6(process.env.CLAUDE_CONFIG_DIR, "projects") : join6(homedir2(), ".claude", "projects") };
|
|
2814
4231
|
}
|
|
2815
4232
|
function hasHostConfigArgument(args) {
|
|
2816
4233
|
return args.some((argument) => argument === HOST_PRODUCER_CONFIG_FLAG || argument.startsWith(`${HOST_PRODUCER_CONFIG_FLAG}=`));
|
|
@@ -2842,6 +4259,7 @@ var DOCTOR_FLAGS = Object.freeze({
|
|
|
2842
4259
|
var execFileAsync = promisify(execFile);
|
|
2843
4260
|
|
|
2844
4261
|
// host/claude/src/claude-installer.ts
|
|
4262
|
+
var ACTIVE_CLAUDE_HOOK_EVENTS = new Set(CLAUDE_HOOK_EVENTS);
|
|
2845
4263
|
var CLAUDE_LOCAL_CAPABILITY_ACTIVATION = Object.freeze({ environment: "CLAUDE_CODE_ENTRYPOINT", value: "cli" });
|
|
2846
4264
|
var CLAUDE_CLIENT_ALLOW_RULE = `Bash(${CEAL_SESSION_ENTRY_INVOCATION}:*)`;
|
|
2847
4265
|
|