@gajae-code/coding-agent 0.11.3 → 0.11.5
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/CHANGELOG.md +47 -0
- package/dist/types/cli/notify-cli.d.ts +5 -1
- package/dist/types/cli/worktree-cli.d.ts +8 -25
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/worktree.d.ts +7 -32
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
- package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/owner.d.ts +3 -1
- package/dist/types/internal-urls/local-protocol.d.ts +18 -0
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +8 -3
- package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-transcript-format.d.ts +77 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
- package/dist/types/modes/controllers/input-controller.d.ts +8 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
- package/dist/types/plan-mode/approved-plan.d.ts +1 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
- package/dist/types/sdk/broker/broker.d.ts +1 -0
- package/dist/types/sdk/broker/discovery.d.ts +10 -0
- package/dist/types/sdk/broker/ensure.d.ts +24 -1
- package/dist/types/sdk/broker/internal.d.ts +3 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -0
- package/dist/types/sdk/broker/state-version.d.ts +2 -1
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
- package/dist/types/sdk/bus/config-commands.d.ts +8 -0
- package/dist/types/sdk/bus/config.d.ts +22 -0
- package/dist/types/sdk/bus/index.d.ts +162 -2
- package/dist/types/sdk/bus/notification-service.d.ts +71 -8
- package/dist/types/sdk/bus/session-control.d.ts +11 -8
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
- package/dist/types/sdk/session.d.ts +16 -1
- package/dist/types/secrets/index.d.ts +4 -2
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +31 -2
- package/dist/types/session/artifacts.d.ts +30 -8
- package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
- package/dist/types/session/session-manager.d.ts +88 -18
- package/dist/types/session/session-storage.d.ts +27 -0
- package/dist/types/session/streaming-output.d.ts +4 -0
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/tool-discovery/tool-index.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +87 -12
- package/dist/types/tools/bash-skill-urls.d.ts +2 -2
- package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
- package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/path-utils.d.ts +9 -14
- package/dist/types/tools/render-utils.d.ts +1 -8
- package/dist/types/tools/resource-gc.d.ts +14 -0
- package/dist/types/utils/image-loading.d.ts +23 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/pasted-image-loading.d.ts +39 -0
- package/dist/types/utils/pasted-image-path.d.ts +21 -19
- package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
- package/dist/types/web/scrapers/types.d.ts +1 -5
- package/package.json +25 -9
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-threat-model-check.ts +60 -0
- package/src/cli/notify-cli.ts +40 -18
- package/src/cli/worktree-cli.ts +9 -287
- package/src/cli.ts +10 -9
- package/src/commands/gc.ts +11 -1
- package/src/commands/sdk.ts +10 -4
- package/src/commands/worktree.ts +10 -53
- package/src/config/model-profiles.ts +15 -15
- package/src/config/settings-schema.ts +17 -0
- package/src/daemon/runtime.ts +2 -2
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
- package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
- package/src/defaults/gjc/skills/team/SKILL.md +7 -9
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/index.ts +2 -2
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/plugins/marketplace/manager.ts +94 -10
- package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
- package/src/gjc-runtime/deep-interview-state.ts +127 -3
- package/src/gjc-runtime/gc-render.ts +19 -1
- package/src/gjc-runtime/gc-runtime.ts +68 -3
- package/src/gjc-runtime/launch-tmux.ts +61 -6
- package/src/gjc-runtime/managed-owner-admission.ts +352 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
- package/src/gjc-runtime/state-argv.ts +292 -0
- package/src/gjc-runtime/state-runtime.ts +80 -222
- package/src/gjc-runtime/team-runtime.ts +1 -1
- package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
- package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
- package/src/harness-control-plane/owner.ts +59 -3
- package/src/hindsight/client.ts +65 -2
- package/src/internal-urls/agent-protocol.ts +76 -5
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/internal-urls/local-protocol.ts +416 -18
- package/src/lsp/index.ts +4 -88
- package/src/main.ts +37 -13
- package/src/modes/acp/acp-agent.ts +8 -0
- package/src/modes/components/ansi-display-validator.ts +54 -0
- package/src/modes/components/custom-editor.ts +103 -32
- package/src/modes/components/custom-provider-wizard.ts +26 -1
- package/src/modes/components/notifications-settings-editor.ts +26 -2
- package/src/modes/components/session-observer-overlay.ts +40 -37
- package/src/modes/components/settings-selector.ts +3 -1
- package/src/modes/components/tool-transcript-format.ts +393 -0
- package/src/modes/components/transcript-viewer-overlay.ts +63 -1
- package/src/modes/controllers/goal-mode-controller.ts +20 -1
- package/src/modes/controllers/input-controller.ts +98 -37
- package/src/modes/controllers/plan-mode-controller.ts +2 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +71 -9
- package/src/modes/interactive-mode.ts +14 -2
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
- package/src/plan-mode/approved-plan.ts +3 -1
- package/src/prompts/agents/architect.md +1 -0
- package/src/prompts/agents/critic.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/prompts/tools/lsp.md +3 -3
- package/src/runtime-mcp/manager.ts +16 -1
- package/src/runtime-mcp/oauth-discovery.ts +116 -26
- package/src/sdk/broker/broker.ts +139 -32
- package/src/sdk/broker/discovery.ts +125 -0
- package/src/sdk/broker/ensure.ts +103 -19
- package/src/sdk/broker/internal.ts +10 -0
- package/src/sdk/broker/session-index.ts +309 -38
- package/src/sdk/broker/state-version.ts +5 -2
- package/src/sdk/bus/chat-daemon-cli.ts +72 -10
- package/src/sdk/bus/chat-daemon-control.ts +526 -130
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/config-commands.ts +22 -0
- package/src/sdk/bus/config.ts +37 -0
- package/src/sdk/bus/index.ts +384 -92
- package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
- package/src/sdk/bus/notification-service.ts +284 -62
- package/src/sdk/bus/recent-activity.ts +14 -5
- package/src/sdk/bus/session-control.ts +150 -44
- package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
- package/src/sdk/bus/telegram-daemon-control.ts +294 -76
- package/src/sdk/bus/telegram-daemon.ts +1842 -281
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +155 -27
- package/src/secrets/index.ts +53 -10
- package/src/secrets/obfuscator.ts +21 -18
- package/src/secrets/regex.ts +2 -0
- package/src/session/agent-session.ts +495 -253
- package/src/session/artifacts.ts +188 -23
- package/src/session/context-estimation.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +215 -39
- package/src/session/internal/managed-session-storage.ts +1152 -46
- package/src/session/session-manager.ts +1055 -148
- package/src/session/session-storage.ts +109 -11
- package/src/session/streaming-output.ts +6 -0
- package/src/task/executor.ts +71 -26
- package/src/task/index.ts +63 -20
- package/src/tool-discovery/tool-index.ts +18 -0
- package/src/tools/ask.ts +329 -57
- package/src/tools/bash-allowed-prefixes.ts +79 -29
- package/src/tools/bash-skill-urls.ts +14 -3
- package/src/tools/bash.ts +2 -2
- package/src/tools/browser/launch.ts +3 -1
- package/src/tools/browser/readable.ts +1 -1
- package/src/tools/browser/screenshot-format.ts +34 -0
- package/src/tools/browser/tab-protocol.ts +3 -0
- package/src/tools/browser/tab-supervisor.ts +125 -122
- package/src/tools/browser/tab-worker-entry.ts +33 -14
- package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
- package/src/tools/browser/tab-worker-smoke.ts +87 -0
- package/src/tools/browser/tab-worker.ts +7 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/path-utils.ts +42 -35
- package/src/tools/plan-mode-guard.ts +1 -0
- package/src/tools/read.ts +7 -1
- package/src/tools/render-utils.ts +1 -27
- package/src/tools/resource-gc.ts +123 -34
- package/src/tools/sqlite-reader.ts +3 -2
- package/src/tools/subagent.ts +2 -0
- package/src/utils/image-loading.ts +85 -34
- package/src/utils/image-resize.ts +16 -9
- package/src/utils/pasted-image-loading.ts +514 -0
- package/src/utils/pasted-image-path.ts +223 -98
- package/src/web/scrapers/html-to-markdown.ts +68 -0
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/types.ts +1 -68
- package/src/web/search/providers/openai-compatible.ts +76 -23
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
1
|
import * as crypto from "node:crypto";
|
|
3
2
|
import * as fs from "node:fs";
|
|
4
3
|
import * as path from "node:path";
|
|
@@ -6,6 +5,18 @@ import { VERSION } from "@gajae-code/utils/dirs";
|
|
|
6
5
|
import { safeStderrWrite } from "@gajae-code/utils/safe-stderr";
|
|
7
6
|
import type { Args } from "../cli/args";
|
|
8
7
|
import { readLinuxProcStartTimeSync } from "./linux-proc";
|
|
8
|
+
import {
|
|
9
|
+
MANAGED_OWNER_PREDECESSOR_GENERATION_ENV,
|
|
10
|
+
MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV,
|
|
11
|
+
MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV,
|
|
12
|
+
MANAGED_OWNER_PREDECESSOR_TOKEN_ENV,
|
|
13
|
+
MANAGED_OWNER_TRANSCRIPT_PATH_ENV,
|
|
14
|
+
} from "./managed-owner-admission";
|
|
15
|
+
import {
|
|
16
|
+
MANAGED_OWNER_INCARNATION_ENV,
|
|
17
|
+
MANAGED_OWNER_RUN_ID_ENV,
|
|
18
|
+
MANAGED_OWNER_SUPERVISOR_ARG,
|
|
19
|
+
} from "./managed-owner-supervisor";
|
|
9
20
|
import { tmuxRuntimeSessionPath } from "./session-layout";
|
|
10
21
|
import {
|
|
11
22
|
GJC_COORDINATOR_SESSION_BRANCH_ENV,
|
|
@@ -42,6 +53,7 @@ import {
|
|
|
42
53
|
type OwnerIsolationProbeSync,
|
|
43
54
|
planTmuxOwnerIsolationSync,
|
|
44
55
|
replaceOwnerGenerationSync,
|
|
56
|
+
resolveManagedOwnerPredecessorSync,
|
|
45
57
|
type TmuxServerProof,
|
|
46
58
|
} from "./tmux-owner-isolation";
|
|
47
59
|
import {
|
|
@@ -152,6 +164,9 @@ export interface TmuxLaunchPlan {
|
|
|
152
164
|
sessionStateFile?: string | null;
|
|
153
165
|
/** Immutable Linux-managed owner provenance, assigned immediately before creation. */
|
|
154
166
|
ownerGeneration?: string;
|
|
167
|
+
/** Immutable run and endpoint identities bound into the supervised command. */
|
|
168
|
+
ownerRunId?: string;
|
|
169
|
+
ownerIncarnation?: string;
|
|
155
170
|
/** Generation state captured before owner-isolation planning; required for publication CAS. */
|
|
156
171
|
ownerGenerationBaseline?: import("./tmux-owner-isolation").OwnerGenerationBaseline;
|
|
157
172
|
/** Native tmux session identity emitted atomically by `new-session -P -F`. */
|
|
@@ -334,6 +349,7 @@ interface CommandResolutionContext {
|
|
|
334
349
|
extraEnv?: Record<string, string>;
|
|
335
350
|
tmuxExitMarkerPath?: string;
|
|
336
351
|
platform?: NodeJS.Platform;
|
|
352
|
+
managedOwnerSupervisor?: boolean;
|
|
337
353
|
}
|
|
338
354
|
|
|
339
355
|
function parseLaunchPolicy(env: NodeJS.ProcessEnv): LaunchPolicy {
|
|
@@ -553,8 +569,15 @@ function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[])
|
|
|
553
569
|
tmuxExitMarkerPath: context.tmuxExitMarkerPath,
|
|
554
570
|
});
|
|
555
571
|
const command = resolveCurrentGjcCommand(context);
|
|
556
|
-
const
|
|
557
|
-
const
|
|
572
|
+
const childArgs = stripRootTmuxFlag(rawArgs);
|
|
573
|
+
const supervisorEnv: Record<string, string> = context.managedOwnerSupervisor
|
|
574
|
+
? { GJC_MANAGED_OWNER_COMMAND_JSON: JSON.stringify([...command, ...childArgs]) }
|
|
575
|
+
: {};
|
|
576
|
+
const invocationArgs = context.managedOwnerSupervisor
|
|
577
|
+
? [...command, MANAGED_OWNER_SUPERVISOR_ARG]
|
|
578
|
+
: [...command, ...childArgs];
|
|
579
|
+
const quoted = invocationArgs.map(shellQuote).join(" ");
|
|
580
|
+
const invocation = `env ${GJC_TMUX_LAUNCHED_ENV}=1${buildEnvAssignments({ ...context.extraEnv, ...supervisorEnv })} ${quoted}`;
|
|
558
581
|
if (!context.tmuxExitMarkerPath) return `exec ${invocation}`;
|
|
559
582
|
return `${buildPosixTmuxExitMarkerPrefix(context.tmuxExitMarkerPath)}; ${invocation}; exit $?`;
|
|
560
583
|
}
|
|
@@ -1026,15 +1049,29 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
1026
1049
|
};
|
|
1027
1050
|
}
|
|
1028
1051
|
|
|
1052
|
+
function trustedReplacementAuthority(
|
|
1053
|
+
stateDir: string,
|
|
1054
|
+
sessionId: string,
|
|
1055
|
+
baseline: ReturnType<typeof captureOwnerGenerationBaselineSync>,
|
|
1056
|
+
): ReturnType<typeof resolveManagedOwnerPredecessorSync> {
|
|
1057
|
+
return resolveManagedOwnerPredecessorSync(stateDir, sessionId, baseline);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1029
1060
|
function prepareManagedOwnerLifecycle(plan: TmuxLaunchPlan, context: TmuxLaunchContext): void {
|
|
1030
1061
|
if (plan.ownerGeneration) return;
|
|
1031
1062
|
const sessionId = plan.sessionId ?? plan.sessionName;
|
|
1032
1063
|
const stateDir = path.dirname(plan.sessionStateFile ?? path.join(plan.cwd, ".gjc", "runtime"));
|
|
1064
|
+
const baseline = captureOwnerGenerationBaselineSync(stateDir, sessionId);
|
|
1065
|
+
const replacement = trustedReplacementAuthority(stateDir, sessionId, baseline);
|
|
1033
1066
|
const generation = crypto.randomUUID();
|
|
1034
|
-
|
|
1067
|
+
const runId = crypto.randomUUID();
|
|
1068
|
+
const incarnation = crypto.randomUUID();
|
|
1069
|
+
plan.ownerGenerationBaseline = baseline;
|
|
1070
|
+
// Stage immutable identity in the child command. It becomes current only after
|
|
1035
1071
|
// immutable creation proof and ownership tagging complete.
|
|
1036
|
-
|
|
1037
1072
|
plan.ownerGeneration = generation;
|
|
1073
|
+
plan.ownerRunId = runId;
|
|
1074
|
+
plan.ownerIncarnation = incarnation;
|
|
1038
1075
|
const innerCommand = buildInnerCommand(
|
|
1039
1076
|
{
|
|
1040
1077
|
cwd: plan.cwd,
|
|
@@ -1047,11 +1084,24 @@ function prepareManagedOwnerLifecycle(plan: TmuxLaunchPlan, context: TmuxLaunchC
|
|
|
1047
1084
|
[GJC_TMUX_OWNER_GENERATION_ENV]: generation,
|
|
1048
1085
|
[GJC_TMUX_OWNER_STATE_DIR_ENV]: stateDir,
|
|
1049
1086
|
[GJC_TMUX_OWNER_SERVER_KEY_ENV]: plan.tmuxCommand,
|
|
1087
|
+
[MANAGED_OWNER_RUN_ID_ENV]: runId,
|
|
1088
|
+
[MANAGED_OWNER_INCARNATION_ENV]: incarnation,
|
|
1089
|
+
...(replacement
|
|
1090
|
+
? {
|
|
1091
|
+
[MANAGED_OWNER_PREDECESSOR_TOKEN_ENV]: replacement.predecessorToken,
|
|
1092
|
+
[MANAGED_OWNER_PREDECESSOR_GENERATION_ENV]: replacement.generation,
|
|
1093
|
+
[MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV]: replacement.runId,
|
|
1094
|
+
[MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV]: replacement.incarnation,
|
|
1095
|
+
[MANAGED_OWNER_TRANSCRIPT_PATH_ENV]:
|
|
1096
|
+
context.env?.GJC_SESSION_FILE ?? process.env.GJC_SESSION_FILE ?? "",
|
|
1097
|
+
}
|
|
1098
|
+
: {}),
|
|
1050
1099
|
},
|
|
1051
1100
|
// Linux managed owner close signals the pane PID. Do not place the exit-marker shell
|
|
1052
1101
|
// in front of it; `buildInnerCommand` therefore execs the GJC owner directly.
|
|
1053
1102
|
tmuxExitMarkerPath: plan.platform === "linux" ? undefined : tmuxExitMarkerPath(plan.sessionStateFile ?? ""),
|
|
1054
1103
|
platform: plan.platform,
|
|
1104
|
+
managedOwnerSupervisor: plan.platform === "linux",
|
|
1055
1105
|
},
|
|
1056
1106
|
context.rawArgs,
|
|
1057
1107
|
);
|
|
@@ -1182,7 +1232,7 @@ function createIsolatedTmuxSession(
|
|
|
1182
1232
|
): TmuxSpawnResult {
|
|
1183
1233
|
const sessionId = plan.sessionId ?? plan.sessionName;
|
|
1184
1234
|
const stateDir = path.dirname(plan.sessionStateFile ?? path.join(plan.cwd, ".gjc", "runtime"));
|
|
1185
|
-
const baseline = captureOwnerGenerationBaselineSync(stateDir, sessionId);
|
|
1235
|
+
const baseline = plan.ownerGenerationBaseline ?? captureOwnerGenerationBaselineSync(stateDir, sessionId);
|
|
1186
1236
|
plan.ownerGenerationBaseline = baseline;
|
|
1187
1237
|
const ownerPlan = planTmuxOwnerIsolationSync(
|
|
1188
1238
|
{
|
|
@@ -1572,6 +1622,11 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1572
1622
|
return true;
|
|
1573
1623
|
}
|
|
1574
1624
|
try {
|
|
1625
|
+
resolveManagedOwnerPredecessorSync(
|
|
1626
|
+
path.dirname(plan.sessionStateFile!),
|
|
1627
|
+
plan.sessionId!,
|
|
1628
|
+
plan.ownerGenerationBaseline!,
|
|
1629
|
+
);
|
|
1575
1630
|
replaceOwnerGenerationSync(
|
|
1576
1631
|
path.dirname(plan.sessionStateFile!),
|
|
1577
1632
|
plan.sessionId!,
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import * as crypto from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { openRecoveryFsRoot } from "@gajae-code/natives";
|
|
6
|
+
import {
|
|
7
|
+
MANAGED_OWNER_CHILD_TOKEN_ENV,
|
|
8
|
+
MANAGED_OWNER_GENERATION_ENV,
|
|
9
|
+
MANAGED_OWNER_INCARNATION_ENV,
|
|
10
|
+
MANAGED_OWNER_RUN_ID_ENV,
|
|
11
|
+
MANAGED_OWNER_SESSION_ID_ENV,
|
|
12
|
+
MANAGED_OWNER_STATE_DIR_ENV,
|
|
13
|
+
type ManagedOwnerBinding,
|
|
14
|
+
type ManagedOwnerSigabrtReceipt,
|
|
15
|
+
} from "./managed-owner-supervisor";
|
|
16
|
+
import { assertSafePathComponent } from "./session-layout";
|
|
17
|
+
import { lifecyclePaths } from "./tmux-owner-isolation";
|
|
18
|
+
import {
|
|
19
|
+
persistUltragoalRecoveryDecision,
|
|
20
|
+
planUltragoalOwnerLossRecovery,
|
|
21
|
+
type UltragoalRecoveryDecision,
|
|
22
|
+
} from "./ultragoal-owner-loss-recovery";
|
|
23
|
+
|
|
24
|
+
export const MANAGED_OWNER_PREDECESSOR_TOKEN_ENV = "GJC_MANAGED_OWNER_PREDECESSOR_TOKEN";
|
|
25
|
+
export const MANAGED_OWNER_PREDECESSOR_GENERATION_ENV = "GJC_MANAGED_OWNER_PREDECESSOR_GENERATION";
|
|
26
|
+
export const MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV = "GJC_MANAGED_OWNER_PREDECESSOR_RUN_ID";
|
|
27
|
+
export const MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV = "GJC_MANAGED_OWNER_PREDECESSOR_INCARNATION";
|
|
28
|
+
export const MANAGED_OWNER_TRANSCRIPT_PATH_ENV = "GJC_MANAGED_OWNER_TRANSCRIPT_PATH";
|
|
29
|
+
|
|
30
|
+
export interface ManagedOwnerRecoveryContext {
|
|
31
|
+
root: string;
|
|
32
|
+
binding: ManagedOwnerBinding;
|
|
33
|
+
receipt: ManagedOwnerSigabrtReceipt;
|
|
34
|
+
admission: { session_id: string; endpoint_incarnation: string; owner_generation: string; admitted: true };
|
|
35
|
+
decision: UltragoalRecoveryDecision;
|
|
36
|
+
}
|
|
37
|
+
export type ManagedOwnerAdmission =
|
|
38
|
+
| { kind: "fresh" | "supervised" }
|
|
39
|
+
| { kind: "recovery"; context: ManagedOwnerRecoveryContext }
|
|
40
|
+
| { kind: "blocked" };
|
|
41
|
+
|
|
42
|
+
function ownerEnvironment(): {
|
|
43
|
+
root: string;
|
|
44
|
+
generation: string;
|
|
45
|
+
sessionId: string;
|
|
46
|
+
runId: string;
|
|
47
|
+
incarnation: string;
|
|
48
|
+
} | null {
|
|
49
|
+
const stateDir = process.env[MANAGED_OWNER_STATE_DIR_ENV]?.trim();
|
|
50
|
+
const sessionId = process.env[MANAGED_OWNER_SESSION_ID_ENV]?.trim();
|
|
51
|
+
const generation = process.env[MANAGED_OWNER_GENERATION_ENV]?.trim();
|
|
52
|
+
const runId = process.env[MANAGED_OWNER_RUN_ID_ENV]?.trim();
|
|
53
|
+
const incarnation = process.env[MANAGED_OWNER_INCARNATION_ENV]?.trim();
|
|
54
|
+
if (!stateDir && !sessionId && !generation && !runId && !incarnation) return null;
|
|
55
|
+
if (!stateDir || !sessionId || !generation || !runId || !incarnation || !path.isAbsolute(stateDir))
|
|
56
|
+
throw new Error("managed_owner_admission_metadata_invalid");
|
|
57
|
+
for (const [value, label] of [
|
|
58
|
+
[sessionId, "managed owner session id"],
|
|
59
|
+
[generation, "managed owner generation"],
|
|
60
|
+
[runId, "managed owner run id"],
|
|
61
|
+
[incarnation, "managed owner incarnation"],
|
|
62
|
+
] as const)
|
|
63
|
+
assertSafePathComponent(value, label);
|
|
64
|
+
const root = lifecyclePaths(stateDir, sessionId, generation).root;
|
|
65
|
+
if (!root.startsWith(`${path.resolve(stateDir)}${path.sep}`)) throw new Error("managed_owner_admission_path_unsafe");
|
|
66
|
+
return { root, generation, sessionId, runId, incarnation };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isCommand(command: unknown): command is string[] {
|
|
70
|
+
return (
|
|
71
|
+
Array.isArray(command) &&
|
|
72
|
+
command.length > 0 &&
|
|
73
|
+
command.every(value => typeof value === "string" && value.length > 0)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isBinding(
|
|
78
|
+
value: unknown,
|
|
79
|
+
expected: { generation: string; sessionId: string; runId: string; incarnation: string; token: string },
|
|
80
|
+
): value is ManagedOwnerBinding {
|
|
81
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
82
|
+
const binding = value as Partial<ManagedOwnerBinding>;
|
|
83
|
+
return (
|
|
84
|
+
binding.schema_version === 2 &&
|
|
85
|
+
binding.generation === expected.generation &&
|
|
86
|
+
binding.session_id === expected.sessionId &&
|
|
87
|
+
binding.run_id === expected.runId &&
|
|
88
|
+
binding.endpoint_incarnation === expected.incarnation &&
|
|
89
|
+
binding.child_token === expected.token &&
|
|
90
|
+
isCommand(binding.command) &&
|
|
91
|
+
typeof binding.command_sha256 === "string" &&
|
|
92
|
+
binding.command_sha256 === crypto.createHash("sha256").update(JSON.stringify(binding.command)).digest("hex") &&
|
|
93
|
+
typeof binding.supervisor_pid === "number" &&
|
|
94
|
+
Number.isSafeInteger(binding.supervisor_pid) &&
|
|
95
|
+
binding.supervisor_pid > 0 &&
|
|
96
|
+
typeof binding.supervisor_start_time === "string" &&
|
|
97
|
+
typeof binding.created_at === "string"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isReceipt(value: unknown, binding: ManagedOwnerBinding): value is ManagedOwnerSigabrtReceipt {
|
|
102
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
103
|
+
const receipt = value as Partial<ManagedOwnerSigabrtReceipt>;
|
|
104
|
+
return (
|
|
105
|
+
receipt.schema_version === 2 &&
|
|
106
|
+
receipt.generation === binding.generation &&
|
|
107
|
+
receipt.session_id === binding.session_id &&
|
|
108
|
+
receipt.run_id === binding.run_id &&
|
|
109
|
+
receipt.endpoint_incarnation === binding.endpoint_incarnation &&
|
|
110
|
+
receipt.child_token === binding.child_token &&
|
|
111
|
+
receipt.command_sha256 === binding.command_sha256 &&
|
|
112
|
+
receipt.supervisor_pid === binding.supervisor_pid &&
|
|
113
|
+
receipt.supervisor_start_time === binding.supervisor_start_time &&
|
|
114
|
+
typeof receipt.child_pid === "number" &&
|
|
115
|
+
Number.isSafeInteger(receipt.child_pid) &&
|
|
116
|
+
receipt.child_pid > 0 &&
|
|
117
|
+
typeof receipt.child_start_time === "string" &&
|
|
118
|
+
receipt.signal === "SIGABRT" &&
|
|
119
|
+
receipt.signal_number === 6 &&
|
|
120
|
+
(receipt.exit_code === null || Number.isSafeInteger(receipt.exit_code)) &&
|
|
121
|
+
typeof receipt.received_at === "string"
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function safeChildToken(value: string): boolean {
|
|
126
|
+
try {
|
|
127
|
+
assertSafePathComponent(value, "managed owner child token");
|
|
128
|
+
return true;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function readExactJsons(root: string, files: readonly string[]): Promise<unknown[] | null> {
|
|
135
|
+
if (process.platform !== "linux") return null;
|
|
136
|
+
try {
|
|
137
|
+
const authority = openRecoveryFsRoot(root);
|
|
138
|
+
try {
|
|
139
|
+
const values: unknown[] = [];
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
const result = authority.read(file, 64 * 1024);
|
|
142
|
+
if (!result.ok || !result.data) return null;
|
|
143
|
+
const content = Buffer.from(result.data).toString("utf8");
|
|
144
|
+
if (!content.endsWith("\n") || content.indexOf("\n") !== content.length - 1) return null;
|
|
145
|
+
values.push(JSON.parse(content));
|
|
146
|
+
}
|
|
147
|
+
return values;
|
|
148
|
+
} finally {
|
|
149
|
+
authority.close();
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function durableHandoff(
|
|
157
|
+
root: string,
|
|
158
|
+
generation: string,
|
|
159
|
+
sessionId: string,
|
|
160
|
+
reason: string,
|
|
161
|
+
details: Record<string, unknown> = {},
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
await fs.mkdir(root, { recursive: true, mode: 0o700 });
|
|
164
|
+
const file = path.join(root, `admission-handoff-${crypto.randomUUID()}.json`);
|
|
165
|
+
const record = {
|
|
166
|
+
schema_version: 2,
|
|
167
|
+
generation,
|
|
168
|
+
session_id: sessionId,
|
|
169
|
+
state: "fail_closed_handoff",
|
|
170
|
+
reason,
|
|
171
|
+
...details,
|
|
172
|
+
created_at: new Date().toISOString(),
|
|
173
|
+
};
|
|
174
|
+
const handle = await fs.open(file, "wx", 0o600);
|
|
175
|
+
try {
|
|
176
|
+
await handle.writeFile(`${JSON.stringify(record)}\n`);
|
|
177
|
+
await handle.sync();
|
|
178
|
+
} finally {
|
|
179
|
+
await handle.close();
|
|
180
|
+
}
|
|
181
|
+
const directory = await fs.open(root, "r");
|
|
182
|
+
try {
|
|
183
|
+
await directory.sync();
|
|
184
|
+
} finally {
|
|
185
|
+
await directory.close();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* This is the pre-CLI barrier. A replacement is identified only by the exact
|
|
191
|
+
* predecessor token supplied by its launch binding; directory enumeration is
|
|
192
|
+
* deliberately never an authority source.
|
|
193
|
+
*/
|
|
194
|
+
export async function admitManagedOwnerBeforeCli(): Promise<ManagedOwnerAdmission> {
|
|
195
|
+
const owner = ownerEnvironment();
|
|
196
|
+
if (!owner) return { kind: "fresh" };
|
|
197
|
+
const childToken = process.env[MANAGED_OWNER_CHILD_TOKEN_ENV]?.trim();
|
|
198
|
+
const predecessorToken = process.env[MANAGED_OWNER_PREDECESSOR_TOKEN_ENV]?.trim();
|
|
199
|
+
if (childToken && !predecessorToken) {
|
|
200
|
+
if (!safeChildToken(childToken)) {
|
|
201
|
+
await durableHandoff(owner.root, owner.generation, owner.sessionId, "exact_child_binding_unavailable");
|
|
202
|
+
process.exitCode = 75;
|
|
203
|
+
return { kind: "blocked" };
|
|
204
|
+
}
|
|
205
|
+
const [binding] = (await readExactJsons(owner.root, [`child-${childToken}.binding.json`])) ?? [];
|
|
206
|
+
if (
|
|
207
|
+
isBinding(binding, {
|
|
208
|
+
generation: owner.generation,
|
|
209
|
+
sessionId: owner.sessionId,
|
|
210
|
+
runId: owner.runId,
|
|
211
|
+
incarnation: owner.incarnation,
|
|
212
|
+
token: childToken,
|
|
213
|
+
})
|
|
214
|
+
)
|
|
215
|
+
return { kind: "supervised" };
|
|
216
|
+
await durableHandoff(owner.root, owner.generation, owner.sessionId, "exact_child_binding_unavailable");
|
|
217
|
+
process.exitCode = 75;
|
|
218
|
+
return { kind: "blocked" };
|
|
219
|
+
}
|
|
220
|
+
const predecessorGeneration = process.env[MANAGED_OWNER_PREDECESSOR_GENERATION_ENV]?.trim();
|
|
221
|
+
const predecessorRunId = process.env[MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV]?.trim();
|
|
222
|
+
const predecessorIncarnation = process.env[MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV]?.trim();
|
|
223
|
+
if (!predecessorGeneration || !predecessorRunId || !predecessorIncarnation) {
|
|
224
|
+
await durableHandoff(owner.root, owner.generation, owner.sessionId, "replacement_predecessor_identity_missing");
|
|
225
|
+
process.exitCode = 75;
|
|
226
|
+
return { kind: "blocked" };
|
|
227
|
+
}
|
|
228
|
+
if (!predecessorToken) {
|
|
229
|
+
await durableHandoff(owner.root, owner.generation, owner.sessionId, "replacement_predecessor_binding_missing");
|
|
230
|
+
process.exitCode = 75;
|
|
231
|
+
return { kind: "blocked" };
|
|
232
|
+
}
|
|
233
|
+
if (!safeChildToken(predecessorToken)) {
|
|
234
|
+
await durableHandoff(owner.root, owner.generation, owner.sessionId, "replacement_predecessor_binding_untrusted");
|
|
235
|
+
process.exitCode = 75;
|
|
236
|
+
return { kind: "blocked" };
|
|
237
|
+
}
|
|
238
|
+
const [binding, receipt] =
|
|
239
|
+
(await readExactJsons(owner.root, [
|
|
240
|
+
`child-${predecessorToken}.binding.json`,
|
|
241
|
+
`sigabrt-${predecessorToken}.receipt.json`,
|
|
242
|
+
])) ?? [];
|
|
243
|
+
if (
|
|
244
|
+
!isBinding(binding, {
|
|
245
|
+
generation: predecessorGeneration,
|
|
246
|
+
sessionId: owner.sessionId,
|
|
247
|
+
runId: predecessorRunId,
|
|
248
|
+
incarnation: predecessorIncarnation,
|
|
249
|
+
token: predecessorToken,
|
|
250
|
+
})
|
|
251
|
+
) {
|
|
252
|
+
await durableHandoff(owner.root, owner.generation, owner.sessionId, "replacement_predecessor_binding_untrusted");
|
|
253
|
+
process.exitCode = 75;
|
|
254
|
+
return { kind: "blocked" };
|
|
255
|
+
}
|
|
256
|
+
if (!isReceipt(receipt, binding)) {
|
|
257
|
+
await durableHandoff(owner.root, owner.generation, owner.sessionId, "exact_sigabrt_receipt_untrusted");
|
|
258
|
+
process.exitCode = 75;
|
|
259
|
+
return { kind: "blocked" };
|
|
260
|
+
}
|
|
261
|
+
const admission = {
|
|
262
|
+
session_id: owner.sessionId,
|
|
263
|
+
endpoint_incarnation: predecessorIncarnation,
|
|
264
|
+
owner_generation: predecessorGeneration,
|
|
265
|
+
admitted: true,
|
|
266
|
+
} as const;
|
|
267
|
+
const decision = await planUltragoalOwnerLossRecovery({
|
|
268
|
+
binding: {
|
|
269
|
+
sessionId: owner.sessionId,
|
|
270
|
+
endpointIncarnation: predecessorIncarnation,
|
|
271
|
+
ownerGeneration: predecessorGeneration,
|
|
272
|
+
cwd: process.cwd(),
|
|
273
|
+
},
|
|
274
|
+
receipt,
|
|
275
|
+
admission,
|
|
276
|
+
transcriptPath: process.env[MANAGED_OWNER_TRANSCRIPT_PATH_ENV] ?? "",
|
|
277
|
+
});
|
|
278
|
+
await persistUltragoalRecoveryDecision({
|
|
279
|
+
cwd: process.cwd(),
|
|
280
|
+
sessionId: owner.sessionId,
|
|
281
|
+
binding: {
|
|
282
|
+
sessionId: owner.sessionId,
|
|
283
|
+
endpointIncarnation: predecessorIncarnation,
|
|
284
|
+
ownerGeneration: predecessorGeneration,
|
|
285
|
+
cwd: process.cwd(),
|
|
286
|
+
},
|
|
287
|
+
decision,
|
|
288
|
+
});
|
|
289
|
+
if (decision.disposition !== "resume") {
|
|
290
|
+
await durableHandoff(owner.root, owner.generation, owner.sessionId, decision.reason);
|
|
291
|
+
process.exitCode = 75;
|
|
292
|
+
return { kind: "blocked" };
|
|
293
|
+
}
|
|
294
|
+
return { kind: "recovery", context: { root: owner.root, binding, receipt, admission, decision } };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The ordinary CLI factory has no recovery-owned writer-lease or exact-child
|
|
299
|
+
* reconciliation capability. Do not route a recovered owner through it: first
|
|
300
|
+
* revalidate the immutable evidence and B0, then publish a terminal handoff.
|
|
301
|
+
*/
|
|
302
|
+
export async function completeManagedOwnerRecovery(
|
|
303
|
+
context: ManagedOwnerRecoveryContext,
|
|
304
|
+
): Promise<{ kind: "handoff"; exitCode: 75 }> {
|
|
305
|
+
const recoveryBinding = {
|
|
306
|
+
sessionId: context.binding.session_id,
|
|
307
|
+
endpointIncarnation: context.binding.endpoint_incarnation,
|
|
308
|
+
ownerGeneration: context.binding.generation,
|
|
309
|
+
cwd: process.cwd(),
|
|
310
|
+
};
|
|
311
|
+
const revalidated = await planUltragoalOwnerLossRecovery({
|
|
312
|
+
binding: recoveryBinding,
|
|
313
|
+
receipt: context.receipt,
|
|
314
|
+
admission: context.admission,
|
|
315
|
+
transcriptPath: process.env[MANAGED_OWNER_TRANSCRIPT_PATH_ENV] ?? "",
|
|
316
|
+
protectedPaths: context.decision.snapshot?.protectedPaths,
|
|
317
|
+
sanctionedDeltas: context.decision.snapshot?.sanctionedDeltas,
|
|
318
|
+
absentArtifacts: context.decision.snapshot?.absentArtifacts,
|
|
319
|
+
transientHistory: context.decision.snapshot?.transientHistory,
|
|
320
|
+
});
|
|
321
|
+
const b0Unchanged =
|
|
322
|
+
context.decision.snapshot !== undefined &&
|
|
323
|
+
revalidated.snapshot !== undefined &&
|
|
324
|
+
context.decision.snapshot.b0.planSha256 === revalidated.snapshot.b0.planSha256 &&
|
|
325
|
+
context.decision.snapshot.b0.ledgerSha256 === revalidated.snapshot.b0.ledgerSha256;
|
|
326
|
+
const transcriptUnchanged =
|
|
327
|
+
context.decision.terminal?.yieldId !== undefined &&
|
|
328
|
+
revalidated.terminal?.yieldId === context.decision.terminal.yieldId;
|
|
329
|
+
const reason =
|
|
330
|
+
revalidated.disposition !== "resume"
|
|
331
|
+
? `recovery_authority_changed:${revalidated.reason}`
|
|
332
|
+
: !b0Unchanged
|
|
333
|
+
? "recovery_b0_changed"
|
|
334
|
+
: !transcriptUnchanged
|
|
335
|
+
? "recovery_transcript_changed"
|
|
336
|
+
: "safe_session_resume_seam_unavailable";
|
|
337
|
+
const decision: UltragoalRecoveryDecision = { disposition: "handoff", reason };
|
|
338
|
+
await persistUltragoalRecoveryDecision({
|
|
339
|
+
cwd: recoveryBinding.cwd,
|
|
340
|
+
sessionId: recoveryBinding.sessionId,
|
|
341
|
+
binding: recoveryBinding,
|
|
342
|
+
decision,
|
|
343
|
+
});
|
|
344
|
+
await durableHandoff(context.root, context.binding.generation, context.binding.session_id, reason, {
|
|
345
|
+
predecessor_child_token: context.binding.child_token,
|
|
346
|
+
predecessor_run_id: context.binding.run_id,
|
|
347
|
+
terminal_reconciliation: "unavailable_without_owning_store_cas",
|
|
348
|
+
b0_preserved: b0Unchanged,
|
|
349
|
+
});
|
|
350
|
+
process.exitCode = 75;
|
|
351
|
+
return { kind: "handoff", exitCode: 75 };
|
|
352
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { readLinuxProcStartTime } from "./linux-proc";
|
|
5
|
+
import { assertSafePathComponent } from "./session-layout";
|
|
6
|
+
import { lifecyclePaths } from "./tmux-owner-isolation";
|
|
7
|
+
|
|
8
|
+
export const MANAGED_OWNER_SUPERVISOR_ARG = "--internal-managed-owner-supervisor";
|
|
9
|
+
export const MANAGED_OWNER_CHILD_TOKEN_ENV = "GJC_MANAGED_OWNER_CHILD_TOKEN";
|
|
10
|
+
export const MANAGED_OWNER_COMMAND_ENV = "GJC_MANAGED_OWNER_COMMAND_JSON";
|
|
11
|
+
export const MANAGED_OWNER_SESSION_ID_ENV = "GJC_COORDINATOR_SESSION_ID";
|
|
12
|
+
export const MANAGED_OWNER_GENERATION_ENV = "GJC_TMUX_OWNER_GENERATION";
|
|
13
|
+
export const MANAGED_OWNER_STATE_DIR_ENV = "GJC_TMUX_OWNER_STATE_DIR";
|
|
14
|
+
export const MANAGED_OWNER_RUN_ID_ENV = "GJC_MANAGED_OWNER_RUN_ID";
|
|
15
|
+
export const MANAGED_OWNER_INCARNATION_ENV = "GJC_MANAGED_OWNER_INCARNATION";
|
|
16
|
+
|
|
17
|
+
export interface ManagedOwnerBinding {
|
|
18
|
+
schema_version: 2;
|
|
19
|
+
generation: string;
|
|
20
|
+
session_id: string;
|
|
21
|
+
run_id: string;
|
|
22
|
+
endpoint_incarnation: string;
|
|
23
|
+
child_token: string;
|
|
24
|
+
command: string[];
|
|
25
|
+
command_sha256: string;
|
|
26
|
+
supervisor_pid: number;
|
|
27
|
+
supervisor_start_time: string;
|
|
28
|
+
created_at: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ManagedOwnerSigabrtReceipt {
|
|
32
|
+
schema_version: 2;
|
|
33
|
+
generation: string;
|
|
34
|
+
session_id: string;
|
|
35
|
+
run_id: string;
|
|
36
|
+
endpoint_incarnation: string;
|
|
37
|
+
child_token: string;
|
|
38
|
+
command_sha256: string;
|
|
39
|
+
supervisor_pid: number;
|
|
40
|
+
supervisor_start_time: string;
|
|
41
|
+
child_pid: number;
|
|
42
|
+
child_start_time: string;
|
|
43
|
+
signal: "SIGABRT";
|
|
44
|
+
signal_number: 6;
|
|
45
|
+
exit_code: number | null;
|
|
46
|
+
received_at: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function requiredEnvironment(name: string): string {
|
|
50
|
+
const value = process.env[name]?.trim();
|
|
51
|
+
if (!value) throw new Error(`managed_owner_${name.toLowerCase()}_missing`);
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function lifecycleRoot(): { root: string; generation: string; sessionId: string; runId: string; incarnation: string } {
|
|
56
|
+
const stateDir = requiredEnvironment(MANAGED_OWNER_STATE_DIR_ENV);
|
|
57
|
+
const sessionId = requiredEnvironment(MANAGED_OWNER_SESSION_ID_ENV);
|
|
58
|
+
const generation = requiredEnvironment(MANAGED_OWNER_GENERATION_ENV);
|
|
59
|
+
const runId = requiredEnvironment(MANAGED_OWNER_RUN_ID_ENV);
|
|
60
|
+
const incarnation = requiredEnvironment(MANAGED_OWNER_INCARNATION_ENV);
|
|
61
|
+
for (const [value, label] of [
|
|
62
|
+
[sessionId, "managed owner session id"],
|
|
63
|
+
[generation, "managed owner generation"],
|
|
64
|
+
[runId, "managed owner run id"],
|
|
65
|
+
[incarnation, "managed owner incarnation"],
|
|
66
|
+
] as const)
|
|
67
|
+
assertSafePathComponent(value, label);
|
|
68
|
+
if (!path.isAbsolute(stateDir)) throw new Error("managed_owner_lifecycle_path_unsafe");
|
|
69
|
+
const root = lifecyclePaths(stateDir, sessionId, generation).root;
|
|
70
|
+
if (!root.startsWith(`${path.resolve(stateDir)}${path.sep}`)) throw new Error("managed_owner_lifecycle_path_unsafe");
|
|
71
|
+
return { root, generation, sessionId, runId, incarnation };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function commandDigest(command: readonly string[]): string {
|
|
75
|
+
return crypto.createHash("sha256").update(JSON.stringify(command)).digest("hex");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function writeDurableExclusive(file: string, value: object): Promise<void> {
|
|
79
|
+
const handle = await fs.open(file, "wx", 0o600);
|
|
80
|
+
try {
|
|
81
|
+
await handle.writeFile(`${JSON.stringify(value)}\n`);
|
|
82
|
+
await handle.sync();
|
|
83
|
+
} finally {
|
|
84
|
+
await handle.close();
|
|
85
|
+
}
|
|
86
|
+
const directory = await fs.open(path.dirname(file), "r");
|
|
87
|
+
try {
|
|
88
|
+
await directory.sync();
|
|
89
|
+
} finally {
|
|
90
|
+
await directory.close();
|
|
91
|
+
}
|
|
92
|
+
const persisted = JSON.parse(await fs.readFile(file, "utf8")) as unknown;
|
|
93
|
+
if (JSON.stringify(persisted) !== JSON.stringify(value)) throw new Error("managed_owner_durable_reread_failed");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function childCommand(): string[] {
|
|
97
|
+
const command = JSON.parse(requiredEnvironment(MANAGED_OWNER_COMMAND_ENV)) as unknown;
|
|
98
|
+
if (!Array.isArray(command) || command.length === 0 || command.some(value => typeof value !== "string" || !value))
|
|
99
|
+
throw new Error("managed_owner_command_invalid");
|
|
100
|
+
return command;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isManagedOwnerSupervisorArgv(args: readonly string[]): boolean {
|
|
104
|
+
return args.length === 1 && args[0] === MANAGED_OWNER_SUPERVISOR_ARG;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Runs one exact child and publishes authority only for a directly observed Linux signal 6. */
|
|
108
|
+
export async function runManagedOwnerSupervisor(): Promise<void> {
|
|
109
|
+
const { root, generation, sessionId, runId, incarnation } = lifecycleRoot();
|
|
110
|
+
const command = childCommand();
|
|
111
|
+
const supervisorStartTime = await readLinuxProcStartTime(process.pid);
|
|
112
|
+
if (!supervisorStartTime) throw new Error("managed_owner_supervisor_start_time_unavailable");
|
|
113
|
+
await fs.mkdir(root, { recursive: true, mode: 0o700 });
|
|
114
|
+
const childToken = crypto.randomUUID();
|
|
115
|
+
const binding: ManagedOwnerBinding = {
|
|
116
|
+
schema_version: 2,
|
|
117
|
+
generation,
|
|
118
|
+
session_id: sessionId,
|
|
119
|
+
run_id: runId,
|
|
120
|
+
endpoint_incarnation: incarnation,
|
|
121
|
+
child_token: childToken,
|
|
122
|
+
command,
|
|
123
|
+
command_sha256: commandDigest(command),
|
|
124
|
+
supervisor_pid: process.pid,
|
|
125
|
+
supervisor_start_time: supervisorStartTime,
|
|
126
|
+
created_at: new Date().toISOString(),
|
|
127
|
+
};
|
|
128
|
+
await writeDurableExclusive(path.join(root, `child-${childToken}.binding.json`), binding);
|
|
129
|
+
const child = Bun.spawn({
|
|
130
|
+
cmd: command,
|
|
131
|
+
stdin: "inherit",
|
|
132
|
+
stdout: "inherit",
|
|
133
|
+
stderr: "inherit",
|
|
134
|
+
env: { ...process.env, [MANAGED_OWNER_CHILD_TOKEN_ENV]: childToken },
|
|
135
|
+
});
|
|
136
|
+
const childStartTime = await readLinuxProcStartTime(child.pid);
|
|
137
|
+
if (!childStartTime) throw new Error("managed_owner_child_start_time_unavailable");
|
|
138
|
+
const exitCode = await child.exited;
|
|
139
|
+
if (child.signalCode === "SIGABRT") {
|
|
140
|
+
const receipt: ManagedOwnerSigabrtReceipt = {
|
|
141
|
+
schema_version: 2,
|
|
142
|
+
generation,
|
|
143
|
+
session_id: sessionId,
|
|
144
|
+
run_id: runId,
|
|
145
|
+
endpoint_incarnation: incarnation,
|
|
146
|
+
child_token: childToken,
|
|
147
|
+
command_sha256: binding.command_sha256,
|
|
148
|
+
supervisor_pid: process.pid,
|
|
149
|
+
supervisor_start_time: supervisorStartTime,
|
|
150
|
+
child_pid: child.pid,
|
|
151
|
+
child_start_time: childStartTime,
|
|
152
|
+
signal: "SIGABRT",
|
|
153
|
+
signal_number: 6,
|
|
154
|
+
exit_code: exitCode,
|
|
155
|
+
received_at: new Date().toISOString(),
|
|
156
|
+
};
|
|
157
|
+
await writeDurableExclusive(path.join(root, `sigabrt-${childToken}.receipt.json`), receipt);
|
|
158
|
+
process.exitCode = 134;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
process.exitCode = exitCode;
|
|
162
|
+
}
|