@sayknow-cli/coding-agent 0.3.12 → 0.3.15
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/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
- package/dist/types/autoresearch/tools/log-experiment.d.ts +2 -2
- package/dist/types/cli/classify-install-target.d.ts +0 -10
- package/dist/types/cli/daemon-cli.d.ts +2 -0
- package/dist/types/cli/initial-message.d.ts +1 -1
- package/dist/types/cli/malloc-env-guard.d.ts +65 -0
- package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +3 -1
- package/dist/types/cli/session-picker.d.ts +4 -3
- package/dist/types/cli/update-cli.d.ts +22 -3
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/commands/daemon.d.ts +6 -2
- package/dist/types/commands/launch.d.ts +1 -0
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/commands/web-search.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-changelog.d.ts +4 -4
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -9
- package/dist/types/commit/agentic/tools/schemas.d.ts +9 -9
- package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -9
- package/dist/types/commit/analysis/conventional.d.ts +1 -1
- package/dist/types/commit/analysis/summary.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +3 -3
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
- package/dist/types/commit/shared-llm.d.ts +9 -9
- package/dist/types/config/keybindings.d.ts +134 -129
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +35 -35
- package/dist/types/config/models-config-schema.d.ts +111 -111
- package/dist/types/config/settings-schema.d.ts +56 -16
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
- package/dist/types/coordinator-mcp/policy.d.ts +3 -0
- package/dist/types/coordinator-mcp/server.d.ts +79 -6
- package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
- package/dist/types/daemon/control-types.d.ts +10 -0
- package/dist/types/daemon/operator-contract.d.ts +47 -0
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
- package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +12 -12
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
- package/dist/types/edit/modes/patch.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -1
- package/dist/types/goals/tools/goal-tool.d.ts +6 -6
- package/dist/types/i18n/messages/en.d.ts +8 -8
- package/dist/types/internal-urls/local-protocol.d.ts +13 -9
- package/dist/types/lsp/index.d.ts +9 -9
- package/dist/types/lsp/types.d.ts +9 -9
- package/dist/types/main.d.ts +66 -2
- package/dist/types/modes/acp/acp-agent.d.ts +19 -2
- package/dist/types/modes/acp/acp-mode.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
- package/dist/types/modes/components/pet-selector.d.ts +11 -0
- package/dist/types/modes/components/sayknow-pet-widget.d.ts +59 -0
- package/dist/types/modes/components/session-selector.d.ts +22 -9
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +0 -10
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/controllers/event-controller.d.ts +4 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +24 -2
- package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
- package/dist/types/modes/print-mode.d.ts +3 -4
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
- package/dist/types/modes/theme/defaults/index.d.ts +568 -568
- package/dist/types/modes/types.d.ts +28 -2
- package/dist/types/modes/utils/abort-message.d.ts +1 -1
- package/dist/types/modes/utils/context-usage.d.ts +4 -19
- package/dist/types/modes/utils/irc-message.d.ts +38 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
- package/dist/types/notifications/config.d.ts +34 -0
- package/dist/types/notifications/daemon-paths.d.ts +7 -0
- package/dist/types/notifications/index.d.ts +27 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
- package/dist/types/notifications/notification-service.d.ts +144 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
- package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
- package/dist/types/notifications/telegram-daemon.d.ts +130 -3
- package/dist/types/notifications/telegram-reference.d.ts +19 -2
- package/dist/types/notifications/topic-registry.d.ts +24 -4
- package/dist/types/registry/agent-registry.d.ts +2 -0
- package/dist/types/rlm/index.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +40 -3
- package/dist/types/session/cache-economics.d.ts +79 -0
- package/dist/types/session/context-estimation.d.ts +31 -0
- package/dist/types/session/provider-safety-stop.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +124 -4
- package/dist/types/session/session-storage.d.ts +155 -13
- package/dist/types/setup/credential-auto-import.d.ts +2 -2
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +34 -3
- package/dist/types/skc-runtime/state-schema.d.ts +19 -19
- package/dist/types/skc-runtime/tmux-common.d.ts +6 -0
- package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +309 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +49 -17
- package/dist/types/task/agents.d.ts +2 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/receipt.d.ts +3 -1
- package/dist/types/task/types.d.ts +35 -27
- package/dist/types/tools/ask-answer-registry.d.ts +5 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
- package/dist/types/tools/browser.d.ts +28 -28
- package/dist/types/tools/computer.d.ts +20 -20
- package/dist/types/tools/cron.d.ts +2 -2
- package/dist/types/tools/debug.d.ts +40 -40
- package/dist/types/tools/gh.d.ts +10 -10
- package/dist/types/tools/image-gen.d.ts +2 -2
- package/dist/types/tools/index.d.ts +58 -6
- package/dist/types/tools/monitor.d.ts +6 -6
- package/dist/types/tools/skill-discovery.d.ts +4 -4
- package/dist/types/tools/subagent.d.ts +12 -12
- package/dist/types/tools/telegram-send.d.ts +7 -0
- package/dist/types/tools/todo-write.d.ts +8 -8
- package/dist/types/utils/external-editor.d.ts +2 -0
- package/dist/types/web/insane/bridge.d.ts +9 -9
- package/dist/types/web/search/index.d.ts +4 -4
- package/package.json +8 -8
- package/scripts/g004-tmux-smoke.ts +196 -36
- package/src/cli/daemon-cli.ts +19 -29
- package/src/cli/malloc-env-guard.test.ts +104 -0
- package/src/cli/malloc-env-guard.ts +149 -0
- package/src/cli/notify-cli.ts +78 -14
- package/src/cli/session-picker.ts +23 -40
- package/src/cli/update-cli.ts +56 -37
- package/src/cli.ts +51 -10
- package/src/commands/daemon.ts +19 -5
- package/src/commands/harness.ts +515 -47
- package/src/commands/launch.ts +11 -3
- package/src/commands/notify.ts +5 -1
- package/src/commands/session.ts +6 -1
- package/src/config/keybindings.ts +5 -0
- package/src/config/model-profile-activation.ts +1 -1
- package/src/config/model-profiles.ts +14 -14
- package/src/config/settings-schema.ts +41 -1
- package/src/config/settings.ts +96 -45
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/model-preset.ts +100 -0
- package/src/coordinator-mcp/policy.ts +30 -0
- package/src/coordinator-mcp/server.ts +2958 -430
- package/src/coordinator-mcp/session-reaper.ts +129 -0
- package/src/daemon/control-types.ts +11 -0
- package/src/daemon/operator-contract.ts +121 -0
- package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
- package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
- package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/internal-urls/local-protocol.ts +29 -10
- package/src/main.ts +301 -80
- package/src/modes/acp/acp-agent.ts +730 -100
- package/src/modes/acp/acp-mode.ts +29 -2
- package/src/modes/components/assistant-message.ts +32 -3
- package/src/modes/components/bash-execution.ts +43 -13
- package/src/modes/components/custom-editor.ts +5 -0
- package/src/modes/components/footer.ts +3 -3
- package/src/modes/components/hook-selector.ts +12 -0
- package/src/modes/components/irc-sidebar.ts +206 -0
- package/src/modes/components/pet-selector.ts +55 -0
- package/src/modes/components/sayknow-pet-widget.ts +360 -0
- package/src/modes/components/session-selector.ts +222 -221
- package/src/modes/components/settings-selector.ts +10 -0
- package/src/modes/components/status-line/segments.ts +14 -7
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +4 -171
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/components/user-message.ts +26 -9
- package/src/modes/controllers/command-controller.ts +28 -2
- package/src/modes/controllers/event-controller.ts +159 -36
- package/src/modes/controllers/extension-ui-controller.ts +37 -18
- package/src/modes/controllers/input-controller.ts +8 -2
- package/src/modes/controllers/selector-controller.ts +62 -15
- package/src/modes/interactive-mode.ts +199 -7
- package/src/modes/irc-observation-ledger.ts +127 -0
- package/src/modes/print-mode.ts +228 -92
- package/src/modes/rpc/rpc-client.ts +58 -1
- package/src/modes/rpc/rpc-mode.ts +286 -85
- package/src/modes/rpc/rpc-types.ts +15 -1
- package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
- package/src/modes/shared/agent-wire/command-validation.ts +10 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
- package/src/modes/types.ts +34 -2
- package/src/modes/utils/context-usage.ts +56 -92
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/irc-message.ts +138 -0
- package/src/modes/utils/ui-helpers.ts +216 -50
- package/src/notifications/config.ts +44 -0
- package/src/notifications/daemon-paths.ts +8 -0
- package/src/notifications/index.ts +535 -75
- package/src/notifications/lifecycle-control-runtime.ts +930 -77
- package/src/notifications/notification-service.ts +710 -0
- package/src/notifications/rate-limit-pool.ts +46 -12
- package/src/notifications/telegram-daemon-control.ts +27 -2
- package/src/notifications/telegram-daemon.ts +635 -94
- package/src/notifications/telegram-reference.ts +73 -13
- package/src/notifications/topic-registry.ts +86 -9
- package/src/prompts/system/irc-peer-roster.md +1 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/irc.md +1 -1
- package/src/registry/agent-registry.ts +3 -0
- package/src/sdk.ts +52 -1
- package/src/secrets/obfuscator.ts +4 -2
- package/src/session/agent-session.ts +670 -217
- package/src/session/cache-economics.ts +275 -0
- package/src/session/context-estimation.ts +91 -0
- package/src/session/provider-safety-stop.ts +9 -0
- package/src/session/session-dump-format.ts +6 -0
- package/src/session/session-manager.ts +809 -39
- package/src/session/session-storage.ts +582 -38
- package/src/skc-runtime/session-state-sidecar.ts +756 -135
- package/src/skc-runtime/tmux-common.ts +26 -7
- package/src/skc-runtime/tmux-gc.ts +52 -9
- package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
- package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
- package/src/skc-runtime/tmux-sessions.ts +871 -68
- package/src/slash-commands/builtin-registry.ts +153 -9
- package/src/slash-commands/helpers/context-report.ts +26 -4
- package/src/task/executor.ts +12 -3
- package/src/task/index.ts +8 -0
- package/src/task/receipt.ts +7 -2
- package/src/task/types.ts +26 -0
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/browser/tab-supervisor.ts +63 -6
- package/src/tools/browser.ts +8 -3
- package/src/tools/index.ts +81 -6
- package/src/tools/sqlite-reader.ts +8 -4
- package/src/tui/output-block.ts +18 -6
- package/src/utils/external-editor.ts +6 -1
package/src/commands/harness.ts
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* until the RuntimeOwner (M3+) lands.
|
|
10
10
|
*/
|
|
11
11
|
import { execFileSync } from "node:child_process";
|
|
12
|
-
import { randomBytes } from "node:crypto";
|
|
12
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
13
13
|
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import * as fs from "node:fs/promises";
|
|
14
15
|
import * as path from "node:path";
|
|
15
16
|
import { Args, Command, Flags } from "@sayknow-cli/utils/cli";
|
|
16
17
|
import { classifyRecovery } from "../harness-control-plane/classifier";
|
|
@@ -46,10 +47,79 @@ import {
|
|
|
46
47
|
type SessionHandle,
|
|
47
48
|
type SessionState,
|
|
48
49
|
} from "../harness-control-plane/types";
|
|
49
|
-
import {
|
|
50
|
+
import { SPAWN_PROVENANCE_ENV } from "../notifications/config";
|
|
51
|
+
import {
|
|
52
|
+
SKC_TMUX_OWNER_GENERATION_ENV,
|
|
53
|
+
SKC_TMUX_OWNER_SERVER_KEY_ENV,
|
|
54
|
+
SKC_TMUX_OWNER_STATE_DIR_ENV,
|
|
55
|
+
} from "../skc-runtime/session-state-sidecar";
|
|
56
|
+
import { resolveSkcTmuxBinary, resolveSkcTmuxCommand, sanitizeTmuxToken } from "../skc-runtime/tmux-common";
|
|
57
|
+
import {
|
|
58
|
+
captureOwnerGenerationBaselineSync,
|
|
59
|
+
classifyCgroup,
|
|
60
|
+
isExactScopedBootstrapSuccessReceipt,
|
|
61
|
+
isOwnerGenerationBaselineCurrentSync,
|
|
62
|
+
type OwnerGenerationBaseline,
|
|
63
|
+
type OwnerIsolationProbe,
|
|
64
|
+
observeOwnerTerminal,
|
|
65
|
+
ownerProcessStartTime,
|
|
66
|
+
planTmuxOwnerIsolation,
|
|
67
|
+
replaceOwnerGenerationSync,
|
|
68
|
+
type TmuxServerProof,
|
|
69
|
+
} from "../skc-runtime/tmux-owner-isolation";
|
|
70
|
+
|
|
71
|
+
const PRIVATE_OWNER_CONTROL_FIELDS = new Set([
|
|
72
|
+
"socket_key",
|
|
73
|
+
"socketKey",
|
|
74
|
+
"tmux_socket_key",
|
|
75
|
+
"tmuxSocketKey",
|
|
76
|
+
"tmux_owner_socket_key",
|
|
77
|
+
"tmuxOwnerSocketKey",
|
|
78
|
+
"owner_generation",
|
|
79
|
+
"ownerGeneration",
|
|
80
|
+
"state_dir",
|
|
81
|
+
"stateDir",
|
|
82
|
+
"owner_state_dir",
|
|
83
|
+
"ownerStateDir",
|
|
84
|
+
"owner_server_key",
|
|
85
|
+
"ownerServerKey",
|
|
86
|
+
"owner_server_pid",
|
|
87
|
+
"ownerServerPid",
|
|
88
|
+
"owner_server_start_time",
|
|
89
|
+
"ownerServerStartTime",
|
|
90
|
+
"tmux_owner_generation",
|
|
91
|
+
"tmuxOwnerGeneration",
|
|
92
|
+
"tmux_owner_state_dir",
|
|
93
|
+
"tmuxOwnerStateDir",
|
|
94
|
+
"tmux_owner_server_key",
|
|
95
|
+
"tmuxOwnerServerKey",
|
|
96
|
+
"tmux_owner_server_pid",
|
|
97
|
+
"tmuxOwnerServerPid",
|
|
98
|
+
"tmux_owner_server_start_time",
|
|
99
|
+
"tmuxOwnerServerStartTime",
|
|
100
|
+
"socket_path",
|
|
101
|
+
"socketPath",
|
|
102
|
+
"endpoint",
|
|
103
|
+
"owner_terminal",
|
|
104
|
+
"ownerTerminal",
|
|
105
|
+
"generation",
|
|
106
|
+
"server_key",
|
|
107
|
+
"intent_id",
|
|
108
|
+
"dedupe_key",
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
function publicHarnessResponse(value: unknown): unknown {
|
|
112
|
+
if (Array.isArray(value)) return value.map(publicHarnessResponse);
|
|
113
|
+
if (!value || typeof value !== "object") return value;
|
|
114
|
+
return Object.fromEntries(
|
|
115
|
+
Object.entries(value as Record<string, unknown>)
|
|
116
|
+
.filter(([key]) => !PRIVATE_OWNER_CONTROL_FIELDS.has(key))
|
|
117
|
+
.map(([key, item]) => [key, publicHarnessResponse(item)]),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
50
120
|
|
|
51
121
|
function writeJson(value: unknown): void {
|
|
52
|
-
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
122
|
+
process.stdout.write(`${JSON.stringify(publicHarnessResponse(value), null, 2)}\n`);
|
|
53
123
|
}
|
|
54
124
|
|
|
55
125
|
function nowIso(): string {
|
|
@@ -473,6 +543,7 @@ interface OwnerSpawnResult {
|
|
|
473
543
|
live: boolean;
|
|
474
544
|
runtime: "tmux" | "detached" | "manual";
|
|
475
545
|
tmuxSessionName: string | null;
|
|
546
|
+
socketKey: string | null;
|
|
476
547
|
fallbackReason: string | null;
|
|
477
548
|
blockerReason: string | null;
|
|
478
549
|
}
|
|
@@ -481,6 +552,113 @@ function shellQuote(value: string): string {
|
|
|
481
552
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
482
553
|
}
|
|
483
554
|
|
|
555
|
+
function isBoundedNoServerDiagnostic(stderr: Uint8Array): boolean {
|
|
556
|
+
const diagnostic = new TextDecoder().decode(stderr);
|
|
557
|
+
return (
|
|
558
|
+
diagnostic.length > 0 &&
|
|
559
|
+
diagnostic.length <= 512 &&
|
|
560
|
+
/^(?:no server running on |failed to connect to server|error connecting to )/i.test(diagnostic.trim())
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function sameServerIdentity(left: TmuxServerProof, right: TmuxServerProof): boolean {
|
|
565
|
+
return (
|
|
566
|
+
left.pid === right.pid &&
|
|
567
|
+
left.startTime === right.startTime &&
|
|
568
|
+
left.cgroup?.classification === right.cgroup?.classification &&
|
|
569
|
+
left.cgroup?.scope === right.cgroup?.scope &&
|
|
570
|
+
left.cgroup?.diagnostic === right.cgroup?.diagnostic
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function isSafeServerProof(proof: TmuxServerProof): boolean {
|
|
575
|
+
return (
|
|
576
|
+
proof.state === "safe" &&
|
|
577
|
+
typeof proof.pid === "number" &&
|
|
578
|
+
Number.isSafeInteger(proof.pid) &&
|
|
579
|
+
proof.pid > 0 &&
|
|
580
|
+
typeof proof.startTime === "string" &&
|
|
581
|
+
proof.startTime.length > 0 &&
|
|
582
|
+
(proof.cgroup?.classification === "safe" ||
|
|
583
|
+
(process.platform !== "linux" && proof.cgroup?.classification === "not_applicable"))
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function exactNativeTmuxSessionId(stdout: Uint8Array): string | null {
|
|
588
|
+
const value = new TextDecoder().decode(stdout);
|
|
589
|
+
const line = value.endsWith("\n") ? value.slice(0, -1) : value;
|
|
590
|
+
return /^\$\d+$/.test(line) ? line : null;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
interface ScopedBootstrapReceipt {
|
|
594
|
+
nativeSessionId: string;
|
|
595
|
+
serverPid: number;
|
|
596
|
+
serverStartTime: string;
|
|
597
|
+
sessionName: string;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function scopedBootstrapReceipt(stdout: Uint8Array): ScopedBootstrapReceipt | null {
|
|
601
|
+
const value = new TextDecoder().decode(stdout);
|
|
602
|
+
if (!isExactScopedBootstrapSuccessReceipt(value)) return null;
|
|
603
|
+
try {
|
|
604
|
+
const receipt = JSON.parse(value) as {
|
|
605
|
+
native_session_id: unknown;
|
|
606
|
+
server_pid: unknown;
|
|
607
|
+
server_start_time: unknown;
|
|
608
|
+
session_name: unknown;
|
|
609
|
+
};
|
|
610
|
+
if (
|
|
611
|
+
typeof receipt.native_session_id !== "string" ||
|
|
612
|
+
!/^\$\d+$/.test(receipt.native_session_id) ||
|
|
613
|
+
typeof receipt.server_pid !== "number" ||
|
|
614
|
+
!Number.isSafeInteger(receipt.server_pid) ||
|
|
615
|
+
receipt.server_pid <= 0 ||
|
|
616
|
+
typeof receipt.server_start_time !== "string" ||
|
|
617
|
+
!receipt.server_start_time ||
|
|
618
|
+
typeof receipt.session_name !== "string" ||
|
|
619
|
+
!receipt.session_name
|
|
620
|
+
)
|
|
621
|
+
return null;
|
|
622
|
+
return {
|
|
623
|
+
nativeSessionId: receipt.native_session_id,
|
|
624
|
+
serverPid: receipt.server_pid,
|
|
625
|
+
serverStartTime: receipt.server_start_time,
|
|
626
|
+
sessionName: receipt.session_name,
|
|
627
|
+
};
|
|
628
|
+
} catch {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function portableProcessStartTime(pid: number): string | null {
|
|
634
|
+
if (process.platform === "linux") return null;
|
|
635
|
+
const configured = process.env.SKC_HARNESS_PROCESS_START_COMMAND;
|
|
636
|
+
let command: string[] = ["ps", "-o", "lstart=", "-p"];
|
|
637
|
+
if (configured) {
|
|
638
|
+
try {
|
|
639
|
+
const parsed = JSON.parse(configured) as unknown;
|
|
640
|
+
if (!Array.isArray(parsed) || parsed.length === 0 || parsed.some(value => typeof value !== "string" || !value))
|
|
641
|
+
return null;
|
|
642
|
+
command = parsed;
|
|
643
|
+
} catch {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
const result = Bun.spawnSync([...command, String(pid)], { stdout: "pipe", stderr: "ignore" });
|
|
648
|
+
if (result.exitCode !== 0) return null;
|
|
649
|
+
const value = result.stdout.toString();
|
|
650
|
+
const line = value.endsWith("\n") ? value.slice(0, -1) : value;
|
|
651
|
+
if (
|
|
652
|
+
!line ||
|
|
653
|
+
line.includes("\n") ||
|
|
654
|
+
line.includes("\r") ||
|
|
655
|
+
Buffer.byteLength(line) > 128 ||
|
|
656
|
+
!/^[\x20-\x7e]+$/.test(line)
|
|
657
|
+
)
|
|
658
|
+
return null;
|
|
659
|
+
return `portable:${line}`;
|
|
660
|
+
}
|
|
661
|
+
|
|
484
662
|
function deterministicHarnessTmuxSessionName(sessionId: string): string {
|
|
485
663
|
return `sayknow_cli_harness_${sanitizeTmuxToken(sessionId)}`;
|
|
486
664
|
}
|
|
@@ -627,7 +805,14 @@ export default class Harness extends Command {
|
|
|
627
805
|
// Optional rpc command override (tests / non-default hosts); defaults to `skc --mode rpc`.
|
|
628
806
|
const override = process.env.SKC_HARNESS_RPC_COMMAND;
|
|
629
807
|
const command = override ? (JSON.parse(override) as string[]) : undefined;
|
|
630
|
-
|
|
808
|
+
// Mark the RPC owner as a SKC-spawned child (canonical provenance marker)
|
|
809
|
+
// so `notifications.sessionScope = "primary"` suppresses its notification
|
|
810
|
+
// endpoint/topic; default `all` preserves historical auto-on behavior.
|
|
811
|
+
const rpc = new SayknowCliRpc({
|
|
812
|
+
sessionDir,
|
|
813
|
+
command,
|
|
814
|
+
env: { ...process.env, [SPAWN_PROVENANCE_ENV]: sessionId },
|
|
815
|
+
});
|
|
631
816
|
const owner = new RuntimeOwner({ root, sessionId, rpc });
|
|
632
817
|
const info = await owner.start();
|
|
633
818
|
writeJson({ ok: true, owner: info });
|
|
@@ -671,57 +856,354 @@ export default class Harness extends Command {
|
|
|
671
856
|
return false;
|
|
672
857
|
}
|
|
673
858
|
|
|
674
|
-
#
|
|
859
|
+
async #harnessOwnerIsolationProbe(tmuxCommand: string): Promise<OwnerIsolationProbe> {
|
|
860
|
+
return {
|
|
861
|
+
readCallerCgroup: async () =>
|
|
862
|
+
process.env.SKC_HARNESS_TEST_CALLER_CGROUP ??
|
|
863
|
+
(await fs.readFile("/proc/self/cgroup", "utf8").catch(() => null)),
|
|
864
|
+
probeServer: async (socketKey, tmuxControlArgv): Promise<TmuxServerProof> => {
|
|
865
|
+
if (!socketKey || socketKey.length > 128) return { state: "unverifiable" };
|
|
866
|
+
const controlArgv = tmuxControlArgv ?? [tmuxCommand, "-L", socketKey];
|
|
867
|
+
if (controlArgv.length < 3 || controlArgv[0] !== tmuxCommand || !controlArgv.includes("-L")) {
|
|
868
|
+
return { state: "unverifiable" };
|
|
869
|
+
}
|
|
870
|
+
const result = Bun.spawnSync([...controlArgv, "display-message", "-p", "#{pid}"], {
|
|
871
|
+
stdout: "pipe",
|
|
872
|
+
stderr: "pipe",
|
|
873
|
+
});
|
|
874
|
+
if (result.exitCode !== 0) {
|
|
875
|
+
return isBoundedNoServerDiagnostic(result.stderr) ? { state: "absent" } : { state: "unverifiable" };
|
|
876
|
+
}
|
|
877
|
+
const pid = Number(result.stdout.toString().trim());
|
|
878
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return { state: "unverifiable" };
|
|
879
|
+
const cgroupText =
|
|
880
|
+
process.env.SKC_HARNESS_TEST_SERVER_CGROUP ??
|
|
881
|
+
(process.platform === "linux"
|
|
882
|
+
? await fs.readFile(`/proc/${pid}/cgroup`, "utf8").catch(() => null)
|
|
883
|
+
: null);
|
|
884
|
+
const testStartTime = process.env.SKC_HARNESS_TEST_SERVER_START_TIME;
|
|
885
|
+
const stat =
|
|
886
|
+
testStartTime || process.platform !== "linux"
|
|
887
|
+
? null
|
|
888
|
+
: await fs.readFile(`/proc/${pid}/stat`, "utf8").catch(() => null);
|
|
889
|
+
const cgroup = classifyCgroup({ platform: process.platform, cgroupText });
|
|
890
|
+
const startTime =
|
|
891
|
+
testStartTime ??
|
|
892
|
+
(process.platform === "linux"
|
|
893
|
+
? ownerProcessStartTime(process.platform, stat)
|
|
894
|
+
: portableProcessStartTime(pid));
|
|
895
|
+
if (!startTime) return { state: "unverifiable", pid, cgroup };
|
|
896
|
+
return {
|
|
897
|
+
state:
|
|
898
|
+
cgroup.classification === "safe" || cgroup.classification === "not_applicable"
|
|
899
|
+
? "safe"
|
|
900
|
+
: cgroup.classification === "unsafe_service"
|
|
901
|
+
? "unsafe"
|
|
902
|
+
: "unverifiable",
|
|
903
|
+
pid,
|
|
904
|
+
startTime,
|
|
905
|
+
cgroup,
|
|
906
|
+
};
|
|
907
|
+
},
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async #nativeSessionBoundToName(
|
|
912
|
+
tmuxCommand: string,
|
|
913
|
+
socketKey: string,
|
|
914
|
+
nativeSessionId: string,
|
|
915
|
+
sessionName: string,
|
|
916
|
+
): Promise<boolean> {
|
|
917
|
+
const result = Bun.spawnSync(
|
|
918
|
+
[
|
|
919
|
+
tmuxCommand,
|
|
920
|
+
"-L",
|
|
921
|
+
socketKey,
|
|
922
|
+
"display-message",
|
|
923
|
+
"-p",
|
|
924
|
+
"-t",
|
|
925
|
+
nativeSessionId,
|
|
926
|
+
"#{session_id}\t#{session_name}",
|
|
927
|
+
],
|
|
928
|
+
{ stdout: "pipe", stderr: "ignore" },
|
|
929
|
+
);
|
|
930
|
+
if (result.exitCode !== 0) return false;
|
|
931
|
+
const value = result.stdout.toString();
|
|
932
|
+
const line = value.endsWith("\n") ? value.slice(0, -1) : value;
|
|
933
|
+
return line === `${nativeSessionId}\t${sessionName}`;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async #cleanupTmuxAttempt(
|
|
937
|
+
tmuxCommand: string,
|
|
938
|
+
socketKey: string,
|
|
939
|
+
nativeSessionId: string | null,
|
|
940
|
+
sessionName: string,
|
|
941
|
+
proof: TmuxServerProof | null,
|
|
942
|
+
probeServer: OwnerIsolationProbe["probeServer"],
|
|
943
|
+
): Promise<void> {
|
|
944
|
+
if (!nativeSessionId || !proof || !isSafeServerProof(proof)) throw new Error("tmux-owner-cleanup_uncertain");
|
|
945
|
+
if (!(await this.#nativeSessionBoundToName(tmuxCommand, socketKey, nativeSessionId, sessionName)))
|
|
946
|
+
throw new Error("tmux-owner-cleanup_uncertain");
|
|
947
|
+
const current = await probeServer(socketKey, [tmuxCommand, "-L", socketKey]);
|
|
948
|
+
if (!sameServerIdentity(proof, current)) throw new Error("tmux-owner-cleanup_uncertain");
|
|
949
|
+
const predicate = `#{&&:#{==:#{pid},${proof.pid}},#{&&:#{==:#{session_id},${nativeSessionId}},#{==:#{session_name},${sessionName}}}}`;
|
|
950
|
+
const killed = Bun.spawnSync(
|
|
951
|
+
[
|
|
952
|
+
tmuxCommand,
|
|
953
|
+
"-L",
|
|
954
|
+
socketKey,
|
|
955
|
+
"if-shell",
|
|
956
|
+
"-t",
|
|
957
|
+
nativeSessionId,
|
|
958
|
+
"-F",
|
|
959
|
+
predicate,
|
|
960
|
+
`kill-session -t '${nativeSessionId}' ; display-message -p __skc_harness_cleanup_ok__`,
|
|
961
|
+
"display-message -p __skc_harness_cleanup_refused__",
|
|
962
|
+
],
|
|
963
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
964
|
+
);
|
|
965
|
+
if (killed.exitCode !== 0 || killed.stdout.toString().trim() !== "__skc_harness_cleanup_ok__")
|
|
966
|
+
throw new Error("tmux-owner-cleanup_uncertain");
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async #startTmuxResidentOwner(
|
|
675
970
|
root: string,
|
|
676
971
|
sessionId: string,
|
|
677
972
|
cwd: string,
|
|
678
|
-
): {
|
|
973
|
+
): Promise<{
|
|
974
|
+
started: boolean;
|
|
975
|
+
sessionName: string;
|
|
976
|
+
socketKey: string | null;
|
|
977
|
+
reason: string | null;
|
|
978
|
+
cleanup?: () => Promise<void>;
|
|
979
|
+
}> {
|
|
679
980
|
const tmuxCommand = resolveSkcTmuxCommand();
|
|
680
|
-
|
|
981
|
+
const sessionName = deterministicHarnessTmuxSessionName(sessionId);
|
|
982
|
+
if (Bun.which(tmuxCommand) === null)
|
|
983
|
+
return { started: false, sessionName, socketKey: null, reason: "tmux-unavailable" };
|
|
984
|
+
if (resolveSkcTmuxBinary({ env: process.env }).isPsmux)
|
|
681
985
|
return {
|
|
682
986
|
started: false,
|
|
683
|
-
sessionName
|
|
684
|
-
|
|
987
|
+
sessionName,
|
|
988
|
+
socketKey: null,
|
|
989
|
+
reason: "tmux-owner-native_session_identity_unavailable",
|
|
685
990
|
};
|
|
991
|
+
const socketKey = `skc-owner-${randomBytes(24).toString("hex")}`;
|
|
992
|
+
const ownerStateDir = root;
|
|
993
|
+
let baseline: OwnerGenerationBaseline;
|
|
994
|
+
try {
|
|
995
|
+
baseline = captureOwnerGenerationBaselineSync(ownerStateDir, sessionId);
|
|
996
|
+
} catch {
|
|
997
|
+
return { started: false, sessionName, socketKey, reason: "tmux-owner-generation_unverifiable" };
|
|
686
998
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
999
|
+
if (process.platform !== "linux" && !portableProcessStartTime(process.pid))
|
|
1000
|
+
return { started: false, sessionName, socketKey, reason: "tmux-owner-generation_unverifiable" };
|
|
1001
|
+
await fs.mkdir(path.join(ownerStateDir, sessionId, "owner-lifecycle"), { recursive: true, mode: 0o700 });
|
|
1002
|
+
const ownerGeneration = randomUUID();
|
|
1003
|
+
const envAssignments = [
|
|
1004
|
+
`SKC_HARNESS_STATE_ROOT=${shellQuote(root)}`,
|
|
1005
|
+
`${SKC_TMUX_OWNER_GENERATION_ENV}=${shellQuote(ownerGeneration)}`,
|
|
1006
|
+
`${SKC_TMUX_OWNER_STATE_DIR_ENV}=${shellQuote(ownerStateDir)}`,
|
|
1007
|
+
`${SKC_TMUX_OWNER_SERVER_KEY_ENV}=${shellQuote(socketKey)}`,
|
|
1008
|
+
];
|
|
1009
|
+
if (process.env[RECEIPT_SPOOL_DIR_ENV])
|
|
690
1010
|
envAssignments.push(`${RECEIPT_SPOOL_DIR_ENV}=${shellQuote(process.env[RECEIPT_SPOOL_DIR_ENV])}`);
|
|
691
|
-
|
|
692
|
-
if (process.env.SKC_HARNESS_RPC_COMMAND) {
|
|
1011
|
+
if (process.env.SKC_HARNESS_RPC_COMMAND)
|
|
693
1012
|
envAssignments.push(`SKC_HARNESS_RPC_COMMAND=${shellQuote(process.env.SKC_HARNESS_RPC_COMMAND)}`);
|
|
694
|
-
|
|
695
|
-
if (process.env.SKC_HARNESS_TEST_NODE_MODULES) {
|
|
1013
|
+
if (process.env.SKC_HARNESS_TEST_NODE_MODULES)
|
|
696
1014
|
envAssignments.push(`SKC_HARNESS_TEST_NODE_MODULES=${shellQuote(process.env.SKC_HARNESS_TEST_NODE_MODULES)}`);
|
|
697
|
-
}
|
|
698
|
-
const
|
|
699
|
-
const
|
|
700
|
-
|
|
1015
|
+
const shellCommand = `exec env ${envAssignments.join(" ")} ${this.#buildOwnerCommand(sessionId).map(shellQuote).join(" ")}`;
|
|
1016
|
+
const probe = await this.#harnessOwnerIsolationProbe(tmuxCommand);
|
|
1017
|
+
const probeServer = probe.probeServer;
|
|
1018
|
+
probe.probeServer = async (requestedSocketKey, controlArgv) =>
|
|
1019
|
+
requestedSocketKey === socketKey ? probeServer(requestedSocketKey, controlArgv) : { state: "unverifiable" };
|
|
1020
|
+
const tmuxArgv = [
|
|
1021
|
+
tmuxCommand,
|
|
1022
|
+
"-L",
|
|
1023
|
+
socketKey,
|
|
1024
|
+
"new-session",
|
|
1025
|
+
"-d",
|
|
1026
|
+
"-s",
|
|
1027
|
+
sessionName,
|
|
1028
|
+
"-P",
|
|
1029
|
+
"-F",
|
|
1030
|
+
"#{session_id}",
|
|
1031
|
+
"-c",
|
|
1032
|
+
cwd,
|
|
1033
|
+
shellCommand,
|
|
1034
|
+
];
|
|
1035
|
+
const plan = await planTmuxOwnerIsolation(
|
|
1036
|
+
{
|
|
1037
|
+
schema_version: 1,
|
|
1038
|
+
op: "plan",
|
|
1039
|
+
platform: process.platform,
|
|
1040
|
+
session_id: sessionId,
|
|
1041
|
+
owner_generation: ownerGeneration,
|
|
1042
|
+
baseline,
|
|
1043
|
+
cwd,
|
|
1044
|
+
state_dir: ownerStateDir,
|
|
1045
|
+
socket_key: socketKey,
|
|
1046
|
+
tmux_argv: tmuxArgv,
|
|
1047
|
+
},
|
|
1048
|
+
probe,
|
|
1049
|
+
);
|
|
1050
|
+
if (!plan.ok) return { started: false, sessionName, socketKey, reason: `tmux-owner-${plan.code}` };
|
|
1051
|
+
if (!isOwnerGenerationBaselineCurrentSync(ownerStateDir, sessionId, baseline))
|
|
1052
|
+
return { started: false, sessionName, socketKey, reason: "tmux-owner-generation_stale" };
|
|
1053
|
+
let nativeSessionId: string | null = null;
|
|
1054
|
+
let cleanupProof: TmuxServerProof | null = null;
|
|
1055
|
+
const fail = async (
|
|
1056
|
+
reason: string,
|
|
1057
|
+
): Promise<{ started: false; sessionName: string; socketKey: string; reason: string }> => {
|
|
1058
|
+
try {
|
|
1059
|
+
await this.#cleanupTmuxAttempt(
|
|
1060
|
+
tmuxCommand,
|
|
1061
|
+
socketKey,
|
|
1062
|
+
nativeSessionId,
|
|
1063
|
+
sessionName,
|
|
1064
|
+
cleanupProof,
|
|
1065
|
+
probeServer,
|
|
1066
|
+
);
|
|
1067
|
+
return { started: false, sessionName, socketKey, reason };
|
|
1068
|
+
} catch {
|
|
1069
|
+
return { started: false, sessionName, socketKey, reason: `${reason}:tmux-owner-cleanup_uncertain` };
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
const created = Bun.spawnSync(plan.execution.argv, {
|
|
701
1073
|
stdout: "pipe",
|
|
702
1074
|
stderr: "pipe",
|
|
703
1075
|
env: process.env,
|
|
1076
|
+
...(plan.execution.mode === "scoped"
|
|
1077
|
+
? { stdin: new TextEncoder().encode(`${plan.execution.stdin_line}\n`) }
|
|
1078
|
+
: {}),
|
|
704
1079
|
});
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1080
|
+
const scopedReceipt = plan.execution.mode === "scoped" ? scopedBootstrapReceipt(created.stdout) : null;
|
|
1081
|
+
nativeSessionId =
|
|
1082
|
+
scopedReceipt?.nativeSessionId ??
|
|
1083
|
+
(plan.execution.mode === "direct" ? exactNativeTmuxSessionId(created.stdout) : null);
|
|
1084
|
+
if (created.exitCode !== 0)
|
|
1085
|
+
return fail(
|
|
1086
|
+
plan.execution.mode === "scoped"
|
|
1087
|
+
? "tmux-owner-scope_bootstrap_failed"
|
|
1088
|
+
: "tmux-owner-direct_creation_failed",
|
|
1089
|
+
);
|
|
1090
|
+
if (!nativeSessionId)
|
|
1091
|
+
return fail(
|
|
1092
|
+
plan.execution.mode === "scoped"
|
|
1093
|
+
? "tmux-owner-scope_bootstrap_failed"
|
|
1094
|
+
: "tmux-owner-native_session_identity_unavailable",
|
|
1095
|
+
);
|
|
1096
|
+
const postSpawnServer = await probeServer(socketKey, [tmuxCommand, "-L", socketKey]);
|
|
1097
|
+
cleanupProof = postSpawnServer;
|
|
1098
|
+
if (postSpawnServer.state === "unsafe") return fail("tmux-owner-server_unsafe");
|
|
1099
|
+
if (!isSafeServerProof(postSpawnServer)) return fail("tmux-owner-server_unverifiable");
|
|
1100
|
+
if (
|
|
1101
|
+
scopedReceipt &&
|
|
1102
|
+
(scopedReceipt.sessionName !== sessionName ||
|
|
1103
|
+
scopedReceipt.serverPid !== postSpawnServer.pid ||
|
|
1104
|
+
scopedReceipt.serverStartTime !== postSpawnServer.startTime)
|
|
1105
|
+
)
|
|
1106
|
+
return fail("tmux-owner-receipt_server_mismatch");
|
|
1107
|
+
if (!(await this.#nativeSessionBoundToName(tmuxCommand, socketKey, nativeSessionId, sessionName)))
|
|
1108
|
+
return fail("tmux-owner-native_session_identity_unproven");
|
|
1109
|
+
const boundServer = await probeServer(socketKey, [tmuxCommand, "-L", socketKey]);
|
|
1110
|
+
if (!sameServerIdentity(postSpawnServer, boundServer)) return fail("tmux-owner-server_race");
|
|
1111
|
+
cleanupProof = boundServer;
|
|
1112
|
+
if (
|
|
1113
|
+
plan.execution.mode === "direct" &&
|
|
1114
|
+
!plan.execution.server_absent_before &&
|
|
1115
|
+
(boundServer.pid !== plan.execution.server_pid || boundServer.startTime !== plan.execution.server_start_time)
|
|
1116
|
+
) {
|
|
1117
|
+
cleanupProof = null;
|
|
1118
|
+
return fail("tmux-owner-server_race");
|
|
1119
|
+
}
|
|
1120
|
+
if (!isOwnerGenerationBaselineCurrentSync(ownerStateDir, sessionId, baseline))
|
|
1121
|
+
return fail("tmux-owner-generation_stale");
|
|
1122
|
+
try {
|
|
1123
|
+
replaceOwnerGenerationSync(ownerStateDir, sessionId, ownerGeneration, baseline);
|
|
1124
|
+
} catch {
|
|
1125
|
+
return fail("tmux-owner-generation_stale");
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
started: true,
|
|
1129
|
+
sessionName,
|
|
1130
|
+
socketKey,
|
|
1131
|
+
reason: null,
|
|
1132
|
+
cleanup: async () => {
|
|
1133
|
+
await this.#cleanupTmuxAttempt(
|
|
1134
|
+
tmuxCommand,
|
|
1135
|
+
socketKey,
|
|
1136
|
+
nativeSessionId,
|
|
1137
|
+
sessionName,
|
|
1138
|
+
cleanupProof,
|
|
1139
|
+
probeServer,
|
|
1140
|
+
);
|
|
1141
|
+
await observeOwnerTerminal({
|
|
1142
|
+
schema_version: 1,
|
|
1143
|
+
op: "observe_terminal",
|
|
1144
|
+
session_id: sessionId,
|
|
1145
|
+
owner_generation: ownerGeneration,
|
|
1146
|
+
state_dir: ownerStateDir,
|
|
1147
|
+
socket_key: socketKey,
|
|
1148
|
+
observer: "sidecar",
|
|
1149
|
+
observed_at: new Date().toISOString(),
|
|
1150
|
+
signal: "UNKNOWN",
|
|
1151
|
+
exit_code: null,
|
|
1152
|
+
exit_kind: "owner_lost",
|
|
1153
|
+
reason: "endpoint_unroutable",
|
|
1154
|
+
});
|
|
1155
|
+
},
|
|
1156
|
+
};
|
|
708
1157
|
}
|
|
709
1158
|
|
|
710
|
-
/** Spawn the owner daemon.
|
|
1159
|
+
/** Spawn the owner daemon. Tmux isolation failures and unroutable starts block; unavailable tmux may fall back. */
|
|
711
1160
|
async #spawnDetachedOwner(root: string, sessionId: string, cwd: string): Promise<OwnerSpawnResult> {
|
|
712
|
-
const tmux = this.#startTmuxResidentOwner(root, sessionId, cwd);
|
|
1161
|
+
const tmux = await this.#startTmuxResidentOwner(root, sessionId, cwd);
|
|
1162
|
+
if (!tmux.started && tmux.reason?.startsWith("tmux-owner-")) {
|
|
1163
|
+
return {
|
|
1164
|
+
live: false,
|
|
1165
|
+
runtime: "manual",
|
|
1166
|
+
tmuxSessionName: null,
|
|
1167
|
+
socketKey: tmux.socketKey,
|
|
1168
|
+
fallbackReason: tmux.reason,
|
|
1169
|
+
blockerReason: "tmux-owner-isolation-failed",
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
713
1172
|
if (tmux.started && (await this.#waitForOwner(root, sessionId))) {
|
|
714
1173
|
return {
|
|
715
1174
|
live: true,
|
|
716
1175
|
runtime: "tmux",
|
|
717
1176
|
tmuxSessionName: tmux.sessionName,
|
|
1177
|
+
socketKey: tmux.socketKey,
|
|
718
1178
|
fallbackReason: null,
|
|
719
1179
|
blockerReason: null,
|
|
720
1180
|
};
|
|
721
1181
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1182
|
+
if (tmux.started) {
|
|
1183
|
+
try {
|
|
1184
|
+
if (!tmux.cleanup) throw new Error("tmux-owner-cleanup_uncertain");
|
|
1185
|
+
await tmux.cleanup();
|
|
1186
|
+
} catch {
|
|
1187
|
+
return {
|
|
1188
|
+
live: false,
|
|
1189
|
+
runtime: "manual",
|
|
1190
|
+
tmuxSessionName: tmux.sessionName,
|
|
1191
|
+
socketKey: tmux.socketKey,
|
|
1192
|
+
fallbackReason:
|
|
1193
|
+
"tmux new-session owner endpoint not routable; exact cleanup or reconciliation uncertain",
|
|
1194
|
+
blockerReason: "tmux-owner-endpoint-cleanup-uncertain",
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
live: false,
|
|
1199
|
+
runtime: "manual",
|
|
1200
|
+
tmuxSessionName: null,
|
|
1201
|
+
socketKey: tmux.socketKey,
|
|
1202
|
+
fallbackReason: "tmux new-session exited 0 but owner endpoint did not become routable; owner cleaned",
|
|
1203
|
+
blockerReason: "tmux-owner-endpoint-not-routable",
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
const fallbackReason = tmux.reason;
|
|
725
1207
|
const cmd = this.#buildOwnerCommand(sessionId);
|
|
726
1208
|
const child = Bun.spawn(cmd, {
|
|
727
1209
|
cwd,
|
|
@@ -745,6 +1227,7 @@ export default class Harness extends Command {
|
|
|
745
1227
|
live,
|
|
746
1228
|
runtime: "detached",
|
|
747
1229
|
tmuxSessionName: null,
|
|
1230
|
+
socketKey: null,
|
|
748
1231
|
fallbackReason,
|
|
749
1232
|
blockerReason: live ? null : "detached-owner-not-live",
|
|
750
1233
|
};
|
|
@@ -815,12 +1298,14 @@ export default class Harness extends Command {
|
|
|
815
1298
|
let ownerRuntime: OwnerSpawnResult["runtime"] = "manual";
|
|
816
1299
|
let ownerFallbackReason: string | null = null;
|
|
817
1300
|
let ownerBlockerReason: string | null = null;
|
|
1301
|
+
let ownerSocketKey: string | null = null;
|
|
818
1302
|
if (input.detach === true) {
|
|
819
1303
|
const ownerSpawn = await this.#spawnDetachedOwner(root, sessionId, workspace);
|
|
820
1304
|
ownerLive = ownerSpawn.live;
|
|
821
1305
|
ownerRuntime = ownerSpawn.runtime;
|
|
822
1306
|
ownerFallbackReason = ownerSpawn.fallbackReason;
|
|
823
1307
|
ownerBlockerReason = ownerSpawn.blockerReason;
|
|
1308
|
+
ownerSocketKey = ownerSpawn.socketKey;
|
|
824
1309
|
handle.viewportHandle = {
|
|
825
1310
|
kind: "event-monitor",
|
|
826
1311
|
tmuxSessionName: ownerSpawn.tmuxSessionName,
|
|
@@ -842,25 +1327,7 @@ export default class Harness extends Command {
|
|
|
842
1327
|
await writeSessionState(root, state);
|
|
843
1328
|
}
|
|
844
1329
|
}
|
|
845
|
-
|
|
846
|
-
const resolved = await resolveOwner(root, sessionId);
|
|
847
|
-
if (resolved.live && resolved.socketPath) {
|
|
848
|
-
ownerLive = true;
|
|
849
|
-
ownerBlockerReason = null;
|
|
850
|
-
handle.processHandle = {
|
|
851
|
-
kind: "runtime-owner",
|
|
852
|
-
ownerId: resolved.lease?.ownerId ?? null,
|
|
853
|
-
pid: resolved.lease?.pid ?? null,
|
|
854
|
-
};
|
|
855
|
-
handle.ownerHandle = {
|
|
856
|
-
leasePath,
|
|
857
|
-
endpoint: resolved.socketPath,
|
|
858
|
-
heartbeatAt: resolved.lease?.heartbeatAt ?? null,
|
|
859
|
-
};
|
|
860
|
-
state.handle = handle;
|
|
861
|
-
await writeSessionState(root, state);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
1330
|
+
// A live endpoint never proves a failed tmux launch safe: preserve the isolation/provenance blocker.
|
|
864
1331
|
if (ownerBlockerReason) {
|
|
865
1332
|
state.lifecycle = "blocked";
|
|
866
1333
|
state.blockers = [...state.blockers, ownerBlockerReason];
|
|
@@ -876,6 +1343,7 @@ export default class Harness extends Command {
|
|
|
876
1343
|
handle,
|
|
877
1344
|
ownerRuntime,
|
|
878
1345
|
preflight,
|
|
1346
|
+
...(ownerSocketKey ? { tmuxOwnerSocketKey: ownerSocketKey } : {}),
|
|
879
1347
|
...(ownerFallbackReason ? { ownerFallbackReason } : {}),
|
|
880
1348
|
...(ownerBlockerReason ? { reason: ownerBlockerReason } : {}),
|
|
881
1349
|
},
|