@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
|
@@ -1,14 +1,30 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import * as crypto from "node:crypto";
|
|
3
|
+
import * as fsSync from "node:fs";
|
|
4
|
+
import * as fs from "node:fs/promises";
|
|
5
|
+
import * as path from "node:path";
|
|
1
6
|
import { resolveSkcTmuxBinary } from "./psmux-detect";
|
|
7
|
+
import { tmuxRuntimeSessionPath } from "./session-layout";
|
|
8
|
+
import {
|
|
9
|
+
SKC_COORDINATOR_SESSION_ID_ENV,
|
|
10
|
+
SKC_COORDINATOR_SESSION_STATE_FILE_ENV,
|
|
11
|
+
SKC_TMUX_OWNER_GENERATION_ENV,
|
|
12
|
+
SKC_TMUX_OWNER_SERVER_KEY_ENV,
|
|
13
|
+
SKC_TMUX_OWNER_STATE_DIR_ENV,
|
|
14
|
+
} from "./session-state-sidecar";
|
|
2
15
|
import {
|
|
3
16
|
buildSkcTmuxExactOptionTarget,
|
|
4
17
|
buildSkcTmuxExactSessionTarget,
|
|
5
18
|
buildSkcTmuxProfileCommands,
|
|
6
19
|
buildSkcTmuxSessionName,
|
|
20
|
+
buildSkcTmuxSessionSlug,
|
|
7
21
|
buildSkcTmuxUntaggedSessionError,
|
|
8
22
|
normalizeTmuxCreatedAt,
|
|
9
23
|
resolveSkcTmuxCommand,
|
|
10
24
|
SKC_TMUX_BRANCH_OPTION,
|
|
11
25
|
SKC_TMUX_BRANCH_SLUG_OPTION,
|
|
26
|
+
SKC_TMUX_OWNER_GENERATION_OPTION,
|
|
27
|
+
SKC_TMUX_OWNER_SERVER_KEY_OPTION,
|
|
12
28
|
SKC_TMUX_PROFILE_OPTION,
|
|
13
29
|
SKC_TMUX_PROFILE_VALUE,
|
|
14
30
|
SKC_TMUX_PROJECT_OPTION,
|
|
@@ -16,6 +32,23 @@ import {
|
|
|
16
32
|
SKC_TMUX_SESSION_STATE_FILE_OPTION,
|
|
17
33
|
SKC_TMUX_VERSION_OPTION,
|
|
18
34
|
} from "./tmux-common";
|
|
35
|
+
import {
|
|
36
|
+
captureOwnerGenerationBaselineSync,
|
|
37
|
+
classifyCgroup,
|
|
38
|
+
closeExactTmuxOwner,
|
|
39
|
+
executeTmuxOwnerIsolationPlanSync,
|
|
40
|
+
isOwnerGenerationBaselineCurrentSync,
|
|
41
|
+
isValidOwnerVerdict,
|
|
42
|
+
lifecyclePaths,
|
|
43
|
+
type OwnerIsolationProbeSync,
|
|
44
|
+
type OwnerVerdict,
|
|
45
|
+
type PlanResponse,
|
|
46
|
+
planTmuxOwnerIsolationSync,
|
|
47
|
+
replaceOwnerGenerationSync,
|
|
48
|
+
type TmuxOwnerIsolationExecutionDependencies,
|
|
49
|
+
type TmuxOwnerIsolationExecutionResult,
|
|
50
|
+
type TmuxServerProof,
|
|
51
|
+
} from "./tmux-owner-isolation";
|
|
19
52
|
|
|
20
53
|
export interface SkcTmuxSessionStatus {
|
|
21
54
|
name: string;
|
|
@@ -30,6 +63,9 @@ export interface SkcTmuxSessionStatus {
|
|
|
30
63
|
sessionId?: string;
|
|
31
64
|
sessionStateFile?: string;
|
|
32
65
|
version?: string;
|
|
66
|
+
ownerGeneration?: string;
|
|
67
|
+
nativeSessionId?: string;
|
|
68
|
+
|
|
33
69
|
panePids: number[];
|
|
34
70
|
profile?: string;
|
|
35
71
|
}
|
|
@@ -42,6 +78,9 @@ export interface SkcTmuxSessionTagsForGc {
|
|
|
42
78
|
sessionId?: string;
|
|
43
79
|
sessionStateFile?: string;
|
|
44
80
|
version?: string;
|
|
81
|
+
ownerGeneration?: string;
|
|
82
|
+
nativeSessionId?: string;
|
|
83
|
+
|
|
45
84
|
createdAt?: string;
|
|
46
85
|
attached?: boolean;
|
|
47
86
|
panePids?: number[];
|
|
@@ -52,19 +91,88 @@ export interface SkcTmuxSessionsForGc {
|
|
|
52
91
|
untagged: SkcTmuxSessionStatus[];
|
|
53
92
|
}
|
|
54
93
|
|
|
94
|
+
export interface ProvenTmuxSessionIdentity {
|
|
95
|
+
nativeSessionId: string;
|
|
96
|
+
serverPid: number;
|
|
97
|
+
serverStartTime: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ExpectedSkcTmuxSessionIdentity {
|
|
101
|
+
nativeSessionId: string;
|
|
102
|
+
ownerGeneration: string;
|
|
103
|
+
sessionId: string;
|
|
104
|
+
sessionStateFile: string;
|
|
105
|
+
project: string;
|
|
106
|
+
createdAt: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ExactOwnerIdentity {
|
|
110
|
+
sessionId: string;
|
|
111
|
+
stateDir: string;
|
|
112
|
+
socketKey: string;
|
|
113
|
+
generation: string;
|
|
114
|
+
pid: number;
|
|
115
|
+
startTime: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ForceCloseOwnerDependencies {
|
|
119
|
+
resolveOwner(sessionName: string, env: NodeJS.ProcessEnv): Promise<ExactOwnerIdentity>;
|
|
120
|
+
signalTerm(pid: number): void;
|
|
121
|
+
readProcessStartTime(pid: number): Promise<string | null>;
|
|
122
|
+
cleanupSession(sessionTarget: string, env: NodeJS.ProcessEnv): void;
|
|
123
|
+
now(): Date;
|
|
124
|
+
sleep(ms: number): Promise<void>;
|
|
125
|
+
listPanePids(sessionName: string, env: NodeJS.ProcessEnv): number[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const FORCE_CLOSE_VERDICT_TIMEOUT_MS = 5_000;
|
|
129
|
+
const FORCE_CLOSE_VERDICT_POLL_MS = 50;
|
|
130
|
+
|
|
131
|
+
export type CreateOwnerIsolationTestDependencies = {
|
|
132
|
+
probe?: Partial<OwnerIsolationProbeSync>;
|
|
133
|
+
execute?: (plan: PlanResponse, deps: TmuxOwnerIsolationExecutionDependencies) => TmuxOwnerIsolationExecutionResult;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
let createOwnerIsolationTestDependencies: CreateOwnerIsolationTestDependencies | null = null;
|
|
137
|
+
let mutationServerProofTestDependency: ((tmuxCommand: string, env: NodeJS.ProcessEnv) => unknown) | null = null;
|
|
138
|
+
|
|
139
|
+
/** @internal Test-only seam; production create always uses live fail-closed probes. */
|
|
140
|
+
export function __setCreateOwnerIsolationForTests(dependencies: CreateOwnerIsolationTestDependencies | null): void {
|
|
141
|
+
createOwnerIsolationTestDependencies = dependencies;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** @internal Test-only seam; production mutations always use live fail-closed proofs. */
|
|
145
|
+
export function __setMutationServerProofForTests(
|
|
146
|
+
dependency: ((tmuxCommand: string, env: NodeJS.ProcessEnv) => unknown) | null,
|
|
147
|
+
): void {
|
|
148
|
+
mutationServerProofTestDependency = dependency;
|
|
149
|
+
}
|
|
150
|
+
|
|
55
151
|
function runTmux(args: string[], env: NodeJS.ProcessEnv = process.env): string {
|
|
56
152
|
const tmuxCommand = resolveSkcTmuxCommand(env);
|
|
57
|
-
const result = Bun.spawnSync([tmuxCommand, ...args], {
|
|
153
|
+
const result = Bun.spawnSync([tmuxCommand, ...args], {
|
|
154
|
+
stdout: "pipe",
|
|
155
|
+
stderr: "pipe",
|
|
156
|
+
env,
|
|
157
|
+
});
|
|
58
158
|
if (result.exitCode === 0) return result.stdout.toString();
|
|
59
159
|
throw new Error(result.stderr.toString().trim() || `tmux ${args.join(" ")} failed`);
|
|
60
160
|
}
|
|
161
|
+
function normalizeExactTmuxTarget(sessionTarget: string, env: NodeJS.ProcessEnv, kind: "session" | "option"): string {
|
|
162
|
+
if (sessionTarget.startsWith("$")) return sessionTarget;
|
|
163
|
+
return kind === "option"
|
|
164
|
+
? buildSkcTmuxExactOptionTarget(sessionTarget, { env })
|
|
165
|
+
: buildSkcTmuxExactSessionTarget(sessionTarget, { env });
|
|
166
|
+
}
|
|
61
167
|
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
168
|
+
function readExactSessionPanePids(sessionName: string, env: NodeJS.ProcessEnv): number[] {
|
|
169
|
+
return runTmux(
|
|
170
|
+
["list-panes", "-s", "-t", normalizeExactTmuxTarget(sessionName, env, "session"), "-F", "#{pane_pid}"],
|
|
171
|
+
env,
|
|
172
|
+
)
|
|
173
|
+
.split("\n")
|
|
174
|
+
.map(value => Number.parseInt(value.trim(), 10))
|
|
175
|
+
.filter(pid => Number.isSafeInteger(pid) && pid > 0);
|
|
68
176
|
}
|
|
69
177
|
|
|
70
178
|
function parseBooleanFlag(value: string | undefined): boolean {
|
|
@@ -91,8 +199,11 @@ function parseSessionLine(line: string): SkcTmuxSessionStatus | null {
|
|
|
91
199
|
project = "",
|
|
92
200
|
sessionId = "",
|
|
93
201
|
sessionStateFile = "",
|
|
202
|
+
ownerGeneration = "",
|
|
94
203
|
version = "",
|
|
204
|
+
nativeSessionId = "",
|
|
95
205
|
] = line.split("\t");
|
|
206
|
+
|
|
96
207
|
if (!name) return null;
|
|
97
208
|
return {
|
|
98
209
|
name,
|
|
@@ -112,6 +223,8 @@ function parseSessionLine(line: string): SkcTmuxSessionStatus | null {
|
|
|
112
223
|
sessionId: sessionId || undefined,
|
|
113
224
|
sessionStateFile: sessionStateFile || undefined,
|
|
114
225
|
version: version || undefined,
|
|
226
|
+
ownerGeneration: ownerGeneration || undefined,
|
|
227
|
+
nativeSessionId: nativeSessionId || undefined,
|
|
115
228
|
};
|
|
116
229
|
}
|
|
117
230
|
|
|
@@ -148,7 +261,8 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
|
|
|
148
261
|
if (!match) return line;
|
|
149
262
|
const [, name, windows, created] = match;
|
|
150
263
|
const createdEpoch = String(Math.floor(new Date(`${created} UTC`).getTime() / 1000) || 0);
|
|
151
|
-
|
|
264
|
+
|
|
265
|
+
return [name, windows, "0", createdEpoch, "", "", "0", "", "", "", "", "", "", "", "", ""].join("\t");
|
|
152
266
|
});
|
|
153
267
|
}
|
|
154
268
|
}
|
|
@@ -157,7 +271,8 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
|
|
|
157
271
|
|
|
158
272
|
function listSessionLines(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
159
273
|
return runListSessions(
|
|
160
|
-
`#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${SKC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{pane_pid}\t#{${SKC_TMUX_BRANCH_OPTION}}\t#{${SKC_TMUX_BRANCH_SLUG_OPTION}}\t#{${SKC_TMUX_PROJECT_OPTION}}\t#{${SKC_TMUX_SESSION_ID_OPTION}}\t#{${SKC_TMUX_SESSION_STATE_FILE_OPTION}}\t#{${SKC_TMUX_VERSION_OPTION}}`,
|
|
274
|
+
`#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${SKC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{pane_pid}\t#{${SKC_TMUX_BRANCH_OPTION}}\t#{${SKC_TMUX_BRANCH_SLUG_OPTION}}\t#{${SKC_TMUX_PROJECT_OPTION}}\t#{${SKC_TMUX_SESSION_ID_OPTION}}\t#{${SKC_TMUX_SESSION_STATE_FILE_OPTION}}\t#{${SKC_TMUX_OWNER_GENERATION_OPTION}}\t#{${SKC_TMUX_VERSION_OPTION}}\t#{session_id}`,
|
|
275
|
+
|
|
161
276
|
env,
|
|
162
277
|
);
|
|
163
278
|
}
|
|
@@ -242,40 +357,443 @@ export function statusSkcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv
|
|
|
242
357
|
export function createSkcTmuxSession(env: NodeJS.ProcessEnv = process.env): SkcTmuxSessionStatus {
|
|
243
358
|
const tmuxCommand = resolveSkcTmuxCommand(env);
|
|
244
359
|
const sessionName = buildSkcTmuxSessionName(env);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
const
|
|
360
|
+
const cwd = process.cwd();
|
|
361
|
+
const sessionId = env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || sessionName;
|
|
362
|
+
const stateFile =
|
|
363
|
+
env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim() ||
|
|
364
|
+
tmuxRuntimeSessionPath(cwd, env.SKC_SESSION_ID?.trim() || sessionId, buildSkcTmuxSessionSlug(sessionName));
|
|
365
|
+
const stateDir = path.dirname(stateFile);
|
|
366
|
+
const generation = crypto.randomUUID();
|
|
367
|
+
const childEnvironment: Record<string, string> = {
|
|
368
|
+
SKC_TMUX_LAUNCHED: "1",
|
|
369
|
+
[SKC_TMUX_OWNER_GENERATION_ENV]: generation,
|
|
370
|
+
[SKC_TMUX_OWNER_STATE_DIR_ENV]: stateDir,
|
|
371
|
+
[SKC_TMUX_OWNER_SERVER_KEY_ENV]: tmuxCommand,
|
|
372
|
+
[SKC_COORDINATOR_SESSION_ID_ENV]: sessionId,
|
|
373
|
+
[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]: stateFile,
|
|
374
|
+
};
|
|
375
|
+
const shellQuote = (value: string): string => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
376
|
+
const powershellQuote = (value: string): string => `'${value.replace(/'/g, "''")}'`;
|
|
377
|
+
const command =
|
|
378
|
+
process.platform === "win32"
|
|
379
|
+
? `${Object.entries(childEnvironment)
|
|
380
|
+
.map(([name, value]) => `$env:${name} = ${powershellQuote(value)}`)
|
|
381
|
+
.join("; ")}; skc`
|
|
382
|
+
: `exec env ${Object.entries(childEnvironment)
|
|
383
|
+
.map(([name, value]) => `${name}=${shellQuote(value)}`)
|
|
384
|
+
.join(" ")} skc`;
|
|
385
|
+
const tmuxArgv = [
|
|
386
|
+
tmuxCommand,
|
|
387
|
+
"new-session",
|
|
388
|
+
"-d",
|
|
389
|
+
"-s",
|
|
390
|
+
sessionName,
|
|
391
|
+
...(process.platform === "win32" ? [] : ["-P", "-F", "#{session_id}"]),
|
|
392
|
+
command,
|
|
393
|
+
];
|
|
394
|
+
function probeTmuxServer(tmuxCommand: string, env: NodeJS.ProcessEnv): TmuxServerProof {
|
|
395
|
+
if (process.platform !== "linux") {
|
|
396
|
+
return {
|
|
397
|
+
state: "safe",
|
|
398
|
+
pid: 1,
|
|
399
|
+
startTime: "not-applicable",
|
|
400
|
+
cgroup: { classification: "not_applicable" },
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
const result = Bun.spawnSync([tmuxCommand, "display-message", "-p", "#{pid}"], {
|
|
404
|
+
stdout: "pipe",
|
|
405
|
+
stderr: "pipe",
|
|
406
|
+
env,
|
|
407
|
+
});
|
|
408
|
+
if (result.exitCode !== 0) {
|
|
409
|
+
const diagnostic = result.stderr.toString();
|
|
410
|
+
return /no server running|failed to connect|error connecting/.test(diagnostic)
|
|
411
|
+
? { state: "absent" }
|
|
412
|
+
: { state: "unverifiable" };
|
|
413
|
+
}
|
|
414
|
+
const pid = Number(result.stdout.toString().trim());
|
|
415
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return { state: "unverifiable" };
|
|
416
|
+
try {
|
|
417
|
+
const stat = fsSync.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
418
|
+
const startTime = stat
|
|
419
|
+
.slice(stat.lastIndexOf(")") + 2)
|
|
420
|
+
.trim()
|
|
421
|
+
.split(/\s+/)[19];
|
|
422
|
+
if (!startTime) return { state: "unverifiable" };
|
|
423
|
+
const cgroup = classifyCgroup({
|
|
424
|
+
platform: process.platform,
|
|
425
|
+
cgroupText: fsSync.readFileSync(`/proc/${pid}/cgroup`, "utf8"),
|
|
426
|
+
});
|
|
427
|
+
return {
|
|
428
|
+
state:
|
|
429
|
+
cgroup.classification === "safe"
|
|
430
|
+
? "safe"
|
|
431
|
+
: cgroup.classification === "unsafe_service"
|
|
432
|
+
? "unsafe"
|
|
433
|
+
: "unverifiable",
|
|
434
|
+
pid,
|
|
435
|
+
startTime,
|
|
436
|
+
cgroup,
|
|
437
|
+
};
|
|
438
|
+
} catch {
|
|
439
|
+
return { state: "unverifiable" };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const probeServer = () => probeTmuxServer(tmuxCommand, env);
|
|
444
|
+
|
|
445
|
+
const testOwnerProbe = createOwnerIsolationTestDependencies?.probe;
|
|
446
|
+
const ownerProbe: OwnerIsolationProbeSync = {
|
|
447
|
+
readCallerCgroup:
|
|
448
|
+
testOwnerProbe?.readCallerCgroup ??
|
|
449
|
+
(() => {
|
|
450
|
+
try {
|
|
451
|
+
return fsSync.readFileSync("/proc/self/cgroup", "utf8");
|
|
452
|
+
} catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}),
|
|
456
|
+
probeServer: testOwnerProbe?.probeServer ?? probeServer,
|
|
457
|
+
recordAttempt:
|
|
458
|
+
testOwnerProbe?.recordAttempt ??
|
|
459
|
+
(({ attempt }) => {
|
|
460
|
+
const root = lifecyclePaths(stateDir, sessionId, generation).root;
|
|
461
|
+
const attemptFile = path.join(root, `attempt-${attempt.token}.json`);
|
|
462
|
+
fsSync.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
463
|
+
const descriptor = fsSync.openSync(attemptFile, "wx", 0o600);
|
|
464
|
+
try {
|
|
465
|
+
fsSync.writeFileSync(
|
|
466
|
+
descriptor,
|
|
467
|
+
`${JSON.stringify({
|
|
468
|
+
schema_version: 1,
|
|
469
|
+
generation,
|
|
470
|
+
session_id: sessionId,
|
|
471
|
+
...attempt,
|
|
472
|
+
created_at: new Date().toISOString(),
|
|
473
|
+
})}\n`,
|
|
474
|
+
);
|
|
475
|
+
fsSync.fsyncSync(descriptor);
|
|
476
|
+
} finally {
|
|
477
|
+
fsSync.closeSync(descriptor);
|
|
478
|
+
}
|
|
479
|
+
const directory = fsSync.openSync(root, "r");
|
|
480
|
+
try {
|
|
481
|
+
fsSync.fsyncSync(directory);
|
|
482
|
+
} finally {
|
|
483
|
+
fsSync.closeSync(directory);
|
|
484
|
+
}
|
|
485
|
+
}),
|
|
486
|
+
};
|
|
487
|
+
if (resolveSkcTmuxBinary({ env }).isPsmux)
|
|
488
|
+
throw new Error("skc_tmux_owner_isolation_native_session_identity_unavailable");
|
|
489
|
+
|
|
490
|
+
const baseline = captureOwnerGenerationBaselineSync(stateDir, sessionId);
|
|
491
|
+
const ownerPlan = planTmuxOwnerIsolationSync(
|
|
492
|
+
{
|
|
493
|
+
schema_version: 1,
|
|
494
|
+
op: "plan",
|
|
495
|
+
platform: process.platform,
|
|
496
|
+
session_id: sessionId,
|
|
497
|
+
owner_generation: generation,
|
|
498
|
+
baseline,
|
|
499
|
+
cwd,
|
|
500
|
+
state_dir: stateDir,
|
|
501
|
+
socket_key: tmuxCommand,
|
|
502
|
+
tmux_argv: tmuxArgv,
|
|
503
|
+
},
|
|
504
|
+
ownerProbe,
|
|
505
|
+
);
|
|
506
|
+
if (!ownerPlan.ok) throw new Error(`skc_tmux_owner_isolation_${ownerPlan.code}:${ownerPlan.diagnostic}`);
|
|
507
|
+
|
|
508
|
+
const outcome = (createOwnerIsolationTestDependencies?.execute ?? executeTmuxOwnerIsolationPlanSync)(ownerPlan, {
|
|
509
|
+
socketKey: tmuxCommand,
|
|
510
|
+
spawn: (argv, stdinLine) => {
|
|
511
|
+
const result = stdinLine
|
|
512
|
+
? Bun.spawnSync(argv, {
|
|
513
|
+
stdout: "pipe",
|
|
514
|
+
stderr: "pipe",
|
|
515
|
+
stdin: Buffer.from(stdinLine),
|
|
516
|
+
env,
|
|
517
|
+
})
|
|
518
|
+
: Bun.spawnSync(argv, { stdout: "pipe", stderr: "pipe", env });
|
|
519
|
+
return { exitCode: result.exitCode, stdout: result.stdout.toString() };
|
|
520
|
+
},
|
|
521
|
+
probeServer: ownerProbe.probeServer,
|
|
522
|
+
isCurrentGeneration: () => isOwnerGenerationBaselineCurrentSync(stateDir, sessionId, baseline),
|
|
523
|
+
cleanupSpawned: ({ execution, nativeSessionId, server }) => {
|
|
524
|
+
if (!server.pid || !server.startTime) throw new Error("skc_tmux_cleanup_target_changed");
|
|
525
|
+
cleanupExactCreatedTmuxSession(
|
|
526
|
+
nativeSessionId,
|
|
527
|
+
execution.attempt_session,
|
|
528
|
+
tmuxCommand,
|
|
529
|
+
env,
|
|
530
|
+
server.pid,
|
|
531
|
+
server.startTime,
|
|
532
|
+
);
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
if (!outcome.ok) throw new Error(`skc_tmux_owner_isolation_${outcome.code}:${outcome.diagnostic}`);
|
|
536
|
+
|
|
537
|
+
const nativeSessionId = outcome.native_session_id;
|
|
538
|
+
if (!nativeSessionId) throw new Error("skc_tmux_owner_isolation_native_session_identity_unavailable");
|
|
539
|
+
const server = requireSafeTmuxServerForMutation(tmuxCommand, env);
|
|
540
|
+
if (server.pid !== outcome.server_pid || server.startTime !== outcome.server_start_time)
|
|
541
|
+
throw new Error("skc_tmux_owner_changed_after_create");
|
|
542
|
+
if (!isNativeTmuxSessionBoundToName(nativeSessionId, sessionName, env))
|
|
543
|
+
throw new Error("skc_tmux_owner_changed_after_create");
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
tagCreatedTmuxSession(
|
|
547
|
+
nativeSessionId,
|
|
548
|
+
sessionName,
|
|
549
|
+
outcome.server_pid,
|
|
550
|
+
env,
|
|
551
|
+
{
|
|
552
|
+
sessionId,
|
|
553
|
+
sessionStateFile: stateFile,
|
|
554
|
+
ownerGeneration: generation,
|
|
555
|
+
ownerServerKey: tmuxCommand,
|
|
556
|
+
version: env.npm_package_version ?? null,
|
|
557
|
+
},
|
|
558
|
+
tmuxCommand,
|
|
559
|
+
);
|
|
560
|
+
} catch (tagError) {
|
|
561
|
+
try {
|
|
562
|
+
cleanupExactCreatedTmuxSession(
|
|
563
|
+
nativeSessionId,
|
|
564
|
+
sessionName,
|
|
565
|
+
tmuxCommand,
|
|
566
|
+
env,
|
|
567
|
+
outcome.server_pid,
|
|
568
|
+
outcome.server_start_time,
|
|
569
|
+
);
|
|
570
|
+
} catch (cleanupError) {
|
|
571
|
+
throw new AggregateError([tagError, cleanupError], "skc_tmux_profile_tag_failed_cleanup_failed");
|
|
572
|
+
}
|
|
573
|
+
throw tagError;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (nativeSessionId) {
|
|
577
|
+
const firstServer = requireSafeTmuxServerForMutation(tmuxCommand, env);
|
|
578
|
+
if (firstServer.pid !== outcome.server_pid || firstServer.startTime !== outcome.server_start_time)
|
|
579
|
+
throw new Error("skc_tmux_owner_changed_after_create");
|
|
580
|
+
const status = statusSkcTmuxSessionByNativeId(nativeSessionId, env);
|
|
581
|
+
const finalServer = requireSafeTmuxServerForMutation(tmuxCommand, env);
|
|
582
|
+
if (finalServer.pid !== firstServer.pid || finalServer.startTime !== firstServer.startTime)
|
|
583
|
+
throw new Error("skc_tmux_owner_changed_after_create");
|
|
584
|
+
try {
|
|
585
|
+
replaceOwnerGenerationSync(stateDir, sessionId, generation, baseline);
|
|
586
|
+
} catch (publicationError) {
|
|
587
|
+
try {
|
|
588
|
+
cleanupExactCreatedTmuxSession(
|
|
589
|
+
nativeSessionId,
|
|
590
|
+
sessionName,
|
|
591
|
+
tmuxCommand,
|
|
592
|
+
env,
|
|
593
|
+
outcome.server_pid,
|
|
594
|
+
outcome.server_start_time,
|
|
595
|
+
);
|
|
596
|
+
} catch (cleanupError) {
|
|
597
|
+
throw new AggregateError(
|
|
598
|
+
[publicationError, cleanupError],
|
|
599
|
+
"skc_tmux_owner_generation_publish_failed_cleanup_failed",
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
throw publicationError;
|
|
603
|
+
}
|
|
604
|
+
return status;
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
replaceOwnerGenerationSync(stateDir, sessionId, generation, baseline);
|
|
608
|
+
} catch (publicationError) {
|
|
609
|
+
try {
|
|
610
|
+
cleanupExactCreatedTmuxSession(
|
|
611
|
+
nativeSessionId,
|
|
612
|
+
sessionName,
|
|
613
|
+
tmuxCommand,
|
|
614
|
+
env,
|
|
615
|
+
outcome.server_pid,
|
|
616
|
+
outcome.server_start_time,
|
|
617
|
+
);
|
|
618
|
+
} catch (cleanupError) {
|
|
619
|
+
throw new AggregateError(
|
|
620
|
+
[publicationError, cleanupError],
|
|
621
|
+
"skc_tmux_owner_generation_publish_failed_cleanup_failed",
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
throw publicationError;
|
|
625
|
+
}
|
|
626
|
+
return statusSkcTmuxSession(sessionName, env);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function statusSkcTmuxSessionByNativeId(nativeSessionId: string, env: NodeJS.ProcessEnv): SkcTmuxSessionStatus {
|
|
630
|
+
const name = runTmux(
|
|
631
|
+
["display-message", "-p", "-t", normalizeExactTmuxTarget(nativeSessionId, env, "option"), "#{session_name}"],
|
|
632
|
+
env,
|
|
633
|
+
).trim();
|
|
634
|
+
if (!name || readNativeTmuxSessionId(nativeSessionId, env) !== nativeSessionId)
|
|
635
|
+
throw new Error(`skc_tmux_owner_changed:${nativeSessionId}`);
|
|
636
|
+
return statusSkcTmuxSession(name, env);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function tmuxCommandArgument(value: string): string {
|
|
640
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("$", "\\$").replaceAll("`", "\\`")}"`;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function guardedTmuxSessionPredicate(
|
|
644
|
+
expectedPid: number,
|
|
645
|
+
nativeSessionId: string,
|
|
646
|
+
sessionName: string,
|
|
647
|
+
expectedOwnerGeneration?: string,
|
|
648
|
+
): string {
|
|
649
|
+
const ownerGenerationPredicate = expectedOwnerGeneration
|
|
650
|
+
? `#{==:#{${SKC_TMUX_OWNER_GENERATION_OPTION}},${expectedOwnerGeneration}}`
|
|
651
|
+
: "1";
|
|
652
|
+
return `#{&&:#{==:#{pid},${expectedPid}},#{&&:#{==:#{session_id},${nativeSessionId}},#{&&:#{==:#{session_name},${sessionName}},${ownerGenerationPredicate}}}}`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function runGuardedTmuxSessionCommand(
|
|
656
|
+
nativeSessionId: string,
|
|
657
|
+
sessionName: string,
|
|
658
|
+
expectedPid: number,
|
|
659
|
+
env: NodeJS.ProcessEnv,
|
|
660
|
+
thenCommand: string,
|
|
661
|
+
expectedOwnerGeneration?: string,
|
|
662
|
+
): void {
|
|
663
|
+
const result = runTmux(
|
|
664
|
+
[
|
|
665
|
+
"if-shell",
|
|
666
|
+
"-t",
|
|
667
|
+
normalizeExactTmuxTarget(nativeSessionId, env, "session"),
|
|
668
|
+
"-F",
|
|
669
|
+
guardedTmuxSessionPredicate(expectedPid, nativeSessionId, sessionName, expectedOwnerGeneration),
|
|
670
|
+
`${thenCommand} ; display-message -p __skc_tmux_guarded_mutation_ok__`,
|
|
671
|
+
"display-message -p __skc_tmux_guarded_mutation_refused__",
|
|
672
|
+
],
|
|
673
|
+
env,
|
|
674
|
+
).trim();
|
|
675
|
+
if (result !== "__skc_tmux_guarded_mutation_ok__") throw new Error("skc_tmux_cleanup_target_changed");
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function tagCreatedTmuxSession(
|
|
679
|
+
nativeSessionId: string,
|
|
680
|
+
sessionName: string,
|
|
681
|
+
expectedPid: number,
|
|
682
|
+
env: NodeJS.ProcessEnv,
|
|
683
|
+
metadata: {
|
|
684
|
+
branch?: string | null;
|
|
685
|
+
branchSlug?: string | null;
|
|
686
|
+
project?: string | null;
|
|
687
|
+
sessionId?: string | null;
|
|
688
|
+
sessionStateFile?: string | null;
|
|
689
|
+
ownerGeneration?: string | null;
|
|
690
|
+
ownerServerKey?: string | null;
|
|
691
|
+
version?: string | null;
|
|
692
|
+
},
|
|
693
|
+
tmuxCommand: string,
|
|
694
|
+
): void {
|
|
695
|
+
const target = `${nativeSessionId}:`;
|
|
696
|
+
const commands = buildSkcTmuxProfileCommands(target, env, metadata, { tmuxCommand })
|
|
697
|
+
.map(command => command.args.map(tmuxCommandArgument).join(" "))
|
|
698
|
+
.join(" ; ");
|
|
699
|
+
runGuardedTmuxSessionCommand(nativeSessionId, sessionName, expectedPid, env, commands);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function cleanupExactCreatedTmuxSession(
|
|
703
|
+
nativeSessionId: string,
|
|
704
|
+
sessionName: string,
|
|
705
|
+
tmuxCommand: string,
|
|
706
|
+
env: NodeJS.ProcessEnv,
|
|
707
|
+
expectedPid: number,
|
|
708
|
+
expectedStartTime: string,
|
|
709
|
+
): void {
|
|
710
|
+
const server = requireSafeTmuxServerForMutation(tmuxCommand, env);
|
|
711
|
+
if (server.pid !== expectedPid || server.startTime !== expectedStartTime)
|
|
712
|
+
throw new Error("skc_tmux_cleanup_target_changed");
|
|
713
|
+
runGuardedTmuxSessionCommand(
|
|
714
|
+
nativeSessionId,
|
|
715
|
+
sessionName,
|
|
716
|
+
expectedPid,
|
|
717
|
+
env,
|
|
718
|
+
`kill-session -t ${tmuxCommandArgument(normalizeExactTmuxTarget(nativeSessionId, env, "session"))}`,
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function requireSafeTmuxServerForMutation(
|
|
723
|
+
tmuxCommand: string,
|
|
724
|
+
env: NodeJS.ProcessEnv,
|
|
725
|
+
): { pid: number; startTime: string } {
|
|
726
|
+
if (mutationServerProofTestDependency) {
|
|
727
|
+
const proof = mutationServerProofTestDependency(tmuxCommand, env);
|
|
728
|
+
if (
|
|
729
|
+
proof &&
|
|
730
|
+
typeof proof === "object" &&
|
|
731
|
+
Number.isSafeInteger((proof as { pid?: unknown }).pid) &&
|
|
732
|
+
typeof (proof as { startTime?: unknown }).startTime === "string"
|
|
733
|
+
)
|
|
734
|
+
return proof as { pid: number; startTime: string };
|
|
735
|
+
return { pid: 1, startTime: "test" };
|
|
736
|
+
}
|
|
737
|
+
if (process.platform !== "linux") return { pid: 1, startTime: "not-applicable" };
|
|
738
|
+
const result = Bun.spawnSync([tmuxCommand, "display-message", "-p", "#{pid}"], {
|
|
253
739
|
stdout: "pipe",
|
|
254
740
|
stderr: "pipe",
|
|
255
741
|
env,
|
|
256
742
|
});
|
|
257
|
-
if (
|
|
743
|
+
if (result.exitCode !== 0) throw new Error("skc_tmux_owner_isolation_server_unverifiable");
|
|
744
|
+
const pid = Number(result.stdout.toString().trim());
|
|
745
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error("skc_tmux_owner_isolation_server_unverifiable");
|
|
258
746
|
try {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
747
|
+
const stat = fsSync.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
748
|
+
const startTime = stat
|
|
749
|
+
.slice(stat.lastIndexOf(")") + 2)
|
|
750
|
+
.trim()
|
|
751
|
+
.split(/\s+/)[19];
|
|
752
|
+
const cgroup = classifyCgroup({
|
|
753
|
+
platform: process.platform,
|
|
754
|
+
cgroupText: fsSync.readFileSync(`/proc/${pid}/cgroup`, "utf8"),
|
|
755
|
+
});
|
|
756
|
+
if (!startTime || cgroup.classification !== "safe")
|
|
757
|
+
throw new Error(
|
|
758
|
+
`skc_tmux_owner_isolation_${cgroup.classification === "unsafe_service" ? "server_unsafe" : "server_unverifiable"}`,
|
|
759
|
+
);
|
|
760
|
+
return { pid, startTime };
|
|
269
761
|
} catch (error) {
|
|
270
|
-
|
|
271
|
-
throw
|
|
762
|
+
if (error instanceof Error && error.message.startsWith("skc_tmux_owner_isolation_")) throw error;
|
|
763
|
+
throw new Error("skc_tmux_owner_isolation_server_unverifiable");
|
|
272
764
|
}
|
|
273
|
-
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** Proves a managed reusable name still resolves to one immutable session on one server. */
|
|
768
|
+
export function proveSkcTmuxSessionMutationTarget(
|
|
769
|
+
sessionName: string,
|
|
770
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
771
|
+
): ProvenTmuxSessionIdentity {
|
|
772
|
+
const session = statusSkcTmuxSession(sessionName, env);
|
|
773
|
+
if (readProfileForExactTarget(session.name, env) !== SKC_TMUX_PROFILE_VALUE)
|
|
774
|
+
throw new Error(`skc_tmux_session_not_managed:${sessionName}`);
|
|
775
|
+
const firstServer = requireSafeTmuxServerForMutation(resolveSkcTmuxCommand(env), env);
|
|
776
|
+
if (resolveSkcTmuxBinary({ env }).isPsmux) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
|
|
777
|
+
const nativeSessionId = readNativeTmuxSessionId(session.name, env);
|
|
778
|
+
if (!nativeSessionId) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
|
|
779
|
+
if (
|
|
780
|
+
readNativeTmuxSessionId(nativeSessionId, env) !== nativeSessionId ||
|
|
781
|
+
readProfileForExactTarget(nativeSessionId, env) !== SKC_TMUX_PROFILE_VALUE
|
|
782
|
+
)
|
|
783
|
+
throw new Error(`skc_tmux_owner_changed:${sessionName}`);
|
|
784
|
+
const finalServer = requireSafeTmuxServerForMutation(resolveSkcTmuxCommand(env), env);
|
|
785
|
+
if (finalServer.pid !== firstServer.pid || finalServer.startTime !== firstServer.startTime)
|
|
786
|
+
throw new Error(`skc_tmux_owner_changed:${sessionName}`);
|
|
787
|
+
return {
|
|
788
|
+
nativeSessionId,
|
|
789
|
+
serverPid: finalServer.pid,
|
|
790
|
+
serverStartTime: finalServer.startTime,
|
|
791
|
+
};
|
|
274
792
|
}
|
|
275
793
|
|
|
276
794
|
function readProfileForExactTarget(sessionName: string, env: NodeJS.ProcessEnv): string {
|
|
277
795
|
const raw = runTmux(
|
|
278
|
-
["show-options", "-qv", "-t",
|
|
796
|
+
["show-options", "-qv", "-t", normalizeExactTmuxTarget(sessionName, env, "option"), SKC_TMUX_PROFILE_OPTION],
|
|
279
797
|
env,
|
|
280
798
|
).trim();
|
|
281
799
|
// tmux returns just the value; psmux returns `key value`. Strip the
|
|
@@ -291,7 +809,7 @@ function readProfileForExactTarget(sessionName: string, env: NodeJS.ProcessEnv):
|
|
|
291
809
|
function readExactOptionForGc(sessionName: string, option: string, env: NodeJS.ProcessEnv): string | undefined {
|
|
292
810
|
try {
|
|
293
811
|
const raw = runTmux(
|
|
294
|
-
["show-options", "-qv", "-t",
|
|
812
|
+
["show-options", "-qv", "-t", normalizeExactTmuxTarget(sessionName, env, "option"), option],
|
|
295
813
|
env,
|
|
296
814
|
).trim();
|
|
297
815
|
if (!raw) return undefined;
|
|
@@ -316,6 +834,38 @@ function readExactOptionForGc(sessionName: string, option: string, env: NodeJS.P
|
|
|
316
834
|
}
|
|
317
835
|
}
|
|
318
836
|
|
|
837
|
+
function readNativeTmuxSessionId(sessionTarget: string, env: NodeJS.ProcessEnv): string | undefined {
|
|
838
|
+
if (resolveSkcTmuxBinary({ env }).isPsmux) return undefined;
|
|
839
|
+
try {
|
|
840
|
+
const sessionId = runTmux(
|
|
841
|
+
["display-message", "-p", "-t", normalizeExactTmuxTarget(sessionTarget, env, "option"), "#{session_id}"],
|
|
842
|
+
env,
|
|
843
|
+
).trim();
|
|
844
|
+
return sessionId || undefined;
|
|
845
|
+
} catch {
|
|
846
|
+
return undefined;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function isNativeTmuxSessionBoundToName(nativeSessionId: string, sessionName: string, env: NodeJS.ProcessEnv): boolean {
|
|
851
|
+
try {
|
|
852
|
+
return (
|
|
853
|
+
runTmux(
|
|
854
|
+
[
|
|
855
|
+
"display-message",
|
|
856
|
+
"-p",
|
|
857
|
+
"-t",
|
|
858
|
+
normalizeExactTmuxTarget(sessionName, env, "option"),
|
|
859
|
+
"#{session_id}\t#{session_name}",
|
|
860
|
+
],
|
|
861
|
+
env,
|
|
862
|
+
).trim() === `${nativeSessionId}\t${sessionName}`
|
|
863
|
+
);
|
|
864
|
+
} catch {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
319
869
|
function hydrateSessionFromExactOptions(session: SkcTmuxSessionStatus, env: NodeJS.ProcessEnv): SkcTmuxSessionStatus {
|
|
320
870
|
if (session.profile === SKC_TMUX_PROFILE_VALUE) return session;
|
|
321
871
|
const profile = readExactOptionForGc(session.name, SKC_TMUX_PROFILE_OPTION, env);
|
|
@@ -329,6 +879,9 @@ function hydrateSessionFromExactOptions(session: SkcTmuxSessionStatus, env: Node
|
|
|
329
879
|
sessionId: session.sessionId ?? readExactOptionForGc(session.name, SKC_TMUX_SESSION_ID_OPTION, env),
|
|
330
880
|
sessionStateFile:
|
|
331
881
|
session.sessionStateFile ?? readExactOptionForGc(session.name, SKC_TMUX_SESSION_STATE_FILE_OPTION, env),
|
|
882
|
+
ownerGeneration:
|
|
883
|
+
session.ownerGeneration ?? readExactOptionForGc(session.name, SKC_TMUX_OWNER_GENERATION_OPTION, env),
|
|
884
|
+
|
|
332
885
|
version: session.version ?? readExactOptionForGc(session.name, SKC_TMUX_VERSION_OPTION, env),
|
|
333
886
|
};
|
|
334
887
|
}
|
|
@@ -347,71 +900,321 @@ export function readTmuxSessionTagsForGc(
|
|
|
347
900
|
sessionId: readExactOptionForGc(sessionName, SKC_TMUX_SESSION_ID_OPTION, env),
|
|
348
901
|
sessionStateFile: readExactOptionForGc(sessionName, SKC_TMUX_SESSION_STATE_FILE_OPTION, env),
|
|
349
902
|
version: readExactOptionForGc(sessionName, SKC_TMUX_VERSION_OPTION, env),
|
|
903
|
+
ownerGeneration: readExactOptionForGc(sessionName, SKC_TMUX_OWNER_GENERATION_OPTION, env),
|
|
904
|
+
nativeSessionId: session?.nativeSessionId,
|
|
350
905
|
createdAt: session?.createdAt,
|
|
351
906
|
attached: session?.attached,
|
|
352
907
|
panePids: session?.panePids,
|
|
353
908
|
};
|
|
354
909
|
}
|
|
355
910
|
|
|
356
|
-
export function removeSkcTmuxSession(
|
|
911
|
+
export function removeSkcTmuxSession(
|
|
912
|
+
sessionName: string,
|
|
913
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
914
|
+
expectedIdentity?: ExpectedSkcTmuxSessionIdentity,
|
|
915
|
+
): SkcTmuxSessionStatus {
|
|
357
916
|
const session = statusSkcTmuxSession(sessionName, env);
|
|
358
917
|
if (session.attached || session.panePids.length > 0) {
|
|
359
918
|
throw new Error(`skc_tmux_session_live:${sessionName}`);
|
|
360
919
|
}
|
|
920
|
+
if (
|
|
921
|
+
expectedIdentity &&
|
|
922
|
+
(session.nativeSessionId !== expectedIdentity.nativeSessionId ||
|
|
923
|
+
session.ownerGeneration !== expectedIdentity.ownerGeneration ||
|
|
924
|
+
session.sessionId !== expectedIdentity.sessionId ||
|
|
925
|
+
session.sessionStateFile !== expectedIdentity.sessionStateFile ||
|
|
926
|
+
session.project !== expectedIdentity.project ||
|
|
927
|
+
session.createdAt !== expectedIdentity.createdAt)
|
|
928
|
+
)
|
|
929
|
+
throw new Error(`skc_tmux_owner_changed:${sessionName}`);
|
|
361
930
|
if (readProfileForExactTarget(session.name, env) !== SKC_TMUX_PROFILE_VALUE) {
|
|
362
931
|
throw new Error(`skc_tmux_session_not_managed:${sessionName}`);
|
|
363
932
|
}
|
|
364
|
-
|
|
933
|
+
if (resolveSkcTmuxBinary({ env }).isPsmux) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
|
|
934
|
+
const nativeSessionId = readNativeTmuxSessionId(session.name, env);
|
|
935
|
+
if (!nativeSessionId) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
|
|
936
|
+
if (expectedIdentity && nativeSessionId !== expectedIdentity.nativeSessionId)
|
|
937
|
+
throw new Error(`skc_tmux_owner_changed:${sessionName}`);
|
|
938
|
+
if (
|
|
939
|
+
expectedIdentity &&
|
|
940
|
+
readExactOptionForGc(session.name, SKC_TMUX_OWNER_GENERATION_OPTION, env) !== expectedIdentity.ownerGeneration
|
|
941
|
+
)
|
|
942
|
+
throw new Error(`skc_tmux_owner_changed:${sessionName}`);
|
|
943
|
+
const firstServer = requireSafeTmuxServerForMutation(resolveSkcTmuxCommand(env), env);
|
|
944
|
+
if (
|
|
945
|
+
readNativeTmuxSessionId(nativeSessionId, env) !== nativeSessionId ||
|
|
946
|
+
readProfileForExactTarget(nativeSessionId, env) !== SKC_TMUX_PROFILE_VALUE
|
|
947
|
+
)
|
|
948
|
+
throw new Error(`skc_tmux_owner_changed:${sessionName}`);
|
|
949
|
+
const finalServer = requireSafeTmuxServerForMutation(resolveSkcTmuxCommand(env), env);
|
|
950
|
+
if (finalServer.pid !== firstServer.pid || finalServer.startTime !== firstServer.startTime)
|
|
951
|
+
throw new Error(`skc_tmux_owner_changed:${sessionName}`);
|
|
952
|
+
runGuardedTmuxSessionCommand(
|
|
953
|
+
nativeSessionId,
|
|
954
|
+
session.name,
|
|
955
|
+
finalServer.pid,
|
|
956
|
+
env,
|
|
957
|
+
`kill-session -t '${nativeSessionId}'`,
|
|
958
|
+
expectedIdentity?.ownerGeneration,
|
|
959
|
+
);
|
|
365
960
|
return session;
|
|
366
961
|
}
|
|
367
962
|
|
|
963
|
+
async function readProcessStartTime(pid: number): Promise<string | null> {
|
|
964
|
+
try {
|
|
965
|
+
const stat = await fs.readFile(`/proc/${pid}/stat`, "utf8");
|
|
966
|
+
const close = stat.lastIndexOf(")");
|
|
967
|
+
const fields = stat
|
|
968
|
+
.slice(close + 2)
|
|
969
|
+
.trim()
|
|
970
|
+
.split(/\s+/);
|
|
971
|
+
return fields[19] ?? null;
|
|
972
|
+
} catch {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
async function readCurrentGeneration(stateDir: string, sessionId: string): Promise<string | null> {
|
|
978
|
+
try {
|
|
979
|
+
const value: unknown = JSON.parse(
|
|
980
|
+
await fs.readFile(path.join(stateDir, sessionId, "owner-lifecycle", "generation.json"), "utf8"),
|
|
981
|
+
);
|
|
982
|
+
return typeof value === "object" &&
|
|
983
|
+
value !== null &&
|
|
984
|
+
typeof (value as { generation?: unknown }).generation === "string"
|
|
985
|
+
? (value as { generation: string }).generation
|
|
986
|
+
: null;
|
|
987
|
+
} catch {
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
async function resolveExactOwner(
|
|
993
|
+
sessionName: string,
|
|
994
|
+
env: NodeJS.ProcessEnv,
|
|
995
|
+
exactPanePid: number,
|
|
996
|
+
): Promise<ExactOwnerIdentity> {
|
|
997
|
+
const session = statusSkcTmuxSession(sessionName, env);
|
|
998
|
+
const sessionId = readExactOptionForGc(session.name, SKC_TMUX_SESSION_ID_OPTION, env);
|
|
999
|
+
const stateFile = readExactOptionForGc(session.name, SKC_TMUX_SESSION_STATE_FILE_OPTION, env);
|
|
1000
|
+
const ownerGeneration = readExactOptionForGc(session.name, SKC_TMUX_OWNER_GENERATION_OPTION, env);
|
|
1001
|
+
const ownerServerKey = readExactOptionForGc(session.name, SKC_TMUX_OWNER_SERVER_KEY_OPTION, env);
|
|
1002
|
+
|
|
1003
|
+
if (!sessionId || !stateFile) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
|
|
1004
|
+
const stateDir = path.dirname(stateFile);
|
|
1005
|
+
const generation = await readCurrentGeneration(stateDir, sessionId);
|
|
1006
|
+
const pid = exactPanePid;
|
|
1007
|
+
const startTime = await readProcessStartTime(pid);
|
|
1008
|
+
if (!generation || !ownerGeneration || ownerGeneration !== generation || !ownerServerKey || !startTime)
|
|
1009
|
+
throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
|
|
1010
|
+
|
|
1011
|
+
return {
|
|
1012
|
+
sessionId,
|
|
1013
|
+
stateDir,
|
|
1014
|
+
socketKey: ownerServerKey,
|
|
1015
|
+
generation,
|
|
1016
|
+
pid,
|
|
1017
|
+
startTime,
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
async function requireUnchangedOwnerForCompatibilityCleanup(
|
|
1022
|
+
sessionName: string,
|
|
1023
|
+
nativeSessionId: string,
|
|
1024
|
+
env: NodeJS.ProcessEnv,
|
|
1025
|
+
identity: ExactOwnerIdentity,
|
|
1026
|
+
initialStateFile: string,
|
|
1027
|
+
initialServer: { pid: number; startTime: string },
|
|
1028
|
+
listPanePids: (sessionName: string, env: NodeJS.ProcessEnv) => number[],
|
|
1029
|
+
readStartTime: (pid: number) => Promise<string | null>,
|
|
1030
|
+
): Promise<void> {
|
|
1031
|
+
try {
|
|
1032
|
+
const currentNativeSessionId = readNativeTmuxSessionId(nativeSessionId, env);
|
|
1033
|
+
if (!currentNativeSessionId) {
|
|
1034
|
+
if (readNativeTmuxSessionId(sessionName, env)) throw new Error(`skc_tmux_owner_changed:${sessionName}`);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const currentServer = requireSafeTmuxServerForMutation(resolveSkcTmuxCommand(env), env);
|
|
1038
|
+
const panePids = listPanePids(nativeSessionId, env);
|
|
1039
|
+
let currentPaneStartTime: string | null;
|
|
1040
|
+
try {
|
|
1041
|
+
currentPaneStartTime = await readStartTime(identity.pid);
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1044
|
+
currentPaneStartTime = null;
|
|
1045
|
+
}
|
|
1046
|
+
const mismatches = [
|
|
1047
|
+
currentServer.pid !== initialServer.pid ? "server_pid" : null,
|
|
1048
|
+
currentServer.startTime !== initialServer.startTime ? "server_start" : null,
|
|
1049
|
+
currentNativeSessionId !== nativeSessionId ? "native_id" : null,
|
|
1050
|
+
readProfileForExactTarget(nativeSessionId, env) !== SKC_TMUX_PROFILE_VALUE ? "profile" : null,
|
|
1051
|
+
panePids.length > 1 ? "pane_count" : null,
|
|
1052
|
+
panePids.length === 1 && panePids[0] !== identity.pid ? "pane_pid" : null,
|
|
1053
|
+
currentPaneStartTime !== null && currentPaneStartTime !== identity.startTime ? "pane_start" : null,
|
|
1054
|
+
readExactOptionForGc(nativeSessionId, SKC_TMUX_SESSION_ID_OPTION, env) !== identity.sessionId
|
|
1055
|
+
? "session_id"
|
|
1056
|
+
: null,
|
|
1057
|
+
readExactOptionForGc(nativeSessionId, SKC_TMUX_SESSION_STATE_FILE_OPTION, env) !== initialStateFile
|
|
1058
|
+
? "state_file"
|
|
1059
|
+
: null,
|
|
1060
|
+
readExactOptionForGc(nativeSessionId, SKC_TMUX_OWNER_GENERATION_OPTION, env) !== identity.generation
|
|
1061
|
+
? "generation"
|
|
1062
|
+
: null,
|
|
1063
|
+
readExactOptionForGc(nativeSessionId, SKC_TMUX_OWNER_SERVER_KEY_OPTION, env) !== identity.socketKey
|
|
1064
|
+
? "server_key"
|
|
1065
|
+
: null,
|
|
1066
|
+
].filter((value): value is string => value !== null);
|
|
1067
|
+
if (mismatches.length > 0) throw new Error(`skc_tmux_owner_changed:${sessionName}:${mismatches.join(",")}`);
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
if (!readNativeTmuxSessionId(nativeSessionId, env)) {
|
|
1070
|
+
if (readNativeTmuxSessionId(sessionName, env)) throw new Error(`skc_tmux_owner_changed:${sessionName}`);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
if (error instanceof Error && error.message.startsWith(`skc_tmux_owner_changed:${sessionName}`))
|
|
1074
|
+
throw new Error(`skc_tmux_owner_changed:${sessionName}`);
|
|
1075
|
+
throw new Error(`skc_tmux_owner_changed:${sessionName}`);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
async function waitForExpectedVerdict(
|
|
1080
|
+
identity: ExactOwnerIdentity,
|
|
1081
|
+
sleep: (ms: number) => Promise<void>,
|
|
1082
|
+
now: () => Date,
|
|
1083
|
+
): Promise<OwnerVerdict | null> {
|
|
1084
|
+
const deadline = now().getTime() + FORCE_CLOSE_VERDICT_TIMEOUT_MS;
|
|
1085
|
+
const paths = lifecyclePaths(identity.stateDir, identity.sessionId, identity.generation);
|
|
1086
|
+
const verdictFile = paths.verdictFile;
|
|
1087
|
+
const verdictAliasFile = paths.verdictAliasFile;
|
|
1088
|
+
while (now().getTime() <= deadline) {
|
|
1089
|
+
try {
|
|
1090
|
+
const [verdictBody, aliasBody] = await Promise.all([
|
|
1091
|
+
fs.readFile(verdictFile, "utf8"),
|
|
1092
|
+
fs.readFile(verdictAliasFile, "utf8"),
|
|
1093
|
+
]);
|
|
1094
|
+
const verdict: unknown = JSON.parse(verdictBody);
|
|
1095
|
+
const alias: unknown = JSON.parse(aliasBody);
|
|
1096
|
+
if (
|
|
1097
|
+
isValidOwnerVerdict(verdict) &&
|
|
1098
|
+
typeof alias === "object" &&
|
|
1099
|
+
alias !== null &&
|
|
1100
|
+
Object.keys(alias).length === Object.keys(verdict).length + 1 &&
|
|
1101
|
+
Object.keys(alias).every(key => key === "owner_generation" || Object.hasOwn(verdict, key)) &&
|
|
1102
|
+
(alias as Record<string, unknown>).owner_generation === identity.generation &&
|
|
1103
|
+
Object.entries(verdict).every(
|
|
1104
|
+
([key, value]) => Object.hasOwn(alias, key) && (alias as Record<string, unknown>)[key] === value,
|
|
1105
|
+
) &&
|
|
1106
|
+
verdict.generation === identity.generation &&
|
|
1107
|
+
verdict.session_id === identity.sessionId &&
|
|
1108
|
+
verdict.server_key === identity.socketKey &&
|
|
1109
|
+
verdict.signal === "SIGTERM" &&
|
|
1110
|
+
verdict.result === "owner_term_then_session_cleanup" &&
|
|
1111
|
+
verdict.classification === "expected_operator_shutdown"
|
|
1112
|
+
)
|
|
1113
|
+
return verdict;
|
|
1114
|
+
} catch {}
|
|
1115
|
+
await sleep(FORCE_CLOSE_VERDICT_POLL_MS);
|
|
1116
|
+
}
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
368
1120
|
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
* This is the lifecycle-control counterpart to {@link removeSkcTmuxSession}: it
|
|
372
|
-
* intentionally does NOT refuse live/attached panes (hard-kill is the contract),
|
|
373
|
-
* but it keeps every safety check so it can only ever kill a genuinely
|
|
374
|
-
* SKC-managed session:
|
|
375
|
-
* - re-reads the exact tmux profile immediately before kill (never a non-SKC
|
|
376
|
-
* session, even one that collides by name);
|
|
377
|
-
* - when `expectedSessionId` is given, requires the `@skc-session-id` tag match;
|
|
378
|
-
* - when `expectedStateFile` is given, requires the `@skc-session-state-file`
|
|
379
|
-
* tag match.
|
|
380
|
-
*
|
|
381
|
-
* Returns the prior status (for audit). Throws a tagged error otherwise:
|
|
382
|
-
* `skc_tmux_session_not_found`, `skc_tmux_session_not_managed`,
|
|
383
|
-
* `skc_tmux_session_id_mismatch`, or `skc_tmux_session_state_file_mismatch`.
|
|
1121
|
+
* Requests an exact tagged owner shutdown. Session cleanup is compatibility-only
|
|
1122
|
+
* and follows a validated SIGTERM verdict; it never authorizes the close.
|
|
384
1123
|
*/
|
|
385
|
-
export function forceCloseSkcTmuxSession(
|
|
1124
|
+
export async function forceCloseSkcTmuxSession(
|
|
386
1125
|
sessionName: string,
|
|
387
1126
|
env: NodeJS.ProcessEnv = process.env,
|
|
388
1127
|
expectedSessionId?: string,
|
|
389
1128
|
expectedStateFile?: string,
|
|
390
|
-
|
|
1129
|
+
deps: Partial<ForceCloseOwnerDependencies> = {},
|
|
1130
|
+
): Promise<SkcTmuxSessionStatus> {
|
|
1131
|
+
if (resolveSkcTmuxBinary({ env }).isPsmux) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
|
|
391
1132
|
const session = statusSkcTmuxSession(sessionName, env);
|
|
392
|
-
if (readProfileForExactTarget(session.name, env) !== SKC_TMUX_PROFILE_VALUE)
|
|
1133
|
+
if (readProfileForExactTarget(session.name, env) !== SKC_TMUX_PROFILE_VALUE)
|
|
393
1134
|
throw new Error(`skc_tmux_session_not_managed:${sessionName}`);
|
|
394
|
-
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
1135
|
+
const exactPanePids = (deps.listPanePids ?? readExactSessionPanePids)(session.name, env);
|
|
1136
|
+
if (exactPanePids.length !== 1) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
|
|
1137
|
+
const actualSessionId = readExactOptionForGc(session.name, SKC_TMUX_SESSION_ID_OPTION, env);
|
|
1138
|
+
const actualStateFile = readExactOptionForGc(session.name, SKC_TMUX_SESSION_STATE_FILE_OPTION, env);
|
|
1139
|
+
const actualGeneration = readExactOptionForGc(session.name, SKC_TMUX_OWNER_GENERATION_OPTION, env);
|
|
1140
|
+
const actualServerKey = readExactOptionForGc(session.name, SKC_TMUX_OWNER_SERVER_KEY_OPTION, env);
|
|
1141
|
+
|
|
1142
|
+
if (expectedSessionId !== undefined && actualSessionId !== expectedSessionId)
|
|
1143
|
+
throw new Error(`skc_tmux_session_id_mismatch:${sessionName}`);
|
|
1144
|
+
if (expectedStateFile !== undefined && actualStateFile !== expectedStateFile)
|
|
1145
|
+
throw new Error(`skc_tmux_session_state_file_mismatch:${sessionName}`);
|
|
1146
|
+
if (!actualSessionId || !actualStateFile || !actualGeneration || !actualServerKey)
|
|
1147
|
+
throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
|
|
1148
|
+
const nativeSessionId = readNativeTmuxSessionId(session.name, env);
|
|
1149
|
+
if (!nativeSessionId) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
|
|
1150
|
+
|
|
1151
|
+
const resolveOwner =
|
|
1152
|
+
deps.resolveOwner ?? ((name, targetEnv) => resolveExactOwner(name, targetEnv, exactPanePids[0]!));
|
|
1153
|
+
const identity = await resolveOwner(session.name, env);
|
|
1154
|
+
if (identity.pid !== exactPanePids[0]) throw new Error(`skc_tmux_owner_identity_mismatch:${sessionName}`);
|
|
1155
|
+
if (identity.sessionId !== actualSessionId || identity.stateDir !== path.dirname(actualStateFile))
|
|
1156
|
+
throw new Error(`skc_tmux_owner_identity_mismatch:${sessionName}`);
|
|
1157
|
+
if (identity.generation !== actualGeneration) throw new Error(`skc_tmux_owner_generation_mismatch:${sessionName}`);
|
|
1158
|
+
if (identity.socketKey !== actualServerKey) throw new Error(`skc_tmux_owner_server_key_mismatch:${sessionName}`);
|
|
1159
|
+
const initialServer = requireSafeTmuxServerForMutation(resolveSkcTmuxCommand(env), env);
|
|
1160
|
+
|
|
1161
|
+
const currentStartTime = await (deps.readProcessStartTime ?? readProcessStartTime)(identity.pid);
|
|
1162
|
+
if (currentStartTime !== identity.startTime) throw new Error("owner_pid_identity_mismatch");
|
|
1163
|
+
const now = deps.now ?? (() => new Date());
|
|
1164
|
+
const sleep = deps.sleep ?? (ms => Bun.sleep(ms));
|
|
1165
|
+
const dispatchId = crypto.randomUUID();
|
|
1166
|
+
await closeExactTmuxOwner(
|
|
1167
|
+
{
|
|
1168
|
+
stateDir: identity.stateDir,
|
|
1169
|
+
sessionId: identity.sessionId,
|
|
1170
|
+
generation: identity.generation,
|
|
1171
|
+
serverKey: identity.socketKey,
|
|
1172
|
+
pid: identity.pid,
|
|
1173
|
+
startTime: identity.startTime,
|
|
1174
|
+
dispatchId,
|
|
1175
|
+
createdAt: now().toISOString(),
|
|
1176
|
+
expiresAt: new Date(now().getTime() + FORCE_CLOSE_VERDICT_TIMEOUT_MS).toISOString(),
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
readStartTime: deps.readProcessStartTime ?? readProcessStartTime,
|
|
1180
|
+
sendSigterm: async pid => {
|
|
1181
|
+
if ((await (deps.readProcessStartTime ?? readProcessStartTime)(pid)) !== identity.startTime)
|
|
1182
|
+
throw new Error("owner_pid_identity_mismatch");
|
|
1183
|
+
(deps.signalTerm ?? (target => process.kill(target, "SIGTERM")))(pid);
|
|
1184
|
+
},
|
|
1185
|
+
|
|
1186
|
+
waitForVerdict: () => waitForExpectedVerdict(identity, sleep, now),
|
|
1187
|
+
cleanupSession: async () => {
|
|
1188
|
+
await requireUnchangedOwnerForCompatibilityCleanup(
|
|
1189
|
+
session.name,
|
|
1190
|
+
nativeSessionId,
|
|
1191
|
+
env,
|
|
1192
|
+
identity,
|
|
1193
|
+
actualStateFile,
|
|
1194
|
+
initialServer,
|
|
1195
|
+
deps.listPanePids ?? readExactSessionPanePids,
|
|
1196
|
+
deps.readProcessStartTime ?? readProcessStartTime,
|
|
1197
|
+
);
|
|
1198
|
+
if (deps.cleanupSession) deps.cleanupSession(nativeSessionId, env);
|
|
1199
|
+
else
|
|
1200
|
+
runGuardedTmuxSessionCommand(
|
|
1201
|
+
nativeSessionId,
|
|
1202
|
+
session.name,
|
|
1203
|
+
initialServer.pid,
|
|
1204
|
+
env,
|
|
1205
|
+
`kill-session -t '${nativeSessionId}'`,
|
|
1206
|
+
);
|
|
1207
|
+
},
|
|
1208
|
+
},
|
|
1209
|
+
);
|
|
409
1210
|
return session;
|
|
410
1211
|
}
|
|
411
1212
|
|
|
412
1213
|
export function attachSkcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): never {
|
|
1214
|
+
if (resolveSkcTmuxBinary({ env }).isPsmux) throw new Error(`skc_tmux_owner_unverifiable:${sessionName}`);
|
|
413
1215
|
const session = statusSkcTmuxSession(sessionName, env);
|
|
414
1216
|
const tmuxCommand = resolveSkcTmuxCommand(env);
|
|
1217
|
+
requireSafeTmuxServerForMutation(tmuxCommand, env);
|
|
415
1218
|
const result = Bun.spawnSync(
|
|
416
1219
|
[tmuxCommand, "attach-session", "-t", buildSkcTmuxExactSessionTarget(session.name, { env })],
|
|
417
1220
|
{
|