@corca-org/ceal-host-win32-x64 1.0.19 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ceal-host.exe +0 -0
- package/libexec/ceal-host/claude-hook.mjs +200 -75
- package/libexec/ceal-host/claude-invocation-authentication.mjs +174 -81
- package/libexec/ceal-host/claude-producer.mjs +212 -119
- package/libexec/ceal-host/codex-hook.mjs +216 -25
- package/libexec/ceal-host/codex-invocation-authentication.mjs +207 -46
- package/libexec/ceal-host/codex-producer.mjs +259 -90
- package/libexec/ceal-host/control.mjs +764 -212
- package/libexec/ceal-host/relay.mjs +92 -14
- package/package.json +1 -1
- package/share/ceal-host/bundle-manifest.json +12 -12
- package/share/ceal-host/bundle-manifest.sigstore.json +1 -1
|
@@ -3093,6 +3093,7 @@ var OPTIONAL_FIELDS = CEAL_CLIENT_WIRE_CONTRACT.command_response.bare_document_o
|
|
|
3093
3093
|
var CEAL_SYSTEMCTL_EXECUTABLE_PATH = "/usr/bin/systemctl";
|
|
3094
3094
|
|
|
3095
3095
|
// packages/ceal-client-protocol/src/resource-row.ts
|
|
3096
|
+
var CEAL_RESOURCE_ROW_TITLE_MATCH_DISPOSITIONS = vocabulary(["exact", "not_exact", "unknown"]);
|
|
3096
3097
|
var CEAL_RESOURCE_SEARCH_ORDERS = vocabulary(["relevance", "modified", "viewed", "created"]);
|
|
3097
3098
|
var CEAL_RESOURCE_SEARCH_DIRECTIONS = vocabulary(["ascending", "descending"]);
|
|
3098
3099
|
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"]);
|
|
@@ -3176,6 +3177,7 @@ var RESOURCE_ROW_KEY_RULES = Object.freeze({
|
|
|
3176
3177
|
next: { required: true, timestamp: false },
|
|
3177
3178
|
kind: { required: true, timestamp: false },
|
|
3178
3179
|
matched_kinds: { required: false, timestamp: false },
|
|
3180
|
+
title_match: { required: false, timestamp: false },
|
|
3179
3181
|
title: { required: true, timestamp: false },
|
|
3180
3182
|
container_label: { required: true, timestamp: false },
|
|
3181
3183
|
container_ref: { required: false, timestamp: false },
|
|
@@ -3663,7 +3665,7 @@ var CEAL_HOST_AUTHENTICATION_POLICY = Object.freeze({
|
|
|
3663
3665
|
invalid_arguments: Object.freeze({ classification: "host_arguments_invalid", next_action: "answer_with_gap" }),
|
|
3664
3666
|
options_invalid: Object.freeze({ classification: "host_configuration_invalid", next_action: "answer_with_gap" }),
|
|
3665
3667
|
session_unavailable: Object.freeze({ classification: "host_session_unavailable", next_action: "answer_with_gap" }),
|
|
3666
|
-
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;
|
|
3668
|
+
context_unavailable: Object.freeze({ classification: "host_context_unavailable", next_action: "refresh_host_invocation_context", remedy: "the Host has no invocation context from the current transcript turn; send a new message to start a fresh Host turn, then retry this command on that turn (run `ceal-host install` only if the Host hook registration is missing or drifted)" }),
|
|
3667
3669
|
context_replaced: Object.freeze({ classification: "host_invocation_context_stale", next_action: "refresh_host_invocation_context" }),
|
|
3668
3670
|
no_binding: Object.freeze({ classification: "host_binding_unavailable", next_action: "refresh_host_invocation_context" }),
|
|
3669
3671
|
context_stale: Object.freeze({ classification: "host_invocation_context_stale", next_action: "refresh_host_invocation_context" }),
|
|
@@ -3693,6 +3695,7 @@ var CEAL_HOST_SERVICE_DIAGNOSTIC_EVENTS = vocabulary([
|
|
|
3693
3695
|
"watcher_turn_observed",
|
|
3694
3696
|
"current_prompt_registered",
|
|
3695
3697
|
"current_prompt_registration_failed",
|
|
3698
|
+
"current_prompt_capacity_exhausted",
|
|
3696
3699
|
"activation_recorded",
|
|
3697
3700
|
"activation_failed",
|
|
3698
3701
|
"watcher_scan_failed",
|
|
@@ -4357,7 +4360,7 @@ import { promisify as promisify2 } from "node:util";
|
|
|
4357
4360
|
// host/relay/src/host-producer-core.ts
|
|
4358
4361
|
import { createHash as createHash3 } from "node:crypto";
|
|
4359
4362
|
import { O_NOFOLLOW as O_NOFOLLOW2, O_RDONLY as O_RDONLY3 } from "node:constants";
|
|
4360
|
-
import { mkdir as mkdir2, open as
|
|
4363
|
+
import { mkdir as mkdir2, open as open5, readFile, rename as rename2, writeFile } from "node:fs/promises";
|
|
4361
4364
|
import { join as join2 } from "node:path";
|
|
4362
4365
|
|
|
4363
4366
|
// host/relay/src/relay-ipc.ts
|
|
@@ -4366,6 +4369,60 @@ import { createConnection, createServer } from "node:net";
|
|
|
4366
4369
|
// host/relay/src/current-prompt-custody.ts
|
|
4367
4370
|
var HOST_CURRENT_PROMPT_MAX_BYTES = 256 * 1024;
|
|
4368
4371
|
|
|
4372
|
+
// host/service-log.ts
|
|
4373
|
+
import { constants as constants2 } from "node:fs";
|
|
4374
|
+
import { copyFile, open as open4, stat, truncate } from "node:fs/promises";
|
|
4375
|
+
var HOST_SERVICE_LOG_ENVIRONMENT = "CEAL_HOST_SERVICE_LOG";
|
|
4376
|
+
function writeHostServiceDiagnostic(producer_id, event, failure_kind, cause, sink) {
|
|
4377
|
+
const line = `${JSON.stringify({ schema_version: CEAL_HOST_SERVICE_DIAGNOSTIC_SCHEMA, producer_id, event, ...event === "watcher_scan_failed" && failure_kind !== void 0 ? { failure_kind } : {}, ...event === "watcher_scan_failed" && cause !== void 0 ? { cause } : {} })}
|
|
4378
|
+
`;
|
|
4379
|
+
try {
|
|
4380
|
+
(sink ?? defaultHostServiceDiagnosticSink)(line);
|
|
4381
|
+
} catch {
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
function defaultHostServiceDiagnosticSink(line) {
|
|
4385
|
+
process.stderr.write(line);
|
|
4386
|
+
}
|
|
4387
|
+
var HOST_SERVICE_LOG_MAX_BYTES = 1024 * 1024;
|
|
4388
|
+
function hostServiceLogPreviousPath(path) {
|
|
4389
|
+
return `${path}.1`;
|
|
4390
|
+
}
|
|
4391
|
+
var HOST_SERVICE_LOG_TAIL_BYTES = 64 * 1024;
|
|
4392
|
+
async function readHostServiceLogTail(path, lines) {
|
|
4393
|
+
const current = await readTail(path);
|
|
4394
|
+
if (current.length >= lines) return current.slice(-lines);
|
|
4395
|
+
return [...await readTail(hostServiceLogPreviousPath(path)), ...current].slice(-lines);
|
|
4396
|
+
}
|
|
4397
|
+
async function readTail(path) {
|
|
4398
|
+
let handle;
|
|
4399
|
+
try {
|
|
4400
|
+
handle = await open4(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
4401
|
+
} catch (error) {
|
|
4402
|
+
if (isNotFound(error) || errorCode2(error) === "ELOOP") return [];
|
|
4403
|
+
throw error;
|
|
4404
|
+
}
|
|
4405
|
+
try {
|
|
4406
|
+
const info = await handle.stat();
|
|
4407
|
+
if (!info.isFile()) return [];
|
|
4408
|
+
const length = Math.min(info.size, HOST_SERVICE_LOG_TAIL_BYTES);
|
|
4409
|
+
const buffer = Buffer.alloc(length);
|
|
4410
|
+
const { bytesRead } = await handle.read(buffer, 0, length, info.size - length);
|
|
4411
|
+
const all = buffer.subarray(0, bytesRead).toString("utf8").split("\n");
|
|
4412
|
+
if (length < info.size) all.shift();
|
|
4413
|
+
all.pop();
|
|
4414
|
+
return all;
|
|
4415
|
+
} finally {
|
|
4416
|
+
await handle.close();
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
function isNotFound(error) {
|
|
4420
|
+
return errorCode2(error) === "ENOENT";
|
|
4421
|
+
}
|
|
4422
|
+
function errorCode2(error) {
|
|
4423
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "unknown";
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4369
4426
|
// host/relay/src/relay-ipc.ts
|
|
4370
4427
|
var HOST_RELAY_REQUEST_BINDING_KEYS = Object.freeze([
|
|
4371
4428
|
"schema_version",
|
|
@@ -4384,6 +4441,7 @@ var HOST_RELAY_REQUEST_BINDING_KEYS = Object.freeze([
|
|
|
4384
4441
|
]);
|
|
4385
4442
|
var HOST_RELAY_IPC_RESPONSE_SCHEMA = "ceal.host_relay_admission.v1";
|
|
4386
4443
|
var HOST_RELAY_CURRENT_PROMPT_REGISTER_SCHEMA = "ceal.host_relay_current_prompt_register.v1";
|
|
4444
|
+
var HOST_RELAY_CURRENT_PROMPT_RELEASE_SCHEMA = "ceal.host_relay_current_prompt_release.v1";
|
|
4387
4445
|
var HOST_RELAY_CURRENT_PROMPT_RESULT_SCHEMA = "ceal.host_relay_current_prompt_result.v1";
|
|
4388
4446
|
var HOST_RELAY_IPC_LENGTH_BYTES = Uint32Array.BYTES_PER_ELEMENT;
|
|
4389
4447
|
var HOST_RELAY_ADMISSION_RESPONSE_MAX_BYTES = 4096;
|
|
@@ -4442,11 +4500,17 @@ var HostRelayIpcError = class extends Error {
|
|
|
4442
4500
|
this.failure = failure;
|
|
4443
4501
|
}
|
|
4444
4502
|
};
|
|
4503
|
+
var HostRelayCurrentPromptCapacityRefusal = class extends Error {
|
|
4504
|
+
constructor() {
|
|
4505
|
+
super("current_prompt_capacity_exhausted");
|
|
4506
|
+
this.name = "HostRelayCurrentPromptCapacityRefusal";
|
|
4507
|
+
}
|
|
4508
|
+
};
|
|
4445
4509
|
function hostRelayExchangeFailure(error) {
|
|
4446
4510
|
return error instanceof HostRelayIpcError ? error.failure : void 0;
|
|
4447
4511
|
}
|
|
4448
4512
|
function socketErrorFailure(error) {
|
|
4449
|
-
switch (
|
|
4513
|
+
switch (errorCode3(error)) {
|
|
4450
4514
|
case "EACCES":
|
|
4451
4515
|
case "EPERM":
|
|
4452
4516
|
return "permission_denied";
|
|
@@ -4481,8 +4545,14 @@ var HostRelayCurrentPromptPort = class {
|
|
|
4481
4545
|
async register(input) {
|
|
4482
4546
|
const request2 = { schema_version: HOST_RELAY_CURRENT_PROMPT_REGISTER_SCHEMA, ...input };
|
|
4483
4547
|
const response2 = await exchange(this.endpoint, encodeLengthPrefixed(request2), this.timeout_ms, decodeCurrentPromptResponseBuffer);
|
|
4548
|
+
if (response2.status === "capacity_exhausted") throw new HostRelayCurrentPromptCapacityRefusal();
|
|
4484
4549
|
if (response2.status !== "registered") throw new HostRelayIpcError("relay_unavailable", response2.status);
|
|
4485
4550
|
}
|
|
4551
|
+
async release(identity) {
|
|
4552
|
+
const request2 = { schema_version: HOST_RELAY_CURRENT_PROMPT_RELEASE_SCHEMA, ...identity };
|
|
4553
|
+
const response2 = await exchange(this.endpoint, encodeLengthPrefixed(request2), this.timeout_ms, decodeCurrentPromptResponseBuffer);
|
|
4554
|
+
if (response2.status !== "released") throw new HostRelayIpcError("relay_unavailable", response2.status, "response_invalid");
|
|
4555
|
+
}
|
|
4486
4556
|
};
|
|
4487
4557
|
function submitHostProducerFrame(endpoint, frame, timeoutMs = 5e3) {
|
|
4488
4558
|
return exchange(endpoint, encodeHostProducerFrame(frame), timeoutMs, decodeResponseBuffer);
|
|
@@ -4565,7 +4635,7 @@ function decodeResponseBuffer(buffer) {
|
|
|
4565
4635
|
function decodeCurrentPromptResponseBuffer(buffer) {
|
|
4566
4636
|
const value = decodeLengthPrefixedJson(buffer, HOST_RELAY_ADMISSION_RESPONSE_MAX_BYTES);
|
|
4567
4637
|
if (value === null) return null;
|
|
4568
|
-
if (!record2(value) || !exactKeys2(value, ["schema_version", "status"]) || value.schema_version !== HOST_RELAY_CURRENT_PROMPT_RESULT_SCHEMA || value.status !== "registered" && value.status !== "current_prompt_unavailable") throw new HostRelayIpcError("relay_unavailable");
|
|
4638
|
+
if (!record2(value) || !exactKeys2(value, ["schema_version", "status"]) || value.schema_version !== HOST_RELAY_CURRENT_PROMPT_RESULT_SCHEMA || value.status !== "registered" && value.status !== "capacity_exhausted" && value.status !== "current_prompt_unavailable" && value.status !== "released") throw new HostRelayIpcError("relay_unavailable", "Host Relay current-prompt response is invalid.", "response_invalid");
|
|
4569
4639
|
return { schema_version: HOST_RELAY_CURRENT_PROMPT_RESULT_SCHEMA, status: value.status };
|
|
4570
4640
|
}
|
|
4571
4641
|
function decodeLengthPrefixedJson(input, maximum) {
|
|
@@ -4591,7 +4661,7 @@ function exactKeys2(value, required, optional = []) {
|
|
|
4591
4661
|
const keys = Object.keys(value);
|
|
4592
4662
|
return required.every((key) => Object.hasOwn(value, key)) && keys.every((key) => required.includes(key) || optional.includes(key));
|
|
4593
4663
|
}
|
|
4594
|
-
function
|
|
4664
|
+
function errorCode3(error) {
|
|
4595
4665
|
return record2(error) && typeof error.code === "string" ? error.code : null;
|
|
4596
4666
|
}
|
|
4597
4667
|
|
|
@@ -4601,6 +4671,29 @@ function isHostTranscriptCursorReason(value) {
|
|
|
4601
4671
|
return typeof value === "string" && HOST_TRANSCRIPT_CURSOR_REASONS.some((reason) => reason === value);
|
|
4602
4672
|
}
|
|
4603
4673
|
var TRANSCRIPT_MISSING_REASON = "transcript_missing";
|
|
4674
|
+
var TRANSCRIPT_SCAN_READ_ERROR_ERRNOS = ["EBADF", "EACCES", "EIO", "EMFILE", "ENFILE", "ENOENT", "ENOTDIR", "EPERM"];
|
|
4675
|
+
function transcriptScanFailureKind(error, adapterFailureKind) {
|
|
4676
|
+
if (transcriptErrorCode(error) === "ELOOP") return "symlink";
|
|
4677
|
+
const adapter = adapterFailureKind(error);
|
|
4678
|
+
if (adapter !== void 0) return adapter;
|
|
4679
|
+
const code2 = transcriptErrorCode(error);
|
|
4680
|
+
if (code2 === "EFBIG") return "size_budget";
|
|
4681
|
+
if (code2 !== null && TRANSCRIPT_SCAN_READ_ERROR_ERRNOS.includes(code2)) return "read_error";
|
|
4682
|
+
return "other";
|
|
4683
|
+
}
|
|
4684
|
+
async function registerHostCurrentPromptDiagnosed(register, diagnostic) {
|
|
4685
|
+
try {
|
|
4686
|
+
await register();
|
|
4687
|
+
diagnostic("current_prompt_registered");
|
|
4688
|
+
return true;
|
|
4689
|
+
} catch (error) {
|
|
4690
|
+
if (!(error instanceof HostRelayCurrentPromptCapacityRefusal)) diagnostic("current_prompt_registration_failed");
|
|
4691
|
+
throw error;
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
function transcriptErrorCode(error) {
|
|
4695
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
4696
|
+
}
|
|
4604
4697
|
var HOST_TRANSCRIPT_READ_CHUNK_BYTES = 256 * 1024;
|
|
4605
4698
|
async function readTranscriptCompleteChunk(handle, offset, size) {
|
|
4606
4699
|
const available = Math.max(0, size - offset);
|
|
@@ -4709,19 +4802,24 @@ var HostTranscriptWatcher = class {
|
|
|
4709
4802
|
return scanTranscriptRegistrations(await this.listRegistrations(), (registration) => this.scanRegistration(registration));
|
|
4710
4803
|
}
|
|
4711
4804
|
async scanRegistration(registration) {
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4805
|
+
try {
|
|
4806
|
+
return await this.openTranscript(registration, (handle) => this.scanOpened(handle, registration), {
|
|
4807
|
+
missing: () => this.retireMissing(registration),
|
|
4808
|
+
failed: (handle, error) => this.scanFailed(handle, registration, error)
|
|
4809
|
+
});
|
|
4810
|
+
} catch (error) {
|
|
4811
|
+
if (error instanceof HostRelayCurrentPromptCapacityRefusal) return { records: 0, enqueued: 0, quarantined: false };
|
|
4812
|
+
throw error;
|
|
4813
|
+
}
|
|
4716
4814
|
}
|
|
4717
4815
|
async openTranscript(registration, scan, handlers) {
|
|
4718
4816
|
let handle;
|
|
4719
4817
|
try {
|
|
4720
|
-
handle = await
|
|
4818
|
+
handle = await open5(this.transcriptPathOf(registration), O_RDONLY3 | O_NOFOLLOW2);
|
|
4721
4819
|
return await scan(handle);
|
|
4722
4820
|
} catch (error) {
|
|
4723
4821
|
if (handle === void 0 && isMissingTranscriptError(error)) return handlers.missing();
|
|
4724
|
-
if (hostRelayExchangeFailure(error) !== void 0) throw error;
|
|
4822
|
+
if (hostRelayExchangeFailure(error) !== void 0 || error instanceof HostRelayCurrentPromptCapacityRefusal) throw error;
|
|
4725
4823
|
return handlers.failed(handle, error);
|
|
4726
4824
|
} finally {
|
|
4727
4825
|
await handle?.close();
|
|
@@ -4862,7 +4960,7 @@ var HostProducerDrainCore = class {
|
|
|
4862
4960
|
const value = JSON.parse(await readFile(this.statePath, "utf8"));
|
|
4863
4961
|
return this.decodeState(value);
|
|
4864
4962
|
} catch (error) {
|
|
4865
|
-
if (
|
|
4963
|
+
if (errorCode4(error) !== "ENOENT") throw error;
|
|
4866
4964
|
return { schema_version: this.stateSchema, producer_installation_ref: this.installationRef, producer_epoch: this.epoch, next_sequence: 1, pending: {} };
|
|
4867
4965
|
}
|
|
4868
4966
|
}
|
|
@@ -4908,7 +5006,7 @@ var HostProducerDrainCore = class {
|
|
|
4908
5006
|
function isDrainStateHeader(value, schema, installationRef) {
|
|
4909
5007
|
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);
|
|
4910
5008
|
}
|
|
4911
|
-
function
|
|
5009
|
+
function errorCode4(error) {
|
|
4912
5010
|
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : null;
|
|
4913
5011
|
}
|
|
4914
5012
|
|
|
@@ -4922,56 +5020,6 @@ var DEFAULT_LIMITS = { global_count: 8192, global_bytes: 64 * 1024 * 1024, produ
|
|
|
4922
5020
|
var RETRY_SECONDS = [1, 2, 4, 8, 16, 32, 60];
|
|
4923
5021
|
var MAX_RETRYABLE_ATTEMPTS = RETRY_SECONDS.length + 10;
|
|
4924
5022
|
|
|
4925
|
-
// host/service-log.ts
|
|
4926
|
-
import { constants as constants2 } from "node:fs";
|
|
4927
|
-
import { copyFile, open as open5, stat, truncate } from "node:fs/promises";
|
|
4928
|
-
var HOST_SERVICE_LOG_ENVIRONMENT = "CEAL_HOST_SERVICE_LOG";
|
|
4929
|
-
function writeHostServiceDiagnostic(producer_id, event, failure_kind) {
|
|
4930
|
-
try {
|
|
4931
|
-
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 } : {} })}
|
|
4932
|
-
`);
|
|
4933
|
-
} catch {
|
|
4934
|
-
}
|
|
4935
|
-
}
|
|
4936
|
-
var HOST_SERVICE_LOG_MAX_BYTES = 1024 * 1024;
|
|
4937
|
-
function hostServiceLogPreviousPath(path) {
|
|
4938
|
-
return `${path}.1`;
|
|
4939
|
-
}
|
|
4940
|
-
var HOST_SERVICE_LOG_TAIL_BYTES = 64 * 1024;
|
|
4941
|
-
async function readHostServiceLogTail(path, lines) {
|
|
4942
|
-
const current = await readTail(path);
|
|
4943
|
-
if (current.length >= lines) return current.slice(-lines);
|
|
4944
|
-
return [...await readTail(hostServiceLogPreviousPath(path)), ...current].slice(-lines);
|
|
4945
|
-
}
|
|
4946
|
-
async function readTail(path) {
|
|
4947
|
-
let handle;
|
|
4948
|
-
try {
|
|
4949
|
-
handle = await open5(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
4950
|
-
} catch (error) {
|
|
4951
|
-
if (isNotFound(error) || errorCode4(error) === "ELOOP") return [];
|
|
4952
|
-
throw error;
|
|
4953
|
-
}
|
|
4954
|
-
try {
|
|
4955
|
-
const info = await handle.stat();
|
|
4956
|
-
if (!info.isFile()) return [];
|
|
4957
|
-
const length = Math.min(info.size, HOST_SERVICE_LOG_TAIL_BYTES);
|
|
4958
|
-
const buffer = Buffer.alloc(length);
|
|
4959
|
-
const { bytesRead } = await handle.read(buffer, 0, length, info.size - length);
|
|
4960
|
-
const all = buffer.subarray(0, bytesRead).toString("utf8").split("\n");
|
|
4961
|
-
if (length < info.size) all.shift();
|
|
4962
|
-
all.pop();
|
|
4963
|
-
return all;
|
|
4964
|
-
} finally {
|
|
4965
|
-
await handle.close();
|
|
4966
|
-
}
|
|
4967
|
-
}
|
|
4968
|
-
function isNotFound(error) {
|
|
4969
|
-
return errorCode4(error) === "ENOENT";
|
|
4970
|
-
}
|
|
4971
|
-
function errorCode4(error) {
|
|
4972
|
-
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "unknown";
|
|
4973
|
-
}
|
|
4974
|
-
|
|
4975
5023
|
// host/claude/src/claude-decoder.ts
|
|
4976
5024
|
import { createHash as createHash4 } from "node:crypto";
|
|
4977
5025
|
|
|
@@ -5628,6 +5676,7 @@ var TOKEN_OBSERVATION_SOURCE = "claude_transcript.message.usage";
|
|
|
5628
5676
|
var ClaudeTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
5629
5677
|
store;
|
|
5630
5678
|
registerCurrentPrompt;
|
|
5679
|
+
releaseCurrentPrompt;
|
|
5631
5680
|
diagnostic;
|
|
5632
5681
|
constructor(options) {
|
|
5633
5682
|
super(options, {
|
|
@@ -5637,6 +5686,7 @@ var ClaudeTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
|
5637
5686
|
});
|
|
5638
5687
|
this.store = new ClaudeOutboxStore(options.outbox_dir);
|
|
5639
5688
|
this.registerCurrentPrompt = options.register_current_prompt;
|
|
5689
|
+
this.releaseCurrentPrompt = options.release_current_prompt;
|
|
5640
5690
|
this.diagnostic = options.diagnostic ?? (() => void 0);
|
|
5641
5691
|
}
|
|
5642
5692
|
transcriptPathOf(registration) {
|
|
@@ -5649,19 +5699,23 @@ var ClaudeTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
|
5649
5699
|
const { size, device, inode } = await ownedClaudeTranscriptFile(handle);
|
|
5650
5700
|
const cursor = await this.preflight(registration, handle, size, device, inode);
|
|
5651
5701
|
if (cursor === null) return { records: 0, enqueued: 0, quarantined: true };
|
|
5652
|
-
if (cursor.status === "quarantined")
|
|
5702
|
+
if (cursor.status === "quarantined") {
|
|
5703
|
+
if (cursor.reason !== "transcript_malformed") await this.releaseStoredPromptBestEffort(registration.host_session_ref, cursor);
|
|
5704
|
+
return { records: 0, enqueued: 0, quarantined: true };
|
|
5705
|
+
}
|
|
5653
5706
|
const draft = mutableCursor(cursor);
|
|
5654
5707
|
await this.scanOpenedTranscript(registration, handle, draft, size, device, inode);
|
|
5655
5708
|
return { records: draft.consumed, enqueued: draft.enqueued, quarantined: false };
|
|
5656
5709
|
}
|
|
5657
5710
|
async scanFailed(handle, registration, error) {
|
|
5658
|
-
this.diagnostic("watcher_scan_failed", claudeTranscriptScanFailureKind(error));
|
|
5711
|
+
this.diagnostic("watcher_scan_failed", claudeTranscriptScanFailureKind(error), claudeTranscriptScanCause(handle, error));
|
|
5659
5712
|
if (handle === void 0 && isSymlinkTranscript(error)) return await this.quarantineUnopened(registration, "transcript_malformed");
|
|
5660
5713
|
if (error instanceof ClaudeHostError && error.code === "payload_rejected") return await this.quarantineDraft(await this.preservedCursor(registration));
|
|
5661
5714
|
await this.quarantineFailure(registration, handle);
|
|
5662
5715
|
return { records: 0, enqueued: 0, quarantined: true };
|
|
5663
5716
|
}
|
|
5664
5717
|
async retireMissing(registration) {
|
|
5718
|
+
await this.releaseStoredPromptBestEffort(registration.host_session_ref, await this.store.readTranscriptCursor(registration).catch(() => null));
|
|
5665
5719
|
return this.retireMissingTranscript(registration.transcript_path, "Claude", () => this.store.retireTranscriptRegistration(registration));
|
|
5666
5720
|
}
|
|
5667
5721
|
initialCursor(registration) {
|
|
@@ -5708,10 +5762,10 @@ var ClaudeTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
|
5708
5762
|
if (isUserTurnLine(record7)) await this.openTurn(registration, draft, record7);
|
|
5709
5763
|
else if (isAssistantUsageLine(record7)) this.accumulateUsage(draft, toUsage(record7.uuid, record7.timestamp, record7.message.usage));
|
|
5710
5764
|
else if (isAssistantLine(record7) && record7.message.usage !== void 0) throw new ClaudeHostError("payload_rejected", "Claude transcript usage is malformed.");
|
|
5765
|
+
if (isAssistantTerminalLine(record7)) await this.releaseOpenTurn(registration, draft);
|
|
5711
5766
|
}
|
|
5712
5767
|
}
|
|
5713
5768
|
async openTurn(registration, draft, record7) {
|
|
5714
|
-
this.diagnostic("watcher_turn_observed");
|
|
5715
5769
|
await this.closeTurn(registration, draft, record7.timestamp);
|
|
5716
5770
|
const sessionRef = registration.host_session_ref;
|
|
5717
5771
|
const turnRef = claudeLocalTurnRef(registration.claude_session_id, record7.uuid);
|
|
@@ -5727,23 +5781,40 @@ var ClaudeTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
|
5727
5781
|
if (session !== null) observations.push({ observation_ref: observationRef("transcript_session", sessionRef, session.value), observation_kind: "session", observed_at: record7.timestamp, value: session.value });
|
|
5728
5782
|
draft.session_observed = true;
|
|
5729
5783
|
}
|
|
5730
|
-
await this.
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5784
|
+
await this.persistOpenedTurnContext(registration, draft, record7, inputRef, turnRef, promptRef, identity, observations);
|
|
5785
|
+
}
|
|
5786
|
+
async persistOpenedTurnContext(registration, draft, record7, inputRef, turnRef, promptRef, identity, observations) {
|
|
5787
|
+
const sessionRef = registration.host_session_ref;
|
|
5788
|
+
const expiresAt = this.expire(this.outboxRetentionMs);
|
|
5789
|
+
const custodyRegistered = await this.registerPrompt(sessionRef, turnRef, promptRef, record7.message.content, expiresAt);
|
|
5790
|
+
if (custodyRegistered) this.diagnostic("watcher_turn_observed");
|
|
5791
|
+
try {
|
|
5792
|
+
await this.store.putEnvelope({
|
|
5793
|
+
schema_version: "ceal.claude_observation_outbox.v1",
|
|
5794
|
+
identity,
|
|
5795
|
+
host_input_ref: inputRef,
|
|
5796
|
+
input_kind: "human",
|
|
5797
|
+
prompt_ref: promptRef,
|
|
5798
|
+
observations,
|
|
5799
|
+
origin_input: true,
|
|
5800
|
+
submission_ref: submissionRef("input", turnRef, inputRef, promptRef),
|
|
5801
|
+
created_at: record7.timestamp,
|
|
5802
|
+
expires_at: expiresAt
|
|
5803
|
+
});
|
|
5804
|
+
draft.enqueued += 1;
|
|
5805
|
+
await this.activateOpenedTurnContext(identity, promptRef, expiresAt);
|
|
5806
|
+
this.setOpenedTurn(draft, record7, inputRef, turnRef, promptRef);
|
|
5807
|
+
} catch (error) {
|
|
5808
|
+
if (custodyRegistered) await this.releasePrompt(sessionRef, { turn_key: turnRef, prompt_ref: promptRef }).catch(() => void 0);
|
|
5809
|
+
throw error;
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
async activateOpenedTurnContext(identity, promptRef, expiresAt) {
|
|
5744
5813
|
const selection = await this.store.readInvocationTurnSelection(identity.host_source_ref, identity.host_session_ref);
|
|
5745
5814
|
const turnGenerationRef = selection?.prompt_ref === promptRef ? selection.turn_generation_ref : void 0;
|
|
5746
|
-
await this.activateContext(identity, promptRef, turnGenerationRef);
|
|
5815
|
+
await this.activateContext(identity, promptRef, turnGenerationRef, expiresAt);
|
|
5816
|
+
}
|
|
5817
|
+
setOpenedTurn(draft, record7, inputRef, turnRef, promptRef) {
|
|
5747
5818
|
const folded = foldUnattributed(draft.unattributed);
|
|
5748
5819
|
draft.open_turn = {
|
|
5749
5820
|
turn_key: turnRef,
|
|
@@ -5762,9 +5833,18 @@ var ClaudeTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
|
5762
5833
|
draft.open_turn = void 0;
|
|
5763
5834
|
if (turn === void 0) return;
|
|
5764
5835
|
const digest3 = terminalDigest(turn);
|
|
5765
|
-
if (digest3
|
|
5766
|
-
|
|
5767
|
-
|
|
5836
|
+
if (digest3 !== turn.last_terminal_digest) {
|
|
5837
|
+
await this.putTerminalEnvelope(registration, turn, digest3, turn.message_ids.length === 0 ? at : void 0);
|
|
5838
|
+
draft.enqueued += 1;
|
|
5839
|
+
}
|
|
5840
|
+
await this.releasePrompt(registration.host_session_ref, turn);
|
|
5841
|
+
}
|
|
5842
|
+
async releaseOpenTurn(registration, draft) {
|
|
5843
|
+
if (draft.open_turn !== void 0) await this.releasePrompt(registration.host_session_ref, draft.open_turn);
|
|
5844
|
+
}
|
|
5845
|
+
async releasePrompt(sessionRef, turn) {
|
|
5846
|
+
if (this.producerInstallationRef === void 0 || this.releaseCurrentPrompt === void 0) return;
|
|
5847
|
+
await this.releaseCurrentPrompt({ producer_id: "claude", producer_installation_ref: this.producerInstallationRef, local_session_ref: sessionRef, local_turn_ref: turn.turn_key, prompt_ref: turn.prompt_ref });
|
|
5768
5848
|
}
|
|
5769
5849
|
accumulateUsage(draft, usage) {
|
|
5770
5850
|
const turn = draft.open_turn;
|
|
@@ -5798,6 +5878,8 @@ var ClaudeTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
|
5798
5878
|
if (isMissingTranscriptError(error) || isNotDirectory(error)) return 0;
|
|
5799
5879
|
throw new ClaudeHostError("payload_rejected", "Claude subagent transcript directory is unavailable.");
|
|
5800
5880
|
}
|
|
5881
|
+
const listed = new Set(names);
|
|
5882
|
+
for (const name of Object.keys(draft.subagents)) if (!listed.has(name)) delete draft.subagents[name];
|
|
5801
5883
|
if (names.length > CLAUDE_TRANSCRIPT_MAX_SUBAGENT_FILES) {
|
|
5802
5884
|
this.log(`Claude ignored subagent transcripts past its file bound: transcript_path=${JSON.stringify(registration.transcript_path)}, files=${names.length}.`);
|
|
5803
5885
|
names = names.slice(0, CLAUDE_TRANSCRIPT_MAX_SUBAGENT_FILES);
|
|
@@ -5912,27 +5994,33 @@ var ClaudeTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
|
5912
5994
|
updated_at: this.stamp()
|
|
5913
5995
|
});
|
|
5914
5996
|
}
|
|
5915
|
-
async registerPrompt(sessionRef, turnRef, promptRef, prompt) {
|
|
5916
|
-
if (this.producerInstallationRef === void 0 || this.registerCurrentPrompt === void 0) return;
|
|
5917
|
-
|
|
5918
|
-
|
|
5997
|
+
async registerPrompt(sessionRef, turnRef, promptRef, prompt, expiresAt) {
|
|
5998
|
+
if (this.producerInstallationRef === void 0 || this.registerCurrentPrompt === void 0) return false;
|
|
5999
|
+
const register = this.registerCurrentPrompt;
|
|
6000
|
+
const installationRef = this.producerInstallationRef;
|
|
6001
|
+
return registerHostCurrentPromptDiagnosed(
|
|
6002
|
+
() => register({
|
|
5919
6003
|
producer_id: "claude",
|
|
5920
|
-
producer_installation_ref:
|
|
6004
|
+
producer_installation_ref: installationRef,
|
|
5921
6005
|
local_session_ref: sessionRef,
|
|
5922
6006
|
local_turn_ref: turnRef,
|
|
5923
6007
|
prompt_ref: promptRef,
|
|
5924
6008
|
current_prompt: prompt,
|
|
5925
|
-
context_expires_at:
|
|
5926
|
-
})
|
|
5927
|
-
this.diagnostic(
|
|
5928
|
-
|
|
5929
|
-
this.diagnostic("current_prompt_registration_failed");
|
|
5930
|
-
throw error;
|
|
5931
|
-
}
|
|
6009
|
+
context_expires_at: expiresAt
|
|
6010
|
+
}),
|
|
6011
|
+
(event) => this.diagnostic(event)
|
|
6012
|
+
);
|
|
5932
6013
|
}
|
|
5933
|
-
async
|
|
6014
|
+
async releaseStoredPromptBestEffort(sessionRef, cursor) {
|
|
6015
|
+
await this.releaseOpenTurnBestEffort(sessionRef, cursor?.open_turn);
|
|
6016
|
+
}
|
|
6017
|
+
async releaseOpenTurnBestEffort(sessionRef, turn) {
|
|
6018
|
+
if (turn === void 0) return;
|
|
6019
|
+
await this.releasePrompt(sessionRef, turn).catch(() => void 0);
|
|
6020
|
+
}
|
|
6021
|
+
async activateContext(identity, promptRef, turnGenerationRef, expiresAt) {
|
|
5934
6022
|
try {
|
|
5935
|
-
await this.store.activateInvocationContext(identity, promptRef,
|
|
6023
|
+
await this.store.activateInvocationContext(identity, promptRef, expiresAt, void 0, turnGenerationRef);
|
|
5936
6024
|
this.diagnostic("activation_recorded");
|
|
5937
6025
|
} catch (error) {
|
|
5938
6026
|
this.diagnostic("activation_failed");
|
|
@@ -5940,6 +6028,7 @@ var ClaudeTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
|
5940
6028
|
}
|
|
5941
6029
|
}
|
|
5942
6030
|
async quarantineStored(cursor, device, inode, reason) {
|
|
6031
|
+
await this.releaseStoredPromptBestEffort(cursor.host_session_ref, cursor);
|
|
5943
6032
|
await this.store.writeTranscriptCursor({ ...cursor, device, inode, status: "quarantined", reason, updated_at: this.stamp() });
|
|
5944
6033
|
return null;
|
|
5945
6034
|
}
|
|
@@ -6056,21 +6145,12 @@ function isUserTurnLine(record7) {
|
|
|
6056
6145
|
if (identity === null) throw new ClaudeHostError("payload_rejected", "Claude transcript user line is malformed.");
|
|
6057
6146
|
return true;
|
|
6058
6147
|
}
|
|
6059
|
-
function claudeTranscriptScanFailureKind(error) {
|
|
6060
|
-
if (isSymlinkTranscript(error)) return "symlink";
|
|
6061
|
-
if (error instanceof ClaudeHostError) {
|
|
6062
|
-
if (error.diagnostic_kind !== void 0) return error.diagnostic_kind;
|
|
6063
|
-
if (error.code === "payload_rejected") return "payload_rejected";
|
|
6064
|
-
if (error.code === "storage") return "read_error";
|
|
6065
|
-
}
|
|
6066
|
-
const code2 = transcriptErrorCode(error);
|
|
6067
|
-
if (code2 === "EFBIG") return "size_budget";
|
|
6068
|
-
if (["EBADF", "EACCES", "EIO", "EMFILE", "ENFILE", "ENOENT", "ENOTDIR", "EPERM"].includes(code2 ?? "")) return "read_error";
|
|
6069
|
-
return "other";
|
|
6070
|
-
}
|
|
6071
6148
|
function isAssistantLine(record7) {
|
|
6072
6149
|
return isTranscriptRecord(record7) && record7.type === "assistant" && isTranscriptRecord(record7.message);
|
|
6073
6150
|
}
|
|
6151
|
+
function isAssistantTerminalLine(record7) {
|
|
6152
|
+
return isAssistantLine(record7) && (record7.message.stop_reason === "end_turn" || record7.message.stop_reason === "stop_sequence");
|
|
6153
|
+
}
|
|
6074
6154
|
function isAssistantUsageLine(record7) {
|
|
6075
6155
|
if (!isTranscriptRecord(record7)) return false;
|
|
6076
6156
|
if (!isAssistantLine(record7) || record7.message.usage === void 0) return false;
|
|
@@ -6098,12 +6178,24 @@ async function ownedClaudeTranscriptFile(handle) {
|
|
|
6098
6178
|
function isNotDirectory(error) {
|
|
6099
6179
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOTDIR";
|
|
6100
6180
|
}
|
|
6181
|
+
function claudeTranscriptScanCause(handle, error) {
|
|
6182
|
+
if (error instanceof ClaudeHostError) return error.diagnostic_kind ?? error.code;
|
|
6183
|
+
const code2 = transcriptErrorCode(error);
|
|
6184
|
+
if (code2 !== null) return `${handle === void 0 ? "open" : "scan"}:${code2}`;
|
|
6185
|
+
return `${handle === void 0 ? "open" : "scan"}:unknown`;
|
|
6186
|
+
}
|
|
6187
|
+
function claudeTranscriptScanFailureKind(error) {
|
|
6188
|
+
return transcriptScanFailureKind(error, (candidate) => {
|
|
6189
|
+
if (!(candidate instanceof ClaudeHostError)) return void 0;
|
|
6190
|
+
if (candidate.diagnostic_kind !== void 0) return candidate.diagnostic_kind;
|
|
6191
|
+
if (candidate.code === "payload_rejected") return "payload_rejected";
|
|
6192
|
+
if (candidate.code === "storage") return "read_error";
|
|
6193
|
+
return void 0;
|
|
6194
|
+
});
|
|
6195
|
+
}
|
|
6101
6196
|
function isSymlinkTranscript(error) {
|
|
6102
6197
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ELOOP";
|
|
6103
6198
|
}
|
|
6104
|
-
function transcriptErrorCode(error) {
|
|
6105
|
-
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : null;
|
|
6106
|
-
}
|
|
6107
6199
|
|
|
6108
6200
|
// host/claude/src/claude-producer-runtime.ts
|
|
6109
6201
|
async function drainClaudeProducerOnce(options) {
|
|
@@ -6112,9 +6204,10 @@ async function drainClaudeProducerOnce(options) {
|
|
|
6112
6204
|
outbox_dir: options.outbox_dir,
|
|
6113
6205
|
producer_installation_ref: options.producer_installation_ref,
|
|
6114
6206
|
register_current_prompt: (input) => promptPort.register(input),
|
|
6115
|
-
|
|
6207
|
+
release_current_prompt: (identity) => promptPort.release(identity),
|
|
6208
|
+
diagnostic: (event, failure_kind, cause) => writeHostServiceDiagnostic("claude", event, failure_kind, cause)
|
|
6116
6209
|
}).scanOnce().catch((error) => {
|
|
6117
|
-
writeHostServiceDiagnostic("claude", "watcher_scan_failed", claudeTranscriptScanFailureKind(error));
|
|
6210
|
+
writeHostServiceDiagnostic("claude", "watcher_scan_failed", claudeTranscriptScanFailureKind(error), claudeTranscriptScanCause(void 0, error));
|
|
6118
6211
|
throw error;
|
|
6119
6212
|
});
|
|
6120
6213
|
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 } });
|
|
@@ -7118,6 +7211,8 @@ function codexSubmissionReference(hostInputRef, event, ordinal) {
|
|
|
7118
7211
|
var CodexTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
7119
7212
|
store;
|
|
7120
7213
|
registerCurrentPrompt;
|
|
7214
|
+
releaseCurrentPrompt;
|
|
7215
|
+
diagnostic;
|
|
7121
7216
|
constructor(options) {
|
|
7122
7217
|
super(options, {
|
|
7123
7218
|
default_retention_ms: CODEX_DEFAULT_RETENTION_MS,
|
|
@@ -7126,6 +7221,8 @@ var CodexTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
|
7126
7221
|
});
|
|
7127
7222
|
this.store = new CodexOutboxStore(options.outbox_dir);
|
|
7128
7223
|
this.registerCurrentPrompt = options.register_current_prompt;
|
|
7224
|
+
this.releaseCurrentPrompt = options.release_current_prompt;
|
|
7225
|
+
this.diagnostic = options.diagnostic ?? (() => void 0);
|
|
7129
7226
|
}
|
|
7130
7227
|
transcriptPathOf(registration) {
|
|
7131
7228
|
return registration.transcript_path;
|
|
@@ -7141,17 +7238,22 @@ var CodexTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
|
7141
7238
|
if (complete === "empty" || complete === "partial") return { records: 0, enqueued: 0, quarantined: false };
|
|
7142
7239
|
return await this.consumeCompleteChunk(registration, handle, preflight.cursor, complete, device, inode);
|
|
7143
7240
|
}
|
|
7144
|
-
async scanFailed(handle, registration,
|
|
7241
|
+
async scanFailed(handle, registration, error) {
|
|
7242
|
+
this.diagnostic("watcher_scan_failed", codexTranscriptScanFailureKind(error));
|
|
7145
7243
|
await this.quarantineFailure(registration, handle);
|
|
7146
7244
|
return { records: 0, enqueued: 0, quarantined: true };
|
|
7147
7245
|
}
|
|
7148
7246
|
async retireMissing(registration) {
|
|
7247
|
+
await this.releaseStoredPromptBestEffort(registration.host_session_ref, await this.store.readTranscriptCursor(registration).catch(() => null));
|
|
7149
7248
|
return this.retireMissingTranscript(registration.transcript_path, "Codex", () => this.store.retireTranscriptRegistration(registration));
|
|
7150
7249
|
}
|
|
7151
7250
|
async preflight(registration, handle, size, device, inode) {
|
|
7152
7251
|
const existing = await this.store.readTranscriptCursor(registration);
|
|
7153
7252
|
let cursor = existing ?? codexTranscriptInitialCursor(registration, device, inode, this.now);
|
|
7154
|
-
if (cursor.status === "quarantined")
|
|
7253
|
+
if (cursor.status === "quarantined") {
|
|
7254
|
+
if (cursor.reason !== "transcript_malformed") await this.releaseStoredPromptBestEffort(registration.host_session_ref, cursor);
|
|
7255
|
+
return { cursor: null, result: { records: 0, enqueued: 0, quarantined: true } };
|
|
7256
|
+
}
|
|
7155
7257
|
const reason = this.cursorIdentityReason(cursor, registration.transcript_path, device, inode) ?? await this.cursorBoundsReason(handle, cursor, size);
|
|
7156
7258
|
if (reason === null) return { cursor, result: { records: 0, enqueued: 0, quarantined: false } };
|
|
7157
7259
|
await this.quarantine(registration, cursor, device, inode, reason);
|
|
@@ -7190,25 +7292,41 @@ var CodexTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
|
7190
7292
|
const stats = await handle.stat().catch(() => null);
|
|
7191
7293
|
if (stats === null) return;
|
|
7192
7294
|
const existing = await this.store.readTranscriptCursor(registration).catch(() => null);
|
|
7193
|
-
|
|
7295
|
+
const quarantined = {
|
|
7296
|
+
...existing ?? codexTranscriptInitialCursor(registration, String(stats.dev), String(stats.ino), this.now),
|
|
7297
|
+
transcript_path: registration.transcript_path,
|
|
7298
|
+
device: String(stats.dev),
|
|
7299
|
+
inode: String(stats.ino),
|
|
7300
|
+
status: "quarantined",
|
|
7301
|
+
reason: "transcript_malformed",
|
|
7302
|
+
updated_at: this.stamp()
|
|
7303
|
+
};
|
|
7304
|
+
await this.store.writeTranscriptCursor(quarantined);
|
|
7194
7305
|
}
|
|
7195
7306
|
async processRecord(registration, activeTask, record7) {
|
|
7196
|
-
if (record7.kind === "task_started")
|
|
7197
|
-
return { activeTask: { codex_turn_id: record7.turn_id, host_turn_ref: hostTurnReference(registration.host_source_ref, registration.host_session_ref, record7.turn_id), started_at: record7.timestamp }, enqueued: 0 };
|
|
7198
|
-
}
|
|
7307
|
+
if (record7.kind === "task_started") return this.processTaskStarted(registration, activeTask, record7);
|
|
7199
7308
|
if (record7.kind === "turn_context") return { ...activeTask === void 0 ? {} : { activeTask: record7.turn_id === activeTask.codex_turn_id ? { ...activeTask, model: record7.model } : activeTask }, enqueued: 0 };
|
|
7200
7309
|
if (record7.kind === "token_count") return { ...activeTask === void 0 ? {} : { activeTask: accumulateTokenCount(activeTask, record7) }, enqueued: 0 };
|
|
7201
7310
|
if (record7.kind === "user_text") return this.processUserInput(registration, activeTask, record7);
|
|
7202
7311
|
if (record7.kind === "task_complete" || record7.kind === "turn_aborted") return this.processTerminal(registration, activeTask, record7);
|
|
7203
7312
|
return { ...activeTask === void 0 ? {} : { activeTask }, enqueued: 0 };
|
|
7204
7313
|
}
|
|
7314
|
+
async processTaskStarted(registration, activeTask, record7) {
|
|
7315
|
+
if (activeTask !== void 0 && activeTask.codex_turn_id !== record7.turn_id && activeTask.prompt_ref !== void 0) await this.releasePrompt(registration.host_session_ref, activeTask, activeTask.prompt_ref);
|
|
7316
|
+
return { activeTask: { codex_turn_id: record7.turn_id, host_turn_ref: hostTurnReference(registration.host_source_ref, registration.host_session_ref, record7.turn_id), started_at: record7.timestamp }, enqueued: 0 };
|
|
7317
|
+
}
|
|
7205
7318
|
async processUserInput(registration, activeTask, record7) {
|
|
7206
|
-
if (activeTask === void 0 || activeTask.codex_turn_id !== record7.turn_id)
|
|
7319
|
+
if (activeTask === void 0 || activeTask.codex_turn_id !== record7.turn_id) {
|
|
7320
|
+
const adopted = await this.adoptHookCorrelatedTurn(registration, activeTask, record7);
|
|
7321
|
+
if (adopted === void 0) return { ...activeTask === void 0 ? {} : { activeTask }, enqueued: 0 };
|
|
7322
|
+
activeTask = adopted;
|
|
7323
|
+
}
|
|
7207
7324
|
const hostInputRef = hostInputReference(registration.host_source_ref, registration.host_session_ref, record7.turn_id, record7.message_id, record7.ordinal, record7.prompt);
|
|
7208
7325
|
const origin = activeTask.origin_host_input_ref === void 0;
|
|
7209
7326
|
const promptValue = promptObservationValue(record7.prompt);
|
|
7210
7327
|
const currentPromptRef = hostProducerPromptRef(promptValue.body_sha256);
|
|
7211
7328
|
const promptRef = activeTask.prompt_ref ?? currentPromptRef;
|
|
7329
|
+
const steeredPromptRef = await this.steeredPromptRef(registration, activeTask, origin, currentPromptRef);
|
|
7212
7330
|
const identity = { host_source_ref: registration.host_source_ref, host_session_ref: registration.host_session_ref, host_turn_ref: activeTask.host_turn_ref };
|
|
7213
7331
|
const observations = [{
|
|
7214
7332
|
observation_ref: observationRef2("transcript_user_text", hostInputRef, record7.prompt),
|
|
@@ -7234,14 +7352,88 @@ var CodexTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
|
7234
7352
|
created_at: record7.timestamp,
|
|
7235
7353
|
expires_at: this.expire(this.outboxRetentionMs)
|
|
7236
7354
|
};
|
|
7237
|
-
await this.
|
|
7238
|
-
await this.
|
|
7239
|
-
|
|
7355
|
+
const persisted = await this.persistUserInputTurn(registration, activeTask, record7, hostInputRef, origin, currentPromptRef, promptRef, identity, envelope);
|
|
7356
|
+
if (steeredPromptRef !== null) await this.activateTranscriptTurnContext(identity, steeredPromptRef, envelope.expires_at);
|
|
7357
|
+
return persisted;
|
|
7358
|
+
}
|
|
7359
|
+
/**
|
|
7360
|
+
* The steered prompt to re-activate under the running task, or null when
|
|
7361
|
+
* there is nothing to repair. A steered message joins the running turn
|
|
7362
|
+
* with new prompt bytes but no task_started. When the steer hook already
|
|
7363
|
+
* replaced the turn selection (a turn id the hook did not recognize as
|
|
7364
|
+
* the running one, or a hook that ran before any context was live), the
|
|
7365
|
+
* running task's context no longer matches and the next invocation would
|
|
7366
|
+
* wait for a boundary that never comes. When the hook kept the live
|
|
7367
|
+
* selection, the context still matches and this stays null.
|
|
7368
|
+
*/
|
|
7369
|
+
async steeredPromptRef(registration, activeTask, origin, currentPromptRef) {
|
|
7370
|
+
if (origin || activeTask.prompt_ref === void 0 || activeTask.prompt_ref === currentPromptRef) return null;
|
|
7371
|
+
const selection = await this.store.readInvocationTurnSelection(registration.host_source_ref, registration.host_session_ref).catch(() => null);
|
|
7372
|
+
return selection?.prompt_ref === currentPromptRef ? currentPromptRef : null;
|
|
7373
|
+
}
|
|
7374
|
+
/**
|
|
7375
|
+
* Adopt the live hooked turn when its transcript message arrives before
|
|
7376
|
+
* (or without) its task_started, as an interrupted turn's follow-up can.
|
|
7377
|
+
* The UserPromptSubmit hook names the exact hooked prompt, so only a
|
|
7378
|
+
* message carrying those bytes adopts; inherited history and other turns
|
|
7379
|
+
* stay ignored. A stale active task for another turn releases first, the
|
|
7380
|
+
* same as a task_started supersession.
|
|
7381
|
+
*/
|
|
7382
|
+
async adoptHookCorrelatedTurn(registration, activeTask, record7) {
|
|
7383
|
+
const currentPromptRef = hostProducerPromptRef(promptObservationValue(record7.prompt).body_sha256);
|
|
7384
|
+
const selection = await this.store.readInvocationTurnSelection(registration.host_source_ref, registration.host_session_ref);
|
|
7385
|
+
if (selection?.prompt_ref !== currentPromptRef) return void 0;
|
|
7386
|
+
if (activeTask !== void 0 && activeTask.codex_turn_id !== record7.turn_id && activeTask.prompt_ref !== void 0) {
|
|
7387
|
+
await this.releasePrompt(registration.host_session_ref, activeTask, activeTask.prompt_ref);
|
|
7388
|
+
}
|
|
7389
|
+
return {
|
|
7390
|
+
codex_turn_id: record7.turn_id,
|
|
7391
|
+
host_turn_ref: hostTurnReference(registration.host_source_ref, registration.host_session_ref, record7.turn_id),
|
|
7392
|
+
started_at: record7.timestamp
|
|
7393
|
+
};
|
|
7394
|
+
}
|
|
7395
|
+
async persistUserInputTurn(registration, activeTask, record7, hostInputRef, origin, currentPromptRef, promptRef, identity, envelope) {
|
|
7396
|
+
const custodyRegistered = await this.registerPrompt(origin, registration, activeTask, currentPromptRef, record7.prompt, envelope.expires_at);
|
|
7397
|
+
if (custodyRegistered) this.diagnostic("watcher_turn_observed");
|
|
7398
|
+
try {
|
|
7399
|
+
await this.store.put(envelope);
|
|
7400
|
+
if (origin) await this.activateTranscriptTurnContext(identity, promptRef, envelope.expires_at);
|
|
7401
|
+
} catch (error) {
|
|
7402
|
+
if (custodyRegistered) await this.releasePrompt(registration.host_session_ref, activeTask, currentPromptRef).catch(() => void 0);
|
|
7403
|
+
throw error;
|
|
7404
|
+
}
|
|
7240
7405
|
return { activeTask: { ...activeTask, origin_host_input_ref: activeTask.origin_host_input_ref ?? hostInputRef, prompt_ref: activeTask.prompt_ref ?? currentPromptRef }, enqueued: 1 };
|
|
7241
7406
|
}
|
|
7242
7407
|
async registerPrompt(origin, registration, activeTask, promptRef, prompt, expiresAt) {
|
|
7243
|
-
if (!origin || this.producerInstallationRef === void 0 || this.registerCurrentPrompt === void 0) return;
|
|
7244
|
-
|
|
7408
|
+
if (!origin || this.producerInstallationRef === void 0 || this.registerCurrentPrompt === void 0) return false;
|
|
7409
|
+
const register = this.registerCurrentPrompt;
|
|
7410
|
+
const installationRef = this.producerInstallationRef;
|
|
7411
|
+
return registerHostCurrentPromptDiagnosed(
|
|
7412
|
+
() => register({ producer_id: "codex", producer_installation_ref: installationRef, local_session_ref: registration.host_session_ref, local_turn_ref: activeTask.host_turn_ref, prompt_ref: promptRef, current_prompt: prompt, context_expires_at: expiresAt }),
|
|
7413
|
+
(event) => this.diagnostic(event)
|
|
7414
|
+
);
|
|
7415
|
+
}
|
|
7416
|
+
async activateTranscriptTurnContext(identity, promptRef, expiresAt) {
|
|
7417
|
+
try {
|
|
7418
|
+
const selection = await this.store.readInvocationTurnSelection(identity.host_source_ref, identity.host_session_ref);
|
|
7419
|
+
const turnGenerationRef = selection?.prompt_ref === promptRef ? selection.turn_generation_ref : void 0;
|
|
7420
|
+
await this.store.activateInvocationContext(identity, promptRef, expiresAt, turnGenerationRef);
|
|
7421
|
+
this.diagnostic("activation_recorded");
|
|
7422
|
+
} catch (error) {
|
|
7423
|
+
this.diagnostic("activation_failed");
|
|
7424
|
+
throw error;
|
|
7425
|
+
}
|
|
7426
|
+
}
|
|
7427
|
+
async releasePrompt(sessionRef, activeTask, promptRef) {
|
|
7428
|
+
if (this.producerInstallationRef === void 0 || this.releaseCurrentPrompt === void 0) return;
|
|
7429
|
+
await this.releaseCurrentPrompt({ producer_id: "codex", producer_installation_ref: this.producerInstallationRef, local_session_ref: sessionRef, local_turn_ref: activeTask.host_turn_ref, prompt_ref: promptRef });
|
|
7430
|
+
}
|
|
7431
|
+
async releaseStoredPromptBestEffort(sessionRef, cursor) {
|
|
7432
|
+
await this.releaseActiveTaskBestEffort(sessionRef, cursor?.active_task);
|
|
7433
|
+
}
|
|
7434
|
+
async releaseActiveTaskBestEffort(sessionRef, activeTask) {
|
|
7435
|
+
if (activeTask?.prompt_ref === void 0) return;
|
|
7436
|
+
await this.releasePrompt(sessionRef, activeTask, activeTask.prompt_ref).catch(() => void 0);
|
|
7245
7437
|
}
|
|
7246
7438
|
async processTerminal(registration, activeTask, record7) {
|
|
7247
7439
|
if (activeTask === void 0 || activeTask.codex_turn_id !== record7.turn_id || activeTask.origin_host_input_ref === void 0 || activeTask.prompt_ref === void 0) return { ...activeTask === void 0 ? {} : { activeTask }, enqueued: 0 };
|
|
@@ -7261,18 +7453,23 @@ var CodexTranscriptWatcher = class extends HostTranscriptWatcher {
|
|
|
7261
7453
|
created_at: record7.timestamp,
|
|
7262
7454
|
expires_at: this.expire(this.outboxRetentionMs)
|
|
7263
7455
|
});
|
|
7456
|
+
await this.releasePrompt(registration.host_session_ref, activeTask, activeTask.prompt_ref);
|
|
7264
7457
|
await this.store.clearInvocationContext(identity, this.now());
|
|
7265
7458
|
return { enqueued: 1 };
|
|
7266
7459
|
}
|
|
7267
7460
|
async quarantine(registration, cursor, device, inode, reason) {
|
|
7461
|
+
await this.releaseStoredPromptBestEffort(registration.host_session_ref, cursor);
|
|
7268
7462
|
const quarantined = { ...cursor, transcript_path: registration.transcript_path, device, inode, status: "quarantined", reason, updated_at: this.stamp() };
|
|
7269
7463
|
await this.store.writeTranscriptCursor(quarantined);
|
|
7270
7464
|
}
|
|
7271
7465
|
};
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7466
|
+
function codexTranscriptScanFailureKind(error) {
|
|
7467
|
+
return transcriptScanFailureKind(error, (candidate) => {
|
|
7468
|
+
if (!(candidate instanceof CodexHostAdapterError)) return void 0;
|
|
7469
|
+
if (candidate.code === "payload_rejected") return "payload_rejected";
|
|
7470
|
+
if (candidate.code === "storage") return "read_error";
|
|
7471
|
+
return void 0;
|
|
7472
|
+
});
|
|
7276
7473
|
}
|
|
7277
7474
|
function accumulateTokenCount(activeTask, record7) {
|
|
7278
7475
|
if (record7.usage_gap_reason !== void 0) return { ...activeTask, token_usage_gap_reason: record7.usage_gap_reason };
|
|
@@ -7360,7 +7557,16 @@ function assertRecordSequence(registration, cursor, records, lastOrdinal, record
|
|
|
7360
7557
|
// host/codex/src/codex-producer-runtime.ts
|
|
7361
7558
|
async function drainCodexProducerOnce(options) {
|
|
7362
7559
|
const promptPort = new HostRelayCurrentPromptPort(options.relay_endpoint, options.timeout_ms);
|
|
7363
|
-
const scan = await new CodexTranscriptWatcher({
|
|
7560
|
+
const scan = await new CodexTranscriptWatcher({
|
|
7561
|
+
outbox_dir: options.outbox_dir,
|
|
7562
|
+
producer_installation_ref: options.producer_installation_ref,
|
|
7563
|
+
register_current_prompt: (input) => promptPort.register(input),
|
|
7564
|
+
release_current_prompt: (identity) => promptPort.release(identity),
|
|
7565
|
+
diagnostic: (event, failure_kind) => writeHostServiceDiagnostic("codex", event, failure_kind)
|
|
7566
|
+
}).scanOnce().catch((error) => {
|
|
7567
|
+
writeHostServiceDiagnostic("codex", "watcher_scan_failed", codexTranscriptScanFailureKind(error));
|
|
7568
|
+
throw error;
|
|
7569
|
+
});
|
|
7364
7570
|
const producer = new CodexProducer({ 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 } });
|
|
7365
7571
|
return { scanned: scan.enqueued, ...await producer.drainOutbox() };
|
|
7366
7572
|
}
|
|
@@ -7972,7 +8178,12 @@ async function readSettingsForMutation(path) {
|
|
|
7972
8178
|
async function readSettings(path) {
|
|
7973
8179
|
const info = await lstat8(path);
|
|
7974
8180
|
if (!info.isFile() || info.isSymbolicLink()) throw new ClaudeHostError("configuration", "Claude settings path is not a regular file.");
|
|
7975
|
-
|
|
8181
|
+
let value;
|
|
8182
|
+
try {
|
|
8183
|
+
value = JSON.parse(await readFile3(path, "utf8"));
|
|
8184
|
+
} catch {
|
|
8185
|
+
throw new ClaudeHostError("configuration", "Claude settings file is not JSON.");
|
|
8186
|
+
}
|
|
7976
8187
|
if (!record6(value)) throw new ClaudeHostError("configuration", "Claude settings must be a JSON object.");
|
|
7977
8188
|
hooksRecord(value);
|
|
7978
8189
|
allowEntries(value);
|
|
@@ -8653,7 +8864,8 @@ function parseHostRuntimeControlRequest(value) {
|
|
|
8653
8864
|
if (value.schema_version !== HOST_RUNTIME_CONTROL_REQUEST_SCHEMA) throw invalid2("invalid_request_schema", [`expected:${HOST_RUNTIME_CONTROL_REQUEST_SCHEMA}`]);
|
|
8654
8865
|
if (!isOperation2(value.operation)) throw invalid2("invalid_request_schema");
|
|
8655
8866
|
assertPrefix(value.prefix);
|
|
8656
|
-
const
|
|
8867
|
+
const scan = value.operation === "installations_scan";
|
|
8868
|
+
const control = parseSelection(value.control_candidate_ref, value.control_components, scan);
|
|
8657
8869
|
const previous = parseSelection(value.previous_candidate_ref, value.previous_components, true);
|
|
8658
8870
|
const target = parseSelection(value.candidate_ref, value.components, true);
|
|
8659
8871
|
if (typeof value.transaction_ref !== "string") throw invalid2("invalid_transaction_ref");
|
|
@@ -8682,32 +8894,37 @@ async function runHostRuntimeControl(value, dependencies) {
|
|
|
8682
8894
|
try {
|
|
8683
8895
|
request2 = parseHostRuntimeControlRequest(value);
|
|
8684
8896
|
} catch (error) {
|
|
8685
|
-
return invalidResult(errorCode6(error));
|
|
8897
|
+
return invalidResult(errorCode6(error), errorReasons(error));
|
|
8686
8898
|
}
|
|
8687
|
-
const paths = resolveInstalledHostRuntimePaths(request2.prefix, dependencies.platform);
|
|
8688
|
-
const transition = requestTransition(request2);
|
|
8689
8899
|
try {
|
|
8690
|
-
|
|
8691
|
-
case "hooks_reconcile":
|
|
8692
|
-
await dependencies.hooks.reconcile(transition, paths);
|
|
8693
|
-
return successResult(request2, "hooks_reconciled");
|
|
8694
|
-
case "services_reconcile":
|
|
8695
|
-
await dependencies.services.reconcile(transition, paths);
|
|
8696
|
-
return successResult(request2, "services_reconciled");
|
|
8697
|
-
case "service_restart":
|
|
8698
|
-
await dependencies.services.restart(request2, paths);
|
|
8699
|
-
return successResult(request2, "restarted");
|
|
8700
|
-
case "readiness_check":
|
|
8701
|
-
await dependencies.readiness.check(transition.target, paths);
|
|
8702
|
-
return successResult(request2, request2.candidate_ref === "" ? "absent" : "ready");
|
|
8703
|
-
case "readiness_observe": {
|
|
8704
|
-
const observation3 = await dependencies.readiness.observe(transition.target, paths);
|
|
8705
|
-
const result = successResult(request2, request2.candidate_ref === "" ? "absent" : "ready");
|
|
8706
|
-
return observation3.recent_invocation_mints.length === 0 ? result : { ...result, recent_invocation_mints: observation3.recent_invocation_mints };
|
|
8707
|
-
}
|
|
8708
|
-
}
|
|
8900
|
+
return await dispatchControlOperation(request2, dependencies);
|
|
8709
8901
|
} catch (error) {
|
|
8710
|
-
return failureResult(request2, errorCode6(error), errorReasons(error));
|
|
8902
|
+
return failureResult(request2, errorCode6(error), errorReasons(error, request2.operation));
|
|
8903
|
+
}
|
|
8904
|
+
}
|
|
8905
|
+
async function dispatchControlOperation(request2, dependencies) {
|
|
8906
|
+
const paths = resolveInstalledHostRuntimePaths(request2.prefix, dependencies.platform);
|
|
8907
|
+
const transition = requestTransition(request2);
|
|
8908
|
+
switch (request2.operation) {
|
|
8909
|
+
case "hooks_reconcile":
|
|
8910
|
+
await dependencies.hooks.reconcile(transition, paths);
|
|
8911
|
+
return successResult(request2, "hooks_reconciled");
|
|
8912
|
+
case "services_reconcile":
|
|
8913
|
+
await dependencies.services.reconcile(transition, paths);
|
|
8914
|
+
return successResult(request2, "services_reconciled");
|
|
8915
|
+
case "service_restart":
|
|
8916
|
+
await dependencies.services.restart(request2, paths);
|
|
8917
|
+
return successResult(request2, "restarted");
|
|
8918
|
+
case "readiness_check":
|
|
8919
|
+
await dependencies.readiness.check(transition.target, paths);
|
|
8920
|
+
return successResult(request2, request2.candidate_ref === "" ? "absent" : "ready");
|
|
8921
|
+
case "readiness_observe": {
|
|
8922
|
+
const observation3 = await dependencies.readiness.observe(transition.target, paths);
|
|
8923
|
+
const result = successResult(request2, request2.candidate_ref === "" ? "absent" : "ready");
|
|
8924
|
+
return observation3.recent_invocation_mints.length === 0 ? result : { ...result, recent_invocation_mints: observation3.recent_invocation_mints };
|
|
8925
|
+
}
|
|
8926
|
+
case "installations_scan":
|
|
8927
|
+
return scanInstallations(request2, dependencies);
|
|
8711
8928
|
}
|
|
8712
8929
|
}
|
|
8713
8930
|
async function runHostRuntimeControlText(text, dependencies) {
|
|
@@ -8722,7 +8939,7 @@ async function runHostRuntimeControlText(text, dependencies) {
|
|
|
8722
8939
|
try {
|
|
8723
8940
|
rejectDuplicateJsonKeys2(decoded);
|
|
8724
8941
|
} catch (error) {
|
|
8725
|
-
return invalidResult(errorCode6(error));
|
|
8942
|
+
return invalidResult(errorCode6(error), errorReasons(error));
|
|
8726
8943
|
}
|
|
8727
8944
|
let value;
|
|
8728
8945
|
try {
|
|
@@ -8757,8 +8974,8 @@ async function runHostRuntimeControlCli(input, output, args, dependencies) {
|
|
|
8757
8974
|
chunks.push(bytes);
|
|
8758
8975
|
}
|
|
8759
8976
|
result = await runHostRuntimeControlText(Buffer2.concat(chunks), dependencies);
|
|
8760
|
-
} catch {
|
|
8761
|
-
result = invalidResult("input_unavailable");
|
|
8977
|
+
} catch (error) {
|
|
8978
|
+
result = invalidResult("input_unavailable", unknownErrorReasons(error));
|
|
8762
8979
|
}
|
|
8763
8980
|
}
|
|
8764
8981
|
output.write(serializeHostRuntimeControlResult(result));
|
|
@@ -8779,6 +8996,7 @@ function parseSelection(candidate, componentsValue, allowAbsent) {
|
|
|
8779
8996
|
}
|
|
8780
8997
|
function parseOperationFields(value, target) {
|
|
8781
8998
|
const component = value.component;
|
|
8999
|
+
if (value.operation === "installations_scan") return parseScanFields(value.transaction_ref, component, target);
|
|
8782
9000
|
if (value.operation === "readiness_observe") return parseObserveFields(value.transaction_ref, component, target);
|
|
8783
9001
|
if (typeof value.transaction_ref !== "string" || !HOST_RUNTIME_TRANSACTION_REF_PATTERN.test(value.transaction_ref)) throw invalid2("invalid_transaction_ref");
|
|
8784
9002
|
if (value.operation === "service_restart") return parseRestartComponent(component, target);
|
|
@@ -8812,7 +9030,12 @@ function hasExactKeys3(value, required, optional) {
|
|
|
8812
9030
|
return required.every((key) => Object.hasOwn(value, key)) && keys.every((key) => allowed.has(key));
|
|
8813
9031
|
}
|
|
8814
9032
|
function isOperation2(value) {
|
|
8815
|
-
return value === "hooks_reconcile" || value === "services_reconcile" || value === "service_restart" || value === "readiness_check" || value === "readiness_observe";
|
|
9033
|
+
return value === "hooks_reconcile" || value === "services_reconcile" || value === "service_restart" || value === "readiness_check" || value === "readiness_observe" || value === "installations_scan";
|
|
9034
|
+
}
|
|
9035
|
+
function parseScanFields(transactionRef, component, target) {
|
|
9036
|
+
if (transactionRef !== "") throw invalid2("invalid_operation_fields", [`transaction_ref_chars:${String(transactionRef).length}`]);
|
|
9037
|
+
if (component !== void 0 || target.candidate_ref !== "" || target.components.length !== 0) throw invalid2("invalid_operation_fields");
|
|
9038
|
+
return void 0;
|
|
8816
9039
|
}
|
|
8817
9040
|
function isRestartComponent(value) {
|
|
8818
9041
|
return isHostBundleComponent(value) && value !== "lifecycle";
|
|
@@ -8829,8 +9052,54 @@ function inputSizeReasons(observed) {
|
|
|
8829
9052
|
function errorCode6(error) {
|
|
8830
9053
|
return error instanceof HostRuntimeControlError && SAFE_CODE2.test(error.code) ? error.code : "control_failed";
|
|
8831
9054
|
}
|
|
8832
|
-
function errorReasons(error) {
|
|
8833
|
-
|
|
9055
|
+
function errorReasons(error, operation) {
|
|
9056
|
+
if (error instanceof HostRuntimeReasonedError) return error.reasons;
|
|
9057
|
+
const reasons = [...unknownErrorReasons(error)];
|
|
9058
|
+
if (operation !== void 0) reasons.unshift(`operation:${operation}`);
|
|
9059
|
+
return reasons.filter((reason) => SAFE_REASON.test(reason)).slice(0, MAX_UNKNOWN_REASONS);
|
|
9060
|
+
}
|
|
9061
|
+
var UNKNOWN_REASON_CODE_WORD = /^[A-Za-z0-9_.-]{1,64}$/u;
|
|
9062
|
+
var MAX_UNKNOWN_REASONS = 5;
|
|
9063
|
+
function unknownErrorReasons(error) {
|
|
9064
|
+
if (error instanceof HostRuntimeReasonedError) return error.reasons;
|
|
9065
|
+
const reasons = [...recordErrorReasons(error), ...factNameReasons(error)];
|
|
9066
|
+
if (error instanceof Error && error.name !== "Error" && UNKNOWN_REASON_CODE_WORD.test(error.name)) reasons.push(`error:${error.name}`);
|
|
9067
|
+
return reasons.filter((reason) => SAFE_REASON.test(reason)).slice(0, MAX_UNKNOWN_REASONS);
|
|
9068
|
+
}
|
|
9069
|
+
function recordErrorReasons(error) {
|
|
9070
|
+
if (!isCealJsonRecord(error)) return [];
|
|
9071
|
+
return [...wordReason(error.code, "code"), ...wordReason(error.syscall, "syscall"), ...pathReason(error.path), ...fieldReasons(error.reasons)];
|
|
9072
|
+
}
|
|
9073
|
+
function fieldReasons(value) {
|
|
9074
|
+
if (!Array.isArray(value)) return [];
|
|
9075
|
+
return value.filter((reason) => typeof reason === "string" && SAFE_REASON.test(reason));
|
|
9076
|
+
}
|
|
9077
|
+
function wordReason(value, head) {
|
|
9078
|
+
return typeof value === "string" && UNKNOWN_REASON_CODE_WORD.test(value) ? [`${head}:${value}`] : [];
|
|
9079
|
+
}
|
|
9080
|
+
function pathReason(value) {
|
|
9081
|
+
if (typeof value !== "string") return [];
|
|
9082
|
+
const leaf = value.split(/[/\\]/u).pop() ?? "";
|
|
9083
|
+
return wordReason(leaf === "" ? void 0 : leaf, "file");
|
|
9084
|
+
}
|
|
9085
|
+
var REFUSAL_FACT_NAME_PATTERN = /"([a-z][a-z0-9_]*)"=/gu;
|
|
9086
|
+
var MAX_FACT_REASONS = 2;
|
|
9087
|
+
function factNameReasons(error) {
|
|
9088
|
+
if (!isCealJsonRecord(error)) return [];
|
|
9089
|
+
if (typeof error.code !== "string" || typeof error.message !== "string") return [];
|
|
9090
|
+
const names = [];
|
|
9091
|
+
for (const match of error.message.matchAll(REFUSAL_FACT_NAME_PATTERN)) {
|
|
9092
|
+
const name = match[1];
|
|
9093
|
+
if (name === void 0) continue;
|
|
9094
|
+
const reason = `fact:${name}`;
|
|
9095
|
+
if (!names.includes(reason)) names.push(reason);
|
|
9096
|
+
if (names.length >= MAX_FACT_REASONS) break;
|
|
9097
|
+
}
|
|
9098
|
+
return names.filter((reason) => SAFE_REASON.test(reason));
|
|
9099
|
+
}
|
|
9100
|
+
async function scanInstallations(request2, dependencies) {
|
|
9101
|
+
const installations = await dependencies.services.listInstallations();
|
|
9102
|
+
return { ...successResult(request2, "scanned"), installations };
|
|
8834
9103
|
}
|
|
8835
9104
|
function successResult(request2, status) {
|
|
8836
9105
|
return { schema_version: HOST_RUNTIME_CONTROL_RESULT_SCHEMA, ok: true, code: "ok", status, candidate_ref: request2.candidate_ref, components: request2.components };
|
|
@@ -8978,14 +9247,14 @@ async function assertRegularRuntimeFile(path, executable) {
|
|
|
8978
9247
|
const module2 = basename3(path);
|
|
8979
9248
|
try {
|
|
8980
9249
|
info = await lstat10(path);
|
|
8981
|
-
} catch {
|
|
8982
|
-
throw runtimeUnavailable(`missing_module:${module2}
|
|
9250
|
+
} catch (error) {
|
|
9251
|
+
throw runtimeUnavailable(`missing_module:${module2}`, ...unknownErrorReasons(error));
|
|
8983
9252
|
}
|
|
8984
9253
|
if (!info.isFile() || info.isSymbolicLink()) throw runtimeUnavailable(`irregular_module:${module2}`);
|
|
8985
9254
|
if (executable && platform !== "win32" && (info.mode & 73) === 0) throw runtimeUnavailable(`non_executable_module:${module2}`);
|
|
8986
9255
|
}
|
|
8987
|
-
function runtimeUnavailable(
|
|
8988
|
-
return new HostRuntimeReasonedError("private_runtime_unavailable",
|
|
9256
|
+
function runtimeUnavailable(...reasons) {
|
|
9257
|
+
return new HostRuntimeReasonedError("private_runtime_unavailable", reasons);
|
|
8989
9258
|
}
|
|
8990
9259
|
function selectedProducers(selection) {
|
|
8991
9260
|
return selection.components.filter(isHostBundleProducer);
|
|
@@ -8994,7 +9263,7 @@ function selectedProducers(selection) {
|
|
|
8994
9263
|
// host/lifecycle/src/host-runtime-services-macos.ts
|
|
8995
9264
|
import { execFile } from "node:child_process";
|
|
8996
9265
|
import { constants as constants4 } from "node:fs";
|
|
8997
|
-
import { chmod as chmod2, lstat as lstat11, mkdir as mkdir8, open as open11, rename as rename7, unlink as unlink6 } from "node:fs/promises";
|
|
9266
|
+
import { chmod as chmod2, lstat as lstat11, mkdir as mkdir8, open as open11, readdir as readdir4, rename as rename7, unlink as unlink6 } from "node:fs/promises";
|
|
8998
9267
|
import { homedir as homedir5 } from "node:os";
|
|
8999
9268
|
import { basename as basename4, dirname as dirname9, join as join13 } from "node:path";
|
|
9000
9269
|
import { promisify } from "node:util";
|
|
@@ -9012,6 +9281,13 @@ function equalStringLists(left, right) {
|
|
|
9012
9281
|
var HOST_RUNTIME_SERVICE_COMPONENTS = Object.freeze(hostBundleComponentsForProducers(HOST_BUNDLE_PRODUCERS).filter((component) => component !== "lifecycle"));
|
|
9013
9282
|
var HOST_RUNTIME_SERVICE_COMMAND_TIMEOUT_MS = 2e4;
|
|
9014
9283
|
var HOST_RUNTIME_SERVICE_STATES = ["absent", "stopped", "running", "unknown"];
|
|
9284
|
+
function assembleHostRuntimeInstallations(groups) {
|
|
9285
|
+
return [...groups.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([installation_id, group]) => ({
|
|
9286
|
+
installation_id,
|
|
9287
|
+
...group.prefix === void 0 ? {} : { prefix: group.prefix },
|
|
9288
|
+
services: [...group.services].sort((left, right) => HOST_RUNTIME_SERVICE_COMPONENTS.indexOf(left.component) - HOST_RUNTIME_SERVICE_COMPONENTS.indexOf(right.component))
|
|
9289
|
+
}));
|
|
9290
|
+
}
|
|
9015
9291
|
var HostRuntimeServiceContractError = class extends Error {
|
|
9016
9292
|
code;
|
|
9017
9293
|
constructor(code2, message) {
|
|
@@ -9056,6 +9332,13 @@ function serviceReadinessReasons(component, wanted, found) {
|
|
|
9056
9332
|
if (found.command === void 0 || !sameCommand(found.command, wanted.command)) reasons.push(`${component}_service_command_drifted`);
|
|
9057
9333
|
return reasons;
|
|
9058
9334
|
}
|
|
9335
|
+
var HOST_PRIVATE_NODE_POSIX_SUFFIX = `/${HOST_BUNDLE_PRIVATE_RUNTIME_ROOT}/node/bin/node`;
|
|
9336
|
+
function hostPrefixFromPosixNodeExecutable(executable) {
|
|
9337
|
+
if (!executable.endsWith(HOST_PRIVATE_NODE_POSIX_SUFFIX)) return void 0;
|
|
9338
|
+
const prefix = executable.slice(0, -HOST_PRIVATE_NODE_POSIX_SUFFIX.length);
|
|
9339
|
+
if (!isAbsoluteNormalizedNonRootPath(prefix, "posix")) return void 0;
|
|
9340
|
+
return prefix;
|
|
9341
|
+
}
|
|
9059
9342
|
function exactSelection(value) {
|
|
9060
9343
|
if (!Array.isArray(value) || new Set(value).size !== value.length || value.some((component) => !HOST_BUNDLE_COMPONENTS.includes(component))) throw invalidComponents();
|
|
9061
9344
|
if (value.length === 0) return [];
|
|
@@ -9104,25 +9387,54 @@ var LAUNCHCTL = "/bin/launchctl";
|
|
|
9104
9387
|
var OWNER_MARKER = "ceal-host-runtime-service-v1";
|
|
9105
9388
|
var MacosHostRuntimeServiceError = class extends Error {
|
|
9106
9389
|
code;
|
|
9107
|
-
|
|
9390
|
+
/**
|
|
9391
|
+
* Bounded code-shaped words naming what the refusal held, so the control
|
|
9392
|
+
* boundary can repeat them without quoting private paths or file bodies.
|
|
9393
|
+
* The message keeps the rendered facts for local reading; only these
|
|
9394
|
+
* words travel in the control result.
|
|
9395
|
+
*/
|
|
9396
|
+
reasons;
|
|
9397
|
+
constructor(code2, message, reasons = []) {
|
|
9108
9398
|
super(message);
|
|
9109
9399
|
this.name = "MacosHostRuntimeServiceError";
|
|
9110
9400
|
this.code = code2;
|
|
9401
|
+
this.reasons = reasons.filter((reason) => typeof reason === "string");
|
|
9111
9402
|
}
|
|
9112
9403
|
};
|
|
9113
9404
|
function createMacosHostRuntimeServiceAdapter(options) {
|
|
9114
9405
|
const platform = options.platform ?? process.platform;
|
|
9115
|
-
if (platform !== "darwin") throw new MacosHostRuntimeServiceError("unsupported", `The launchd Host service adapter requires macOS (${refusalFact("platform", platform, "darwin")})
|
|
9406
|
+
if (platform !== "darwin") throw new MacosHostRuntimeServiceError("unsupported", `The launchd Host service adapter requires macOS (${refusalFact("platform", platform, "darwin")}).`, ["fact:platform"]);
|
|
9116
9407
|
const uid = options.uid ?? (typeof process.geteuid === "function" ? process.geteuid() : -1);
|
|
9117
|
-
if (!Number.isSafeInteger(uid) || uid < 0) throw new MacosHostRuntimeServiceError("invalid_owner", "The launchd user identity is unavailable.");
|
|
9408
|
+
if (!Number.isSafeInteger(uid) || uid < 0) throw new MacosHostRuntimeServiceError("invalid_owner", "The launchd user identity is unavailable.", ["fact:uid"]);
|
|
9118
9409
|
const directory = absoluteNormalized(options.launch_agents_directory ?? join13(homedir5(), "Library", "LaunchAgents"));
|
|
9119
9410
|
return new MacosHostRuntimeServiceAdapter(directory, absoluteNormalized(options.log_directory), uid, options.run_launchctl ?? runMacosLaunchctl, options.progress ?? (() => void 0));
|
|
9120
9411
|
}
|
|
9121
9412
|
function macosHostRuntimeServiceLabel(prefix, component) {
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9413
|
+
if (!HOST_RUNTIME_SERVICE_COMPONENTS.includes(component)) throw new MacosHostRuntimeServiceError("invalid_owner", "The launchd service component is invalid.", ["fact:component"]);
|
|
9414
|
+
return `ai.corca.ceal.host.${macosHostRuntimeServiceInstallation(prefix)}.${component}`;
|
|
9415
|
+
}
|
|
9416
|
+
function macosHostRuntimeServiceInstallation(prefix) {
|
|
9417
|
+
return createHash10("sha256").update(absoluteNormalized(prefix)).digest("hex").slice(0, 24);
|
|
9418
|
+
}
|
|
9419
|
+
var CEAL_LAUNCHD_LABEL_PATTERN = new RegExp(`^ai[.]corca[.]ceal[.]host[.]([0-9a-f]{24})[.](${HOST_RUNTIME_SERVICE_COMPONENTS.join("|")})$`, "u");
|
|
9420
|
+
function parseCealLaunchdLabel(label) {
|
|
9421
|
+
const match = CEAL_LAUNCHD_LABEL_PATTERN.exec(label);
|
|
9422
|
+
const installation = match?.[1];
|
|
9423
|
+
const component = match?.[2];
|
|
9424
|
+
if (installation === void 0 || !isServiceComponent(component)) return void 0;
|
|
9425
|
+
return { installation, component };
|
|
9426
|
+
}
|
|
9427
|
+
function isServiceComponent(value) {
|
|
9428
|
+
return value !== void 0 && HOST_RUNTIME_SERVICE_COMPONENTS.some((component) => component === value);
|
|
9429
|
+
}
|
|
9430
|
+
function launchctlListLabels(stdout) {
|
|
9431
|
+
const labels = [];
|
|
9432
|
+
for (const line of stdout.split("\n")) {
|
|
9433
|
+
const fields = line.split(/\s+/u).filter((field) => field !== "");
|
|
9434
|
+
const label = fields[fields.length - 1];
|
|
9435
|
+
if (label !== void 0) labels.push(label);
|
|
9436
|
+
}
|
|
9437
|
+
return labels;
|
|
9126
9438
|
}
|
|
9127
9439
|
async function runMacosLaunchctl(args) {
|
|
9128
9440
|
try {
|
|
@@ -9150,6 +9462,7 @@ var MacosHostRuntimeServiceAdapter = class {
|
|
|
9150
9462
|
assertSamePrefix(input.previous, input.target);
|
|
9151
9463
|
await ensureLaunchAgentsDirectory(this.directory, this.uid);
|
|
9152
9464
|
if (input.target.services.length > 0) await mkdir8(this.logDirectory, { recursive: true, mode: 448 });
|
|
9465
|
+
if (input.target.services.length > 0) await this.refuseForeignCealOrphans(input.target.prefix);
|
|
9153
9466
|
const targetComponents = new Set(input.target.services.map((service) => service.component));
|
|
9154
9467
|
for (const previous of [...input.previous.services].reverse()) {
|
|
9155
9468
|
if (!targetComponents.has(previous.component)) await this.remove(input.target.prefix, previous.component);
|
|
@@ -9163,17 +9476,17 @@ var MacosHostRuntimeServiceAdapter = class {
|
|
|
9163
9476
|
}
|
|
9164
9477
|
async restart(input) {
|
|
9165
9478
|
const directoryAvailable = await inspectLaunchAgentsDirectory(this.directory, this.uid);
|
|
9166
|
-
if (!directoryAvailable) throw new MacosHostRuntimeServiceError("service_not_installed", `The LaunchAgents directory is unavailable (${refusalFact("directory", this.directory, "an existing LaunchAgents directory")})
|
|
9479
|
+
if (!directoryAvailable) throw new MacosHostRuntimeServiceError("service_not_installed", `The LaunchAgents directory is unavailable (${refusalFact("directory", this.directory, "an existing LaunchAgents directory")}).`, [`service:${input.service.component}`, "fact:directory"]);
|
|
9167
9480
|
const owner = this.owner(input.target.prefix, input.service.component);
|
|
9168
9481
|
const current = await readManagedPlist(owner.path, owner.marker, this.uid);
|
|
9169
9482
|
const expected = launchAgentPlist(owner, input.service);
|
|
9170
9483
|
if (current !== expected) throw new MacosHostRuntimeServiceError("service_not_installed", `The selected launchd service definition is not installed exactly (${refusalFacts([
|
|
9171
9484
|
{ name: "installed_bytes", value: current === null ? -1 : Buffer.byteLength(current, "utf8"), expected: `${Buffer.byteLength(expected, "utf8")} bytes` },
|
|
9172
9485
|
{ name: "installed_digest", value: current === null ? "absent" : plistDigest(current), expected: plistDigest(expected) }
|
|
9173
|
-
])})
|
|
9486
|
+
])}).`, [`service:${input.service.component}`, "fact:installed_bytes", "fact:installed_digest"]);
|
|
9174
9487
|
const loaded = await this.launchctl(["print", owner.target]);
|
|
9175
|
-
if (loaded.exit_code !== 0) throw new MacosHostRuntimeServiceError("service_not_installed", "The selected launchd service is not loaded.");
|
|
9176
|
-
await this.requireLaunchctl(["kickstart", "-k", owner.target], "Unable to restart the selected launchd service.");
|
|
9488
|
+
if (loaded.exit_code !== 0) throw new MacosHostRuntimeServiceError("service_not_installed", "The selected launchd service is not loaded.", [`service:${input.service.component}`, "fact:loaded"]);
|
|
9489
|
+
await this.requireLaunchctl(["kickstart", "-k", owner.target], "Unable to restart the selected launchd service.", [`service:${input.service.component}`, "launchctl:kickstart"]);
|
|
9177
9490
|
}
|
|
9178
9491
|
async inspect(input) {
|
|
9179
9492
|
const directoryAvailable = await inspectLaunchAgentsDirectory(this.directory, this.uid);
|
|
@@ -9194,6 +9507,62 @@ var MacosHostRuntimeServiceAdapter = class {
|
|
|
9194
9507
|
async recentOutput(input) {
|
|
9195
9508
|
return readHostServiceLogTail(this.owner(input.target.prefix, input.component).log, input.lines).catch(() => []);
|
|
9196
9509
|
}
|
|
9510
|
+
async listInstallations() {
|
|
9511
|
+
const groups = /* @__PURE__ */ new Map();
|
|
9512
|
+
for (const label of await this.cealLabels()) {
|
|
9513
|
+
const record7 = await this.describeLabel(label);
|
|
9514
|
+
if (record7 === void 0) continue;
|
|
9515
|
+
let group = groups.get(record7.installation);
|
|
9516
|
+
if (group === void 0) {
|
|
9517
|
+
group = { services: [] };
|
|
9518
|
+
groups.set(record7.installation, group);
|
|
9519
|
+
}
|
|
9520
|
+
if (record7.prefix !== void 0 && group.prefix === void 0) group.prefix = record7.prefix;
|
|
9521
|
+
group.services.push(record7.service);
|
|
9522
|
+
}
|
|
9523
|
+
return assembleHostRuntimeInstallations(groups);
|
|
9524
|
+
}
|
|
9525
|
+
async cealLabels() {
|
|
9526
|
+
const labels = /* @__PURE__ */ new Set();
|
|
9527
|
+
let files;
|
|
9528
|
+
try {
|
|
9529
|
+
files = (await readdir4(this.directory)).sort();
|
|
9530
|
+
} catch {
|
|
9531
|
+
files = [];
|
|
9532
|
+
}
|
|
9533
|
+
for (const file of files) {
|
|
9534
|
+
if (!file.endsWith(".plist")) continue;
|
|
9535
|
+
const label = file.slice(0, -".plist".length);
|
|
9536
|
+
if (parseCealLaunchdLabel(label) !== void 0) labels.add(label);
|
|
9537
|
+
}
|
|
9538
|
+
const listed = await this.launchctl(["list"]);
|
|
9539
|
+
if (listed.exit_code === 0) for (const label of launchctlListLabels(listed.stdout)) {
|
|
9540
|
+
if (parseCealLaunchdLabel(label) !== void 0) labels.add(label);
|
|
9541
|
+
}
|
|
9542
|
+
return [...labels].sort();
|
|
9543
|
+
}
|
|
9544
|
+
async describeLabel(label) {
|
|
9545
|
+
const parsed = parseCealLaunchdLabel(label);
|
|
9546
|
+
if (parsed === void 0) return void 0;
|
|
9547
|
+
const owned = await this.ownedLabelPrefix(label);
|
|
9548
|
+
const loaded = await this.launchctl(["print", `gui/${this.uid}/${label}`]);
|
|
9549
|
+
if (owned.prefix === void 0 && loaded.exit_code !== 0) return void 0;
|
|
9550
|
+
return { installation: parsed.installation, ...owned.prefix === void 0 ? {} : { prefix: owned.prefix }, service: { component: parsed.component, state: loaded.exit_code !== 0 ? "stopped" : launchctlState(loaded.stdout), name: label } };
|
|
9551
|
+
}
|
|
9552
|
+
/**
|
|
9553
|
+
* The owning prefix an owned plist's program arguments prove. Anything
|
|
9554
|
+
* unowned, unreadable, or unparseable reads as absent, never as a guess.
|
|
9555
|
+
*/
|
|
9556
|
+
async ownedLabelPrefix(label) {
|
|
9557
|
+
let text;
|
|
9558
|
+
try {
|
|
9559
|
+
text = await readManagedPlist(join13(this.directory, `${label}.plist`), `<!-- ${OWNER_MARKER}:${label} -->`, this.uid);
|
|
9560
|
+
} catch {
|
|
9561
|
+
return {};
|
|
9562
|
+
}
|
|
9563
|
+
if (text === null) return {};
|
|
9564
|
+
return { prefix: ownedPlistPrefix(text) };
|
|
9565
|
+
}
|
|
9197
9566
|
async ensureReported(prefix, service) {
|
|
9198
9567
|
const started = Date.now();
|
|
9199
9568
|
this.progress(`service ${service.component}: starting`);
|
|
@@ -9205,30 +9574,51 @@ var MacosHostRuntimeServiceAdapter = class {
|
|
|
9205
9574
|
throw error;
|
|
9206
9575
|
}
|
|
9207
9576
|
}
|
|
9577
|
+
async refuseForeignCealOrphans(prefix) {
|
|
9578
|
+
const installation = macosHostRuntimeServiceInstallation(prefix);
|
|
9579
|
+
const listed = await this.launchctl(["list"]);
|
|
9580
|
+
if (listed.exit_code !== 0) return;
|
|
9581
|
+
for (const label of launchctlListLabels(listed.stdout)) {
|
|
9582
|
+
const parsed = parseCealLaunchdLabel(label);
|
|
9583
|
+
if (parsed === void 0 || parsed.installation === installation) continue;
|
|
9584
|
+
if (await this.hasOwnedDefinition(label)) continue;
|
|
9585
|
+
throw new MacosHostRuntimeServiceError("unsafe_plist", `A loaded launchd service "${label}" from another Host installation has no Ceal-owned definition. Remove the orphan with \`launchctl bootout gui/${this.uid}/${label}\`, then rerun the operation.`, [`service:${parsed.component}`, `label:${label}`, "launchctl:bootout"]);
|
|
9586
|
+
}
|
|
9587
|
+
}
|
|
9588
|
+
async hasOwnedDefinition(label) {
|
|
9589
|
+
try {
|
|
9590
|
+
return await readManagedPlist(join13(this.directory, `${label}.plist`), `<!-- ${OWNER_MARKER}:${label} -->`, this.uid) !== null;
|
|
9591
|
+
} catch (error) {
|
|
9592
|
+
if (error instanceof MacosHostRuntimeServiceError) return false;
|
|
9593
|
+
throw error;
|
|
9594
|
+
}
|
|
9595
|
+
}
|
|
9208
9596
|
async ensure(prefix, service) {
|
|
9209
9597
|
const owner = this.owner(prefix, service.component);
|
|
9210
9598
|
const expected = launchAgentPlist(owner, service);
|
|
9211
9599
|
const current = await readManagedPlist(owner.path, owner.marker, this.uid);
|
|
9212
|
-
|
|
9213
|
-
if (current === null && loaded.exit_code === 0)
|
|
9600
|
+
let loaded = await this.launchctl(["print", owner.target]);
|
|
9601
|
+
if (current === null && loaded.exit_code === 0) {
|
|
9602
|
+
await this.requireLaunchctl(["bootout", owner.target], "Unable to stop the orphaned launchd service.", [`service:${service.component}`, "launchctl:bootout"]);
|
|
9603
|
+
loaded = { exit_code: 1, stdout: "", stderr: "" };
|
|
9604
|
+
}
|
|
9214
9605
|
if (current === expected && loaded.exit_code === 0) {
|
|
9215
9606
|
if (launchctlState(loaded.stdout) === "running") return "already running";
|
|
9216
|
-
await this.requireLaunchctl(["kickstart", owner.target], "Unable to start the launchd service.");
|
|
9607
|
+
await this.requireLaunchctl(["kickstart", owner.target], "Unable to start the launchd service.", [`service:${service.component}`, "launchctl:kickstart"]);
|
|
9217
9608
|
return "started in place";
|
|
9218
9609
|
}
|
|
9219
|
-
if (loaded.exit_code === 0) await this.requireLaunchctl(["bootout", owner.target], "Unable to stop the existing launchd service.");
|
|
9610
|
+
if (loaded.exit_code === 0) await this.requireLaunchctl(["bootout", owner.target], "Unable to stop the existing launchd service.", [`service:${service.component}`, "launchctl:bootout"]);
|
|
9220
9611
|
if (current !== expected) await writeManagedPlist(owner.path, owner.marker, expected, this.uid);
|
|
9221
|
-
await this.requireLaunchctl(["enable", owner.target], "Unable to enable the launchd service.");
|
|
9222
|
-
await this.requireLaunchctl(["bootstrap", `gui/${this.uid}`, owner.path], "Unable to load the launchd service definition.");
|
|
9223
|
-
await this.requireLaunchctl(["kickstart", owner.target], "Unable to start the launchd service.");
|
|
9612
|
+
await this.requireLaunchctl(["enable", owner.target], "Unable to enable the launchd service.", [`service:${service.component}`, "launchctl:enable"]);
|
|
9613
|
+
await this.requireLaunchctl(["bootstrap", `gui/${this.uid}`, owner.path], "Unable to load the launchd service definition.", [`service:${service.component}`, "launchctl:bootstrap"]);
|
|
9614
|
+
await this.requireLaunchctl(["kickstart", owner.target], "Unable to start the launchd service.", [`service:${service.component}`, "launchctl:kickstart"]);
|
|
9224
9615
|
return "loaded and started";
|
|
9225
9616
|
}
|
|
9226
9617
|
async remove(prefix, component) {
|
|
9227
9618
|
const owner = this.owner(prefix, component);
|
|
9228
9619
|
const current = await readManagedPlist(owner.path, owner.marker, this.uid);
|
|
9229
9620
|
const loaded = await this.launchctl(["print", owner.target]);
|
|
9230
|
-
if (
|
|
9231
|
-
if (loaded.exit_code === 0) await this.requireLaunchctl(["bootout", owner.target], "Unable to stop the launchd service.");
|
|
9621
|
+
if (loaded.exit_code === 0) await this.requireLaunchctl(["bootout", owner.target], current === null ? "Unable to stop the orphaned launchd service." : "Unable to stop the launchd service.", [`service:${component}`, "launchctl:bootout"]);
|
|
9232
9622
|
if (current !== null) await unlink6(owner.path);
|
|
9233
9623
|
}
|
|
9234
9624
|
owner(prefix, component) {
|
|
@@ -9238,8 +9628,8 @@ var MacosHostRuntimeServiceAdapter = class {
|
|
|
9238
9628
|
async launchctl(args) {
|
|
9239
9629
|
return boundedLaunchctlResult(await this.runner(args));
|
|
9240
9630
|
}
|
|
9241
|
-
async requireLaunchctl(args, message) {
|
|
9242
|
-
if ((await this.launchctl(args)).exit_code !== 0) throw new MacosHostRuntimeServiceError("launchctl_failed", message);
|
|
9631
|
+
async requireLaunchctl(args, message, reasons = []) {
|
|
9632
|
+
if ((await this.launchctl(args)).exit_code !== 0) throw new MacosHostRuntimeServiceError("launchctl_failed", message, reasons);
|
|
9243
9633
|
}
|
|
9244
9634
|
};
|
|
9245
9635
|
function macosHostRuntimeServiceLogPath(logDirectory, component) {
|
|
@@ -9270,7 +9660,7 @@ function launchAgentPlist(owner, service) {
|
|
|
9270
9660
|
async function ensureLaunchAgentsDirectory(path, uid) {
|
|
9271
9661
|
await mkdir8(path, { recursive: true, mode: 448 });
|
|
9272
9662
|
const directoryAvailable = await inspectLaunchAgentsDirectory(path, uid);
|
|
9273
|
-
if (!directoryAvailable) throw new MacosHostRuntimeServiceError("invalid_owner", `The LaunchAgents directory is unavailable (${refusalFact("directory", path, "an existing LaunchAgents directory")})
|
|
9663
|
+
if (!directoryAvailable) throw new MacosHostRuntimeServiceError("invalid_owner", `The LaunchAgents directory is unavailable (${refusalFact("directory", path, "an existing LaunchAgents directory")}).`, ["fact:directory"]);
|
|
9274
9664
|
}
|
|
9275
9665
|
async function inspectLaunchAgentsDirectory(path, uid) {
|
|
9276
9666
|
let info;
|
|
@@ -9280,9 +9670,9 @@ async function inspectLaunchAgentsDirectory(path, uid) {
|
|
|
9280
9670
|
if (isNotFound5(error)) return false;
|
|
9281
9671
|
throw error;
|
|
9282
9672
|
}
|
|
9283
|
-
if (!info.isDirectory() || info.isSymbolicLink()) throw new MacosHostRuntimeServiceError("invalid_owner", `The LaunchAgents path is not a directory (${refusalFact("mode", `0o${info.mode.toString(8)}`, "a directory mode (0o40xxx)")})
|
|
9284
|
-
if (info.uid !== uid) throw new MacosHostRuntimeServiceError("invalid_owner", `The LaunchAgents directory is owned by another user (${refusalFact("uid", info.uid, `expected ${uid}`)})
|
|
9285
|
-
if ((info.mode & 18) !== 0) throw new MacosHostRuntimeServiceError("invalid_owner", `The LaunchAgents directory is group- or world-writable (${refusalFact("permission_bits", `0o${(info.mode & 511).toString(8)}`, "no group or other write bit")})
|
|
9673
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new MacosHostRuntimeServiceError("invalid_owner", `The LaunchAgents path is not a directory (${refusalFact("mode", `0o${info.mode.toString(8)}`, "a directory mode (0o40xxx)")}).`, ["fact:mode"]);
|
|
9674
|
+
if (info.uid !== uid) throw new MacosHostRuntimeServiceError("invalid_owner", `The LaunchAgents directory is owned by another user (${refusalFact("uid", info.uid, `expected ${uid}`)}).`, ["fact:uid"]);
|
|
9675
|
+
if ((info.mode & 18) !== 0) throw new MacosHostRuntimeServiceError("invalid_owner", `The LaunchAgents directory is group- or world-writable (${refusalFact("permission_bits", `0o${(info.mode & 511).toString(8)}`, "no group or other write bit")}).`, ["fact:permission_bits"]);
|
|
9286
9676
|
return true;
|
|
9287
9677
|
}
|
|
9288
9678
|
async function readManagedPlist(path, marker, uid) {
|
|
@@ -9290,12 +9680,12 @@ async function readManagedPlist(path, marker, uid) {
|
|
|
9290
9680
|
try {
|
|
9291
9681
|
handle = await open11(path, constants4.O_RDONLY | (constants4.O_NOFOLLOW ?? 0));
|
|
9292
9682
|
const info = await handle.stat();
|
|
9293
|
-
if (!info.isFile()) throw unsafePlist(refusalFact("mode", `0o${info.mode.toString(8)}`, "a regular-file mode (0o100xxx)"));
|
|
9294
|
-
if (info.uid !== uid) throw unsafePlist(refusalFact("uid", info.uid, `expected ${uid}`));
|
|
9295
|
-
if ((info.mode & 63) !== 0) throw unsafePlist(refusalFact("permission_bits", `0o${(info.mode & 511).toString(8)}`, "no group or other bits"));
|
|
9296
|
-
if (info.size > MAX_PLIST_BYTES) throw unsafePlist(refusalFact("size", info.size, `at most ${MAX_PLIST_BYTES} bytes`));
|
|
9683
|
+
if (!info.isFile()) throw unsafePlist(refusalFact("mode", `0o${info.mode.toString(8)}`, "a regular-file mode (0o100xxx)"), ["fact:mode"]);
|
|
9684
|
+
if (info.uid !== uid) throw unsafePlist(refusalFact("uid", info.uid, `expected ${uid}`), ["fact:uid"]);
|
|
9685
|
+
if ((info.mode & 63) !== 0) throw unsafePlist(refusalFact("permission_bits", `0o${(info.mode & 511).toString(8)}`, "no group or other bits"), ["fact:permission_bits"]);
|
|
9686
|
+
if (info.size > MAX_PLIST_BYTES) throw unsafePlist(refusalFact("size", info.size, `at most ${MAX_PLIST_BYTES} bytes`), ["fact:size"]);
|
|
9297
9687
|
const text = await handle.readFile("utf8");
|
|
9298
|
-
if (!text.includes(marker)) throw unsafePlist(refusalFact("owner_marker_present", false, "the managed owner marker"));
|
|
9688
|
+
if (!text.includes(marker)) throw unsafePlist(refusalFact("owner_marker_present", false, "the managed owner marker"), ["fact:owner_marker_present"]);
|
|
9299
9689
|
return text;
|
|
9300
9690
|
} catch (error) {
|
|
9301
9691
|
if (isNotFound5(error)) return null;
|
|
@@ -9330,9 +9720,9 @@ async function writeManagedPlist(path, marker, text, uid) {
|
|
|
9330
9720
|
}
|
|
9331
9721
|
}
|
|
9332
9722
|
function boundedLaunchctlResult(value) {
|
|
9333
|
-
if (!Number.isSafeInteger(value.exit_code) || value.exit_code < 0) throw new MacosHostRuntimeServiceError("launchctl_failed", `The launchctl exit status is invalid (${refusalFact("exit_code", value.exit_code, "a non-negative safe integer")})
|
|
9334
|
-
if (typeof value.stdout !== "string" || typeof value.stderr !== "string") throw new MacosHostRuntimeServiceError("launchctl_failed", `The launchctl result streams are invalid (${refusalFacts([{ name: "stdout_kind", value: typeof value.stdout, expected: "string" }, { name: "stderr_kind", value: typeof value.stderr, expected: "string" }])})
|
|
9335
|
-
if (Buffer.byteLength(value.stdout) > MAX_LAUNCHCTL_OUTPUT_BYTES || Buffer.byteLength(value.stderr) > MAX_LAUNCHCTL_OUTPUT_BYTES) throw new MacosHostRuntimeServiceError("launchctl_failed", `The launchctl result exceeds its bound (${refusalFacts([{ name: "stdout_bytes", value: Buffer.byteLength(value.stdout), expected: `at most ${MAX_LAUNCHCTL_OUTPUT_BYTES}` }, { name: "stderr_bytes", value: Buffer.byteLength(value.stderr), expected: `at most ${MAX_LAUNCHCTL_OUTPUT_BYTES}` }])})
|
|
9723
|
+
if (!Number.isSafeInteger(value.exit_code) || value.exit_code < 0) throw new MacosHostRuntimeServiceError("launchctl_failed", `The launchctl exit status is invalid (${refusalFact("exit_code", value.exit_code, "a non-negative safe integer")}).`, ["fact:exit_code"]);
|
|
9724
|
+
if (typeof value.stdout !== "string" || typeof value.stderr !== "string") throw new MacosHostRuntimeServiceError("launchctl_failed", `The launchctl result streams are invalid (${refusalFacts([{ name: "stdout_kind", value: typeof value.stdout, expected: "string" }, { name: "stderr_kind", value: typeof value.stderr, expected: "string" }])}).`, ["fact:stdout_kind", "fact:stderr_kind"]);
|
|
9725
|
+
if (Buffer.byteLength(value.stdout) > MAX_LAUNCHCTL_OUTPUT_BYTES || Buffer.byteLength(value.stderr) > MAX_LAUNCHCTL_OUTPUT_BYTES) throw new MacosHostRuntimeServiceError("launchctl_failed", `The launchctl result exceeds its bound (${refusalFacts([{ name: "stdout_bytes", value: Buffer.byteLength(value.stdout), expected: `at most ${MAX_LAUNCHCTL_OUTPUT_BYTES}` }, { name: "stderr_bytes", value: Buffer.byteLength(value.stderr), expected: `at most ${MAX_LAUNCHCTL_OUTPUT_BYTES}` }])}).`, ["fact:stdout_bytes", "fact:stderr_bytes"]);
|
|
9336
9726
|
return value;
|
|
9337
9727
|
}
|
|
9338
9728
|
function launchctlState(stdout) {
|
|
@@ -9340,12 +9730,25 @@ function launchctlState(stdout) {
|
|
|
9340
9730
|
if (HOST_RUNTIME_LAUNCHCTL_STOPPED_STATE_PATTERN.test(stdout)) return "stopped";
|
|
9341
9731
|
return "unknown";
|
|
9342
9732
|
}
|
|
9733
|
+
function ownedPlistPrefix(text) {
|
|
9734
|
+
const anchor = "<key>ProgramArguments</key><array>";
|
|
9735
|
+
const start = text.indexOf(anchor);
|
|
9736
|
+
if (start < 0) return void 0;
|
|
9737
|
+
const open13 = text.indexOf("<string>", start);
|
|
9738
|
+
if (open13 < 0) return void 0;
|
|
9739
|
+
const close = text.indexOf("</string>", open13);
|
|
9740
|
+
if (close < 0) return void 0;
|
|
9741
|
+
return hostPrefixFromPosixNodeExecutable(xmlUnescape(text.slice(open13 + "<string>".length, close)));
|
|
9742
|
+
}
|
|
9743
|
+
function xmlUnescape(value) {
|
|
9744
|
+
return value.replaceAll("<", "<").replaceAll(">", ">").replaceAll(""", '"').replaceAll("'", "'").replaceAll("&", "&");
|
|
9745
|
+
}
|
|
9343
9746
|
function absoluteNormalized(value) {
|
|
9344
|
-
if (!isAbsoluteNormalizedNonRootPath(value, "posix")) throw new MacosHostRuntimeServiceError("invalid_owner", "The launchd owner path must be absolute, normalized, and non-root.");
|
|
9747
|
+
if (!isAbsoluteNormalizedNonRootPath(value, "posix")) throw new MacosHostRuntimeServiceError("invalid_owner", "The launchd owner path must be absolute, normalized, and non-root.", ["fact:path"]);
|
|
9345
9748
|
return value;
|
|
9346
9749
|
}
|
|
9347
9750
|
function assertSamePrefix(previous, target) {
|
|
9348
|
-
if (previous.prefix !== target.prefix) throw new MacosHostRuntimeServiceError("invalid_owner", `Launchd reconciliation cannot cross Host prefixes (${refusalFacts([{ name: "previous_prefix", value: previous.prefix }, { name: "target_prefix", value: target.prefix }])})
|
|
9751
|
+
if (previous.prefix !== target.prefix) throw new MacosHostRuntimeServiceError("invalid_owner", `Launchd reconciliation cannot cross Host prefixes (${refusalFacts([{ name: "previous_prefix", value: previous.prefix }, { name: "target_prefix", value: target.prefix }])}).`, ["fact:previous_prefix", "fact:target_prefix"]);
|
|
9349
9752
|
}
|
|
9350
9753
|
function executionFailure(error) {
|
|
9351
9754
|
if (!isCealJsonRecord(error)) return { stdout: "", stderr: "" };
|
|
@@ -9363,8 +9766,8 @@ function xml(value) {
|
|
|
9363
9766
|
function isNotFound5(error) {
|
|
9364
9767
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
9365
9768
|
}
|
|
9366
|
-
function unsafePlist(detail) {
|
|
9367
|
-
return new MacosHostRuntimeServiceError("unsafe_plist", detail === void 0 ? "The LaunchAgent plist is unsafe or belongs to another owner." : `The LaunchAgent plist is unsafe or belongs to another owner (${detail})
|
|
9769
|
+
function unsafePlist(detail, reasons = []) {
|
|
9770
|
+
return new MacosHostRuntimeServiceError("unsafe_plist", detail === void 0 ? "The LaunchAgent plist is unsafe or belongs to another owner." : `The LaunchAgent plist is unsafe or belongs to another owner (${detail}).`, reasons);
|
|
9368
9771
|
}
|
|
9369
9772
|
|
|
9370
9773
|
// host/lifecycle/src/host-runtime-services-windows.ts
|
|
@@ -9420,6 +9823,13 @@ function createWindowsHostRuntimeServiceAdapter(input) {
|
|
|
9420
9823
|
// Task Scheduler keeps no service output, and the Windows Host has no live acceptance to design a log destination against.
|
|
9421
9824
|
async recentOutput() {
|
|
9422
9825
|
return [];
|
|
9826
|
+
},
|
|
9827
|
+
// Enumerating every Ceal task in the Scheduler folder would need a new
|
|
9828
|
+
// PowerShell wire operation, and that program cannot be verified without
|
|
9829
|
+
// a Windows runner: the scan honestly reports nothing here instead of
|
|
9830
|
+
// shipping unverified manager behavior.
|
|
9831
|
+
async listInstallations() {
|
|
9832
|
+
return [];
|
|
9423
9833
|
}
|
|
9424
9834
|
};
|
|
9425
9835
|
}
|
|
@@ -9502,9 +9912,9 @@ function isWireResult(value) {
|
|
|
9502
9912
|
return isRecord9(value) && value.schema_version === WINDOWS_SERVICE_RESULT_SCHEMA && value.ok === true && (value.observations === void 0 || Array.isArray(value.observations));
|
|
9503
9913
|
}
|
|
9504
9914
|
function isWireObservation(value) {
|
|
9505
|
-
return isRecord9(value) &&
|
|
9915
|
+
return isRecord9(value) && isServiceComponent2(value.component) && isServiceState(value.state) && typeof value.definition_matches === "boolean";
|
|
9506
9916
|
}
|
|
9507
|
-
function
|
|
9917
|
+
function isServiceComponent2(value) {
|
|
9508
9918
|
return HOST_RUNTIME_SERVICE_COMPONENTS.some((component) => component === value);
|
|
9509
9919
|
}
|
|
9510
9920
|
function isServiceState(value) {
|
|
@@ -9633,7 +10043,7 @@ var WINDOWS_TASK_SCHEDULER_PROGRAM = Buffer.from(TASK_SCHEDULER_SOURCE, "utf16le
|
|
|
9633
10043
|
|
|
9634
10044
|
// host/lifecycle/src/host-runtime-systemd.ts
|
|
9635
10045
|
import { createHash as createHash11, randomUUID } from "node:crypto";
|
|
9636
|
-
import { chmod as chmod3, lstat as lstat12, mkdir as mkdir9, open as open12, readFile as readFile5, readdir as
|
|
10046
|
+
import { chmod as chmod3, lstat as lstat12, mkdir as mkdir9, open as open12, readFile as readFile5, readdir as readdir5, rename as rename8, unlink as unlink7 } from "node:fs/promises";
|
|
9637
10047
|
import { join as join14 } from "node:path";
|
|
9638
10048
|
|
|
9639
10049
|
// host/lifecycle/src/systemd-unit-show.ts
|
|
@@ -9694,13 +10104,22 @@ function validSystemdUnitShow(shown) {
|
|
|
9694
10104
|
var DEFAULT_JOURNALCTL_PATH = "/usr/bin/journalctl";
|
|
9695
10105
|
var MAX_COMMAND_OUTPUT_BYTES = SYSTEMD_SHOW_MAX_OUTPUT_BYTES;
|
|
9696
10106
|
var UNIT_ID_PATTERN = /^[a-f0-9]{64}$/u;
|
|
10107
|
+
var CEAL_HOST_UNIT_PATTERN = /^ceal-host-([a-f0-9]{64})-(lifecycle|relay|codex|claude)\.service$/u;
|
|
9697
10108
|
var OWNER_HEADER = "# Managed by ceal-host. Do not edit.";
|
|
9698
10109
|
var LinuxUserSystemdAdapterError = class extends Error {
|
|
9699
10110
|
code;
|
|
9700
|
-
|
|
10111
|
+
/**
|
|
10112
|
+
* Bounded code-shaped words naming what the refusal held, so the control
|
|
10113
|
+
* boundary can repeat them without quoting unit bodies. The message keeps
|
|
10114
|
+
* the exact removal command for local reading; only these words travel in
|
|
10115
|
+
* the control result.
|
|
10116
|
+
*/
|
|
10117
|
+
reasons;
|
|
10118
|
+
constructor(code2, message, reasons = []) {
|
|
9701
10119
|
super(message);
|
|
9702
10120
|
this.name = "LinuxUserSystemdAdapterError";
|
|
9703
10121
|
this.code = code2;
|
|
10122
|
+
this.reasons = reasons.filter((reason) => typeof reason === "string");
|
|
9704
10123
|
}
|
|
9705
10124
|
};
|
|
9706
10125
|
function createLinuxUserSystemdAdapter(options) {
|
|
@@ -9714,6 +10133,7 @@ function createLinuxUserSystemdAdapter(options) {
|
|
|
9714
10133
|
const previousByComponent = serviceMap(previous);
|
|
9715
10134
|
const targetByComponent = serviceMap(target);
|
|
9716
10135
|
await ensurePrivateUnitDirectory(unitDirectory);
|
|
10136
|
+
if (target.services.length > 0) await refuseForeignCealUnits(unitDirectory, run, systemctl, identity);
|
|
9717
10137
|
const removableUnits = await ownedHostUnits(unitDirectory, identity);
|
|
9718
10138
|
for (const service of target.services) await replaceOwnedUnit(unitDirectory, identity, service);
|
|
9719
10139
|
for (const unit of removableUnits) {
|
|
@@ -9735,6 +10155,9 @@ function createLinuxUserSystemdAdapter(options) {
|
|
|
9735
10155
|
await invokeSystemctl(run, systemctl, ["--user", "restart", unitName(identity, service.component)]);
|
|
9736
10156
|
await requireExpectedReadback(run, systemctl, unitDirectory, identity, service, "running");
|
|
9737
10157
|
},
|
|
10158
|
+
async listInstallations() {
|
|
10159
|
+
return listSystemdInstallations(unitDirectory, run, systemctl);
|
|
10160
|
+
},
|
|
9738
10161
|
async inspect({ target }) {
|
|
9739
10162
|
const identity = prefixIdentity(target.prefix);
|
|
9740
10163
|
const expected = serviceMap(target);
|
|
@@ -9780,8 +10203,8 @@ function serviceMap(set) {
|
|
|
9780
10203
|
}
|
|
9781
10204
|
async function ownedHostUnits(directory, identity) {
|
|
9782
10205
|
const owned = [];
|
|
9783
|
-
for (const name of await
|
|
9784
|
-
const match =
|
|
10206
|
+
for (const name of await readdir5(directory)) {
|
|
10207
|
+
const match = CEAL_HOST_UNIT_PATTERN.exec(name);
|
|
9785
10208
|
if (match === null) continue;
|
|
9786
10209
|
const unitIdentity = match[1];
|
|
9787
10210
|
const component = match[2];
|
|
@@ -9790,6 +10213,132 @@ async function ownedHostUnits(directory, identity) {
|
|
|
9790
10213
|
}
|
|
9791
10214
|
return owned.sort((left, right) => unitName(left.identity, left.component).localeCompare(unitName(right.identity, right.component)));
|
|
9792
10215
|
}
|
|
10216
|
+
async function refuseForeignCealUnits(directory, run, systemctl, identity) {
|
|
10217
|
+
const names = (await readdir5(directory)).sort();
|
|
10218
|
+
for (const name of names) {
|
|
10219
|
+
const parsed = parseCealHostUnit(name);
|
|
10220
|
+
if (parsed === void 0 || parsed.identity === identity) continue;
|
|
10221
|
+
if (await provenOwnedUnitPresent(directory, parsed.identity, parsed.component)) refuseForeignCealUnit(name, parsed.identity, parsed.component);
|
|
10222
|
+
}
|
|
10223
|
+
for (const name of await listedCealUnitNames(run, systemctl)) {
|
|
10224
|
+
const parsed = parseCealHostUnit(name);
|
|
10225
|
+
if (parsed === void 0 || parsed.identity === identity) continue;
|
|
10226
|
+
refuseForeignCealUnit(name, parsed.identity, parsed.component);
|
|
10227
|
+
}
|
|
10228
|
+
}
|
|
10229
|
+
function parseCealHostUnit(name) {
|
|
10230
|
+
const match = CEAL_HOST_UNIT_PATTERN.exec(name);
|
|
10231
|
+
const identity = match?.[1];
|
|
10232
|
+
const component = match?.[2];
|
|
10233
|
+
if (identity === void 0 || !isHostRuntimeServiceComponent(component)) return void 0;
|
|
10234
|
+
return { identity, component };
|
|
10235
|
+
}
|
|
10236
|
+
async function provenOwnedUnitPresent(directory, identity, component) {
|
|
10237
|
+
try {
|
|
10238
|
+
return await ownedUnitPresent(unitPath(directory, identity, component), identity, component);
|
|
10239
|
+
} catch {
|
|
10240
|
+
return false;
|
|
10241
|
+
}
|
|
10242
|
+
}
|
|
10243
|
+
async function listSystemdInstallations(directory, run, systemctl) {
|
|
10244
|
+
const groups = /* @__PURE__ */ new Map();
|
|
10245
|
+
for (const name of await collectCealUnitNames(directory, run, systemctl)) {
|
|
10246
|
+
const record7 = await describeCealUnit(directory, run, systemctl, name);
|
|
10247
|
+
if (record7 === void 0) continue;
|
|
10248
|
+
let group = groups.get(record7.identity);
|
|
10249
|
+
if (group === void 0) {
|
|
10250
|
+
group = { services: [] };
|
|
10251
|
+
groups.set(record7.identity, group);
|
|
10252
|
+
}
|
|
10253
|
+
if (record7.prefix !== void 0 && group.prefix === void 0) group.prefix = record7.prefix;
|
|
10254
|
+
group.services.push(record7.service);
|
|
10255
|
+
}
|
|
10256
|
+
return assembleHostRuntimeInstallations(groups);
|
|
10257
|
+
}
|
|
10258
|
+
async function collectCealUnitNames(directory, run, systemctl) {
|
|
10259
|
+
let files;
|
|
10260
|
+
try {
|
|
10261
|
+
files = (await readdir5(directory)).sort();
|
|
10262
|
+
} catch {
|
|
10263
|
+
return [];
|
|
10264
|
+
}
|
|
10265
|
+
const names = /* @__PURE__ */ new Set();
|
|
10266
|
+
for (const file of files) if (parseCealHostUnit(file) !== void 0) names.add(file);
|
|
10267
|
+
for (const name of await listedCealUnitNames(run, systemctl)) names.add(name);
|
|
10268
|
+
return [...names].sort();
|
|
10269
|
+
}
|
|
10270
|
+
async function describeCealUnit(directory, run, systemctl, name) {
|
|
10271
|
+
const parsed = parseCealHostUnit(name);
|
|
10272
|
+
if (parsed === void 0) return void 0;
|
|
10273
|
+
const owned = await provenOwnedUnitPresent(directory, parsed.identity, parsed.component);
|
|
10274
|
+
const shown = await shownUnitState(run, systemctl, name);
|
|
10275
|
+
if (!owned && shown === void 0) return void 0;
|
|
10276
|
+
const state = unitReportState(owned, shown);
|
|
10277
|
+
if (state === void 0) return void 0;
|
|
10278
|
+
const prefix = owned ? await ownedUnitPrefix(directory, parsed.identity, parsed.component) : void 0;
|
|
10279
|
+
return { identity: parsed.identity, ...prefix === void 0 ? {} : { prefix }, service: { component: parsed.component, state, name } };
|
|
10280
|
+
}
|
|
10281
|
+
function unitReportState(owned, shown) {
|
|
10282
|
+
if (shown !== void 0) {
|
|
10283
|
+
if (shown.loadState === "not-found") return owned ? "stopped" : void 0;
|
|
10284
|
+
return shown.activeState === "active" ? "running" : "stopped";
|
|
10285
|
+
}
|
|
10286
|
+
return owned ? "stopped" : void 0;
|
|
10287
|
+
}
|
|
10288
|
+
async function shownUnitState(run, systemctl, name) {
|
|
10289
|
+
let result;
|
|
10290
|
+
try {
|
|
10291
|
+
result = await run(systemctl, ["--user", ...systemdShowArguments(name)], { timeout_ms: HOST_RUNTIME_SERVICE_COMMAND_TIMEOUT_MS, max_output_bytes: MAX_COMMAND_OUTPUT_BYTES });
|
|
10292
|
+
} catch {
|
|
10293
|
+
return void 0;
|
|
10294
|
+
}
|
|
10295
|
+
if (!validCommandResult(result) || result.exit_code !== 0) {
|
|
10296
|
+
const shown = systemdUnitShow(result.stdout);
|
|
10297
|
+
return shown !== null && shown.loadState === "not-found" ? shown : void 0;
|
|
10298
|
+
}
|
|
10299
|
+
return systemdUnitShow(result.stdout) ?? void 0;
|
|
10300
|
+
}
|
|
10301
|
+
async function ownedUnitPrefix(directory, identity, component) {
|
|
10302
|
+
let text;
|
|
10303
|
+
try {
|
|
10304
|
+
text = await readFile5(unitPath(directory, identity, component), "utf8");
|
|
10305
|
+
} catch {
|
|
10306
|
+
return void 0;
|
|
10307
|
+
}
|
|
10308
|
+
for (const line of text.split("\n")) {
|
|
10309
|
+
if (!line.startsWith("ExecStart=")) continue;
|
|
10310
|
+
const executable = firstExecToken(line.slice("ExecStart=".length));
|
|
10311
|
+
return executable === void 0 ? void 0 : hostPrefixFromPosixNodeExecutable(executable);
|
|
10312
|
+
}
|
|
10313
|
+
return void 0;
|
|
10314
|
+
}
|
|
10315
|
+
function firstExecToken(value) {
|
|
10316
|
+
const text = value.trimStart();
|
|
10317
|
+
if (text.startsWith('"')) {
|
|
10318
|
+
const body = /^"((?:[^"\\]|\\.)*)"/u.exec(text)?.[1];
|
|
10319
|
+
return body === void 0 ? void 0 : body.replaceAll(/\\(.)/gu, "$1");
|
|
10320
|
+
}
|
|
10321
|
+
const token = text.split(/\s+/u)[0];
|
|
10322
|
+
return token === void 0 || token === "" ? void 0 : token;
|
|
10323
|
+
}
|
|
10324
|
+
async function listedCealUnitNames(run, systemctl) {
|
|
10325
|
+
let result;
|
|
10326
|
+
try {
|
|
10327
|
+
result = await run(systemctl, ["--user", "list-units", "--all", "--no-legend", "--plain", "ceal-host-*"], { timeout_ms: HOST_RUNTIME_SERVICE_COMMAND_TIMEOUT_MS, max_output_bytes: MAX_COMMAND_OUTPUT_BYTES });
|
|
10328
|
+
} catch {
|
|
10329
|
+
return [];
|
|
10330
|
+
}
|
|
10331
|
+
if (!validCommandResult(result) || result.exit_code !== 0) return [];
|
|
10332
|
+
const names = [];
|
|
10333
|
+
for (const line of result.stdout.split("\n")) {
|
|
10334
|
+
const name = line.split(/\s+/u).filter((field) => field !== "")[0];
|
|
10335
|
+
if (name !== void 0 && CEAL_HOST_UNIT_PATTERN.test(name)) names.push(name);
|
|
10336
|
+
}
|
|
10337
|
+
return names;
|
|
10338
|
+
}
|
|
10339
|
+
function refuseForeignCealUnit(name, identity, component) {
|
|
10340
|
+
throw new LinuxUserSystemdAdapterError("foreign_unit", `A systemd user unit "${name}" belongs to another Host installation (installation ${identity}). This installation cannot retire another installation's units: remove each stale Ceal unit with \`systemctl --user disable --now ${name}\` (list them with \`systemctl --user list-units 'ceal-host-*' --all\`), then rerun the operation.`, [`service:${component}`, `installation:${identity}`, "systemctl:disable"]);
|
|
10341
|
+
}
|
|
9793
10342
|
function isHostRuntimeServiceComponent(value) {
|
|
9794
10343
|
return value !== void 0 && HOST_RUNTIME_SERVICE_COMPONENTS.some((component) => component === value);
|
|
9795
10344
|
}
|
|
@@ -10064,6 +10613,9 @@ var InstalledHostRuntimeServiceComposition = class {
|
|
|
10064
10613
|
if (service === void 0) throw configInvalid();
|
|
10065
10614
|
await this.platform.restart({ target, service });
|
|
10066
10615
|
}
|
|
10616
|
+
async listInstallations() {
|
|
10617
|
+
return this.platform.listInstallations();
|
|
10618
|
+
}
|
|
10067
10619
|
};
|
|
10068
10620
|
var InstalledHostRuntimeReadinessComposition = class {
|
|
10069
10621
|
owner;
|