@gajae-code/coding-agent 0.12.5 → 0.12.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -3
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +43 -0
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/async/job-manager.ts +430 -46
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +360 -156
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/modes/acp/acp-agent.ts +27 -13
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +691 -92
- package/src/sdk/bus/chat-daemon-control.ts +30 -6
- package/src/sdk/bus/index.ts +23 -6
- package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/session.ts +7 -6
- package/src/session/agent-session.ts +752 -339
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/internal/managed-session-scope.ts +252 -67
- package/src/session/internal/managed-session-storage.ts +1 -0
- package/src/session/session-manager.ts +7 -0
- package/src/session/session-storage.ts +146 -24
- package/src/task/index.ts +230 -19
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +2 -0
- package/src/task/types.ts +10 -0
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +10 -2
- package/src/tools/subagent.ts +97 -75
- package/src/tools/todo-write.ts +47 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
captureManagedFileNoFollow,
|
|
6
|
+
type ManagedFileSnapshot,
|
|
7
|
+
prepareManagedDirectoryRoot,
|
|
8
|
+
publishManagedFileNoReplaceSync,
|
|
9
|
+
} from "../session/internal/managed-session-storage";
|
|
10
|
+
import {
|
|
11
|
+
type PsmuxSpawnRunner,
|
|
12
|
+
type ResolvedTmuxBinary,
|
|
13
|
+
resolveGjcTmuxBinary,
|
|
14
|
+
resolveGjcTmuxExecutableIdentity,
|
|
15
|
+
resolveGjcTmuxExecutablePath,
|
|
16
|
+
} from "./psmux-detect";
|
|
17
|
+
import { isCanonicalUtcTimestamp, lifecyclePaths } from "./tmux-owner-isolation";
|
|
18
|
+
|
|
19
|
+
const AUTHORITY_SCHEMA_VERSION = 2;
|
|
20
|
+
const MAX_AUTHORITY_BYTES = 4096;
|
|
21
|
+
|
|
22
|
+
let authorityPlatformForTests: NodeJS.Platform | null = null;
|
|
23
|
+
|
|
24
|
+
function authorityPlatform(): NodeJS.Platform {
|
|
25
|
+
return authorityPlatformForTests ?? process.platform;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @internal Test-only seam for hermetic Windows authority storage tests. */
|
|
29
|
+
/**
|
|
30
|
+
* The platform that governs psmux authority decisions, honoring the test
|
|
31
|
+
* override. Every authority gate must read this rather than `process.platform`,
|
|
32
|
+
* or a pinned test platform is silently ignored on non-Windows hosts.
|
|
33
|
+
*/
|
|
34
|
+
export function gjcTmuxAuthorityPlatform(): NodeJS.Platform {
|
|
35
|
+
return authorityPlatform();
|
|
36
|
+
}
|
|
37
|
+
export function __setTmuxProviderAuthorityPlatformForTests(platform: NodeJS.Platform | null): void {
|
|
38
|
+
authorityPlatformForTests = platform;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function requireWindowsAuthorityPlatform(): void {
|
|
42
|
+
if (authorityPlatform() !== "win32") throw new Error("gjc_tmux_provider_authority_windows_required");
|
|
43
|
+
}
|
|
44
|
+
export type TmuxProviderKind = "native-tmux" | "windows-psmux";
|
|
45
|
+
|
|
46
|
+
/** A structured tmux provider. Native tmux intentionally has no prefix. */
|
|
47
|
+
export interface ProviderContext {
|
|
48
|
+
readonly kind: TmuxProviderKind;
|
|
49
|
+
readonly command: string;
|
|
50
|
+
readonly commandPrefix: readonly string[];
|
|
51
|
+
readonly namespace: string | null;
|
|
52
|
+
readonly executableIdentity: string | null;
|
|
53
|
+
readonly binary: ResolvedTmuxBinary;
|
|
54
|
+
readonly platform: NodeJS.Platform;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Durable authority binding for one owner generation. */
|
|
58
|
+
export interface ProviderAuthority extends ProviderContext {
|
|
59
|
+
readonly stateDir: string;
|
|
60
|
+
readonly sessionId: string;
|
|
61
|
+
readonly generation: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type ProviderRecord = {
|
|
65
|
+
schema_version: 2;
|
|
66
|
+
kind: "windows-psmux";
|
|
67
|
+
platform: "win32";
|
|
68
|
+
session_id: string;
|
|
69
|
+
owner_generation: string;
|
|
70
|
+
namespace: string;
|
|
71
|
+
target_syntax: "-L";
|
|
72
|
+
executable_path: string;
|
|
73
|
+
executable_identity: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function rejectUnsafeToken(value: string, name: string): string {
|
|
77
|
+
const trimmed = value.trim();
|
|
78
|
+
if (!trimmed || /[\0\r\n]|\s/.test(trimmed) || /[;&|`$<>]/.test(trimmed))
|
|
79
|
+
throw new Error(`gjc_tmux_provider_invalid_${name}`);
|
|
80
|
+
return trimmed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function requireSafePathComponent(value: string, name: string): string {
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
if (trimmed === "." || trimmed === ".." || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(trimmed))
|
|
86
|
+
throw new Error(`gjc_tmux_provider_invalid_${name}`);
|
|
87
|
+
return trimmed;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function randomNamespace(): string {
|
|
91
|
+
return `gjc-${crypto.randomBytes(16).toString("hex")}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Resolve a structured provider. Native tmux remains byte-for-byte argv compatible. */
|
|
95
|
+
export function resolveGjcTmuxProviderContext(
|
|
96
|
+
options: {
|
|
97
|
+
env?: NodeJS.ProcessEnv;
|
|
98
|
+
platform?: NodeJS.Platform;
|
|
99
|
+
runner?: PsmuxSpawnRunner;
|
|
100
|
+
binary?: ResolvedTmuxBinary;
|
|
101
|
+
} = {},
|
|
102
|
+
): ProviderContext {
|
|
103
|
+
// Route through the authority-platform accessor so a test pin propagates to
|
|
104
|
+
// every platform decision in this module. Three separate sources previously
|
|
105
|
+
// read process.platform directly, so a pinned win32 test still took POSIX
|
|
106
|
+
// branches; production is unchanged because the accessor defaults to
|
|
107
|
+
// process.platform when unpinned.
|
|
108
|
+
const platform = options.platform ?? gjcTmuxAuthorityPlatform();
|
|
109
|
+
const binary = options.binary ?? resolveGjcTmuxBinary(options);
|
|
110
|
+
const selectedCommand = rejectUnsafeToken(binary.command, "command");
|
|
111
|
+
if (binary.isPsmux && platform !== "win32")
|
|
112
|
+
throw new Error("gjc_tmux_provider_ambiguous: selected psmux command requires Windows");
|
|
113
|
+
if (!binary.isPsmux) {
|
|
114
|
+
return Object.freeze({
|
|
115
|
+
kind: "native-tmux",
|
|
116
|
+
command: selectedCommand,
|
|
117
|
+
commandPrefix: Object.freeze([]),
|
|
118
|
+
namespace: null,
|
|
119
|
+
executableIdentity: null,
|
|
120
|
+
binary,
|
|
121
|
+
platform,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const resolved = resolveGjcTmuxExecutablePath(selectedCommand);
|
|
125
|
+
if (!resolved || !(path.win32.isAbsolute(resolved) || path.isAbsolute(resolved)))
|
|
126
|
+
throw new Error("gjc_tmux_provider_ambiguous: selected Windows psmux command is not an absolute executable");
|
|
127
|
+
const identity = resolveGjcTmuxExecutableIdentity(resolved);
|
|
128
|
+
if (!identity)
|
|
129
|
+
throw new Error("gjc_tmux_provider_ambiguous: selected Windows psmux executable identity is unavailable");
|
|
130
|
+
const namespace = randomNamespace();
|
|
131
|
+
return Object.freeze({
|
|
132
|
+
kind: "windows-psmux",
|
|
133
|
+
command: resolved,
|
|
134
|
+
commandPrefix: Object.freeze(["-L", namespace]),
|
|
135
|
+
namespace,
|
|
136
|
+
executableIdentity: identity,
|
|
137
|
+
binary,
|
|
138
|
+
platform,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function buildTmuxProviderCommand(
|
|
143
|
+
context: ProviderContext,
|
|
144
|
+
command: string,
|
|
145
|
+
args: readonly string[] = [],
|
|
146
|
+
): string[] {
|
|
147
|
+
if (!/^[a-z][a-z-]*$/i.test(command) || args.some(arg => typeof arg !== "string" || arg.includes("\0")))
|
|
148
|
+
throw new Error("gjc_tmux_provider_invalid_command");
|
|
149
|
+
return [...context.commandPrefix, command, ...args];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function rootFor(authority: Pick<ProviderAuthority, "stateDir" | "sessionId" | "generation">): string {
|
|
153
|
+
return lifecyclePaths(authority.stateDir, authority.sessionId, authority.generation).root;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function authorityName(generation: string): string {
|
|
157
|
+
return `provider-authority-${encodeURIComponent(generation)}.json`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function assertCurrentGeneration(root: string, sessionId: string, generation: string): void {
|
|
161
|
+
const snapshot = captureManagedFileNoFollow(path.join(root, "generation.json"));
|
|
162
|
+
let payload: unknown;
|
|
163
|
+
try {
|
|
164
|
+
payload = JSON.parse(new TextDecoder().decode(snapshot.bytes));
|
|
165
|
+
} catch {
|
|
166
|
+
throw new Error("gjc_tmux_provider_authority_generation_unavailable");
|
|
167
|
+
}
|
|
168
|
+
const record = payload as Record<string, unknown>;
|
|
169
|
+
const publishedAt = record.published_at;
|
|
170
|
+
if (
|
|
171
|
+
!payload ||
|
|
172
|
+
typeof payload !== "object" ||
|
|
173
|
+
Array.isArray(payload) ||
|
|
174
|
+
Object.keys(record).sort().join(",") !== "generation,published_at,schema_version,session_id" ||
|
|
175
|
+
record.schema_version !== 1 ||
|
|
176
|
+
record.session_id !== sessionId ||
|
|
177
|
+
record.generation !== generation ||
|
|
178
|
+
!isCanonicalUtcTimestamp(publishedAt)
|
|
179
|
+
)
|
|
180
|
+
throw new Error("gjc_tmux_provider_authority_generation_mismatch");
|
|
181
|
+
}
|
|
182
|
+
/** Returns whether an owner generation has a persisted Windows psmux authority. */
|
|
183
|
+
export function hasGjcTmuxProviderAuthoritySync(input: {
|
|
184
|
+
stateDir: string;
|
|
185
|
+
sessionId: string;
|
|
186
|
+
generation: string;
|
|
187
|
+
}): boolean {
|
|
188
|
+
if (authorityPlatform() !== "win32") return false;
|
|
189
|
+
const stateDir = path.resolve(input.stateDir);
|
|
190
|
+
const sessionId = requireSafePathComponent(input.sessionId, "session_id");
|
|
191
|
+
const generation = requireSafePathComponent(input.generation, "generation");
|
|
192
|
+
const root = rootFor({ stateDir, sessionId, generation });
|
|
193
|
+
const generationFile = path.join(root, "generation.json");
|
|
194
|
+
const authorityFile = path.join(root, authorityName(generation));
|
|
195
|
+
try {
|
|
196
|
+
fs.lstatSync(root);
|
|
197
|
+
fs.lstatSync(generationFile);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
requireWindowsAuthorityPlatform();
|
|
203
|
+
assertCurrentGeneration(root, sessionId, generation);
|
|
204
|
+
try {
|
|
205
|
+
fs.lstatSync(authorityFile);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
readGjcTmuxProviderAuthoritySync({ stateDir, sessionId, generation });
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function readWindowsAuthority(
|
|
215
|
+
root: string,
|
|
216
|
+
generation: string,
|
|
217
|
+
): {
|
|
218
|
+
record: ProviderRecord;
|
|
219
|
+
identity: ManagedFileSnapshot["identity"];
|
|
220
|
+
} {
|
|
221
|
+
const snapshot = captureManagedFileNoFollow(path.join(root, authorityName(generation)));
|
|
222
|
+
return { record: parseRecord(snapshot.bytes), identity: snapshot.identity };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function prepareWindowsAuthorityRoot(root: string) {
|
|
226
|
+
return prepareManagedDirectoryRoot(root, "windows-existing-verify-first");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function parseRecord(data: Uint8Array): ProviderRecord {
|
|
230
|
+
if (data.byteLength === 0 || data.byteLength > MAX_AUTHORITY_BYTES)
|
|
231
|
+
throw new Error("gjc_tmux_provider_authority_invalid_record");
|
|
232
|
+
let payload: unknown;
|
|
233
|
+
try {
|
|
234
|
+
payload = JSON.parse(new TextDecoder().decode(data));
|
|
235
|
+
} catch {
|
|
236
|
+
throw new Error("gjc_tmux_provider_authority_invalid_record");
|
|
237
|
+
}
|
|
238
|
+
if (!payload || typeof payload !== "object") throw new Error("gjc_tmux_provider_authority_invalid_record");
|
|
239
|
+
const value = payload as Record<string, unknown>;
|
|
240
|
+
if (
|
|
241
|
+
value.schema_version !== AUTHORITY_SCHEMA_VERSION ||
|
|
242
|
+
value.kind !== "windows-psmux" ||
|
|
243
|
+
value.platform !== "win32" ||
|
|
244
|
+
typeof value.session_id !== "string" ||
|
|
245
|
+
typeof value.owner_generation !== "string" ||
|
|
246
|
+
typeof value.namespace !== "string" ||
|
|
247
|
+
!/^gjc-[a-f0-9]{32}$/.test(value.namespace) ||
|
|
248
|
+
value.target_syntax !== "-L" ||
|
|
249
|
+
typeof value.executable_path !== "string" ||
|
|
250
|
+
!(path.win32.isAbsolute(value.executable_path) || path.isAbsolute(value.executable_path)) ||
|
|
251
|
+
typeof value.executable_identity !== "string"
|
|
252
|
+
)
|
|
253
|
+
throw new Error("gjc_tmux_provider_authority_invalid_record");
|
|
254
|
+
return value as ProviderRecord;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function bindGjcTmuxProviderAuthority(
|
|
258
|
+
context: ProviderContext,
|
|
259
|
+
input: { stateDir: string; sessionId: string; generation: string },
|
|
260
|
+
): ProviderAuthority {
|
|
261
|
+
const stateDir = input.stateDir.trim();
|
|
262
|
+
if (!stateDir || /[\0\r\n]/.test(stateDir)) throw new Error("gjc_tmux_provider_invalid_state_dir");
|
|
263
|
+
return Object.freeze({
|
|
264
|
+
...context,
|
|
265
|
+
stateDir: path.resolve(stateDir),
|
|
266
|
+
sessionId: requireSafePathComponent(input.sessionId, "session_id"),
|
|
267
|
+
generation: requireSafePathComponent(input.generation, "generation"),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function recordFor(authority: ProviderAuthority): ProviderRecord {
|
|
272
|
+
if (!authority.namespace || !authority.executableIdentity)
|
|
273
|
+
throw new Error("gjc_tmux_provider_authority_invalid_context");
|
|
274
|
+
return {
|
|
275
|
+
schema_version: 2,
|
|
276
|
+
kind: "windows-psmux",
|
|
277
|
+
platform: "win32",
|
|
278
|
+
session_id: authority.sessionId,
|
|
279
|
+
owner_generation: authority.generation,
|
|
280
|
+
namespace: authority.namespace,
|
|
281
|
+
target_syntax: "-L",
|
|
282
|
+
executable_path: authority.command,
|
|
283
|
+
executable_identity: authority.executableIdentity,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Persist an immutable, generation-scoped authority before generation publication. Native tmux needs no record. */
|
|
288
|
+
// A retained lock descriptor for this exact authority name means another writer
|
|
289
|
+
// is mid-publish. Its lease may already be expired while its owner process is
|
|
290
|
+
// still alive — a paused or slow writer — and stealing in that window would let
|
|
291
|
+
// two writers publish the same generation-scoped authority. Expiry alone is
|
|
292
|
+
// therefore not permission to proceed: only an owner that is definitely gone
|
|
293
|
+
// releases the name. This is deliberately local to the psmux authority path so
|
|
294
|
+
// it does not depend on the managed-storage lease helpers.
|
|
295
|
+
function migrationBusyIfLiveHolder(locksDirectory: string, name: string): void {
|
|
296
|
+
let raw: string;
|
|
297
|
+
try {
|
|
298
|
+
raw = fs.readFileSync(path.join(locksDirectory, `${name}.lock`), "utf8");
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
301
|
+
throw new Error("migration_busy");
|
|
302
|
+
}
|
|
303
|
+
let holder: { pid?: unknown } | undefined;
|
|
304
|
+
try {
|
|
305
|
+
holder = JSON.parse(raw) as { pid?: unknown };
|
|
306
|
+
} catch {
|
|
307
|
+
// An unreadable descriptor is indistinguishable from a live one.
|
|
308
|
+
throw new Error("migration_busy");
|
|
309
|
+
}
|
|
310
|
+
const pid = typeof holder?.pid === "number" && Number.isInteger(holder.pid) ? holder.pid : undefined;
|
|
311
|
+
if (pid === undefined) throw new Error("migration_busy");
|
|
312
|
+
try {
|
|
313
|
+
process.kill(pid, 0);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
// ESRCH proves the owner is gone; EPERM proves it is alive under another uid.
|
|
316
|
+
if ((error as NodeJS.ErrnoException).code === "ESRCH") return;
|
|
317
|
+
}
|
|
318
|
+
throw new Error("migration_busy");
|
|
319
|
+
}
|
|
320
|
+
export function persistGjcTmuxProviderAuthoritySync(authority: ProviderAuthority): void {
|
|
321
|
+
if (authority.kind !== "windows-psmux") return;
|
|
322
|
+
requireWindowsAuthorityPlatform();
|
|
323
|
+
const root = rootFor(authority);
|
|
324
|
+
const bytes = new TextEncoder().encode(JSON.stringify(recordFor(authority)));
|
|
325
|
+
const name = authorityName(authority.generation);
|
|
326
|
+
const managedRoot = prepareWindowsAuthorityRoot(root);
|
|
327
|
+
migrationBusyIfLiveHolder(path.join(managedRoot.canonicalPath, "provider-authority-locks"), name);
|
|
328
|
+
// `publishManagedFileNoReplaceSync` is create-without-clobber, so it is itself
|
|
329
|
+
// the mutual exclusion for this generation-scoped authority: a second writer
|
|
330
|
+
// loses the create rather than racing a separate lock file. The readback below
|
|
331
|
+
// still proves the published record binds this session and generation.
|
|
332
|
+
publishManagedFileNoReplaceSync(
|
|
333
|
+
path.join(managedRoot.canonicalPath, name),
|
|
334
|
+
bytes,
|
|
335
|
+
managedRoot,
|
|
336
|
+
"windows-existing-verify-first",
|
|
337
|
+
);
|
|
338
|
+
const verified = readWindowsAuthority(managedRoot.canonicalPath, authority.generation);
|
|
339
|
+
if (verified.record.session_id !== authority.sessionId || verified.record.owner_generation !== authority.generation)
|
|
340
|
+
throw new Error("gjc_tmux_provider_authority_publish_failed");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function readGjcTmuxProviderAuthoritySync(input: {
|
|
344
|
+
stateDir: string;
|
|
345
|
+
sessionId: string;
|
|
346
|
+
generation: string;
|
|
347
|
+
}): ProviderAuthority {
|
|
348
|
+
const stateDir = path.resolve(input.stateDir);
|
|
349
|
+
const sessionId = requireSafePathComponent(input.sessionId, "session_id");
|
|
350
|
+
const generation = requireSafePathComponent(input.generation, "generation");
|
|
351
|
+
const root = rootFor({ stateDir, sessionId, generation });
|
|
352
|
+
requireWindowsAuthorityPlatform();
|
|
353
|
+
const managedRoot = prepareWindowsAuthorityRoot(root);
|
|
354
|
+
assertCurrentGeneration(managedRoot.canonicalPath, sessionId, generation);
|
|
355
|
+
const { record } = readWindowsAuthority(managedRoot.canonicalPath, generation);
|
|
356
|
+
if (record.session_id !== sessionId || record.owner_generation !== generation)
|
|
357
|
+
throw new Error("gjc_tmux_provider_authority_mismatch");
|
|
358
|
+
const identity = resolveGjcTmuxExecutableIdentity(record.executable_path);
|
|
359
|
+
if (!identity || identity !== record.executable_identity)
|
|
360
|
+
throw new Error("gjc_tmux_provider_authority_executable_changed");
|
|
361
|
+
return Object.freeze({
|
|
362
|
+
kind: "windows-psmux",
|
|
363
|
+
command: record.executable_path,
|
|
364
|
+
commandPrefix: Object.freeze([record.target_syntax, record.namespace]),
|
|
365
|
+
namespace: record.namespace,
|
|
366
|
+
executableIdentity: record.executable_identity,
|
|
367
|
+
binary: Object.freeze({ command: record.executable_path, isPsmux: true, viaExplicitOverride: true }),
|
|
368
|
+
platform: "win32",
|
|
369
|
+
stateDir,
|
|
370
|
+
sessionId,
|
|
371
|
+
generation,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
/** Enumerates owner-secured, executable-validated psmux authorities in one durable state root. */
|
|
375
|
+
export function listGjcTmuxProviderAuthoritiesSync(stateDirInput: string): ProviderAuthority[] {
|
|
376
|
+
const stateDir = path.resolve(stateDirInput);
|
|
377
|
+
requireWindowsAuthorityPlatform();
|
|
378
|
+
prepareWindowsAuthorityRoot(stateDir);
|
|
379
|
+
let entries: fs.Dirent[];
|
|
380
|
+
try {
|
|
381
|
+
entries = fs.readdirSync(stateDir, { withFileTypes: true });
|
|
382
|
+
} catch {
|
|
383
|
+
throw new Error("gjc_tmux_provider_authority_unavailable");
|
|
384
|
+
}
|
|
385
|
+
const authorities: ProviderAuthority[] = [];
|
|
386
|
+
for (const entry of entries) {
|
|
387
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
388
|
+
try {
|
|
389
|
+
const sessionId = requireSafePathComponent(entry.name, "session_id");
|
|
390
|
+
const root = rootFor({ stateDir, sessionId, generation: "enumerate" });
|
|
391
|
+
const generationSnapshot = captureManagedFileNoFollow(path.join(root, "generation.json"));
|
|
392
|
+
const generationPayload = JSON.parse(new TextDecoder().decode(generationSnapshot.bytes)) as {
|
|
393
|
+
generation?: unknown;
|
|
394
|
+
session_id?: unknown;
|
|
395
|
+
};
|
|
396
|
+
if (typeof generationPayload.generation !== "string" || generationPayload.session_id !== sessionId)
|
|
397
|
+
throw new Error("gjc_tmux_provider_authority_generation_mismatch");
|
|
398
|
+
const generation = requireSafePathComponent(generationPayload.generation, "generation");
|
|
399
|
+
authorities.push(
|
|
400
|
+
readGjcTmuxProviderAuthoritySync({
|
|
401
|
+
stateDir,
|
|
402
|
+
sessionId,
|
|
403
|
+
generation,
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
406
|
+
} catch {
|
|
407
|
+
throw new Error("gjc_tmux_provider_authority_unavailable");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return authorities;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Re-proves the staged immutable record and executable identity before generation publication. */
|
|
414
|
+
export function assertGjcTmuxStagedMutationAuthoritySync(authority: ProviderAuthority): void {
|
|
415
|
+
if (authority.kind !== "windows-psmux") return;
|
|
416
|
+
requireWindowsAuthorityPlatform();
|
|
417
|
+
const root = prepareWindowsAuthorityRoot(rootFor(authority));
|
|
418
|
+
const { record } = readWindowsAuthority(root.canonicalPath, authority.generation);
|
|
419
|
+
if (
|
|
420
|
+
record.session_id !== authority.sessionId ||
|
|
421
|
+
record.owner_generation !== authority.generation ||
|
|
422
|
+
record.namespace !== authority.namespace ||
|
|
423
|
+
record.target_syntax !== "-L" ||
|
|
424
|
+
record.executable_path !== authority.command ||
|
|
425
|
+
record.executable_identity !== authority.executableIdentity
|
|
426
|
+
)
|
|
427
|
+
throw new Error("gjc_tmux_provider_authority_mismatch");
|
|
428
|
+
const identity = resolveGjcTmuxExecutableIdentity(authority.command);
|
|
429
|
+
if (!identity || identity !== authority.executableIdentity)
|
|
430
|
+
throw new Error("gjc_tmux_provider_authority_executable_changed");
|
|
431
|
+
}
|
|
432
|
+
/** Re-proves the current pointer, exact executable identity, and native root before mutation. */
|
|
433
|
+
export function assertGjcTmuxMutationAuthoritySync(authority: ProviderAuthority): void {
|
|
434
|
+
if (authority.kind !== "windows-psmux") return;
|
|
435
|
+
requireWindowsAuthorityPlatform();
|
|
436
|
+
const root = prepareWindowsAuthorityRoot(rootFor(authority));
|
|
437
|
+
assertCurrentGeneration(root.canonicalPath, authority.sessionId, authority.generation);
|
|
438
|
+
const { record } = readWindowsAuthority(root.canonicalPath, authority.generation);
|
|
439
|
+
if (
|
|
440
|
+
record.session_id !== authority.sessionId ||
|
|
441
|
+
record.owner_generation !== authority.generation ||
|
|
442
|
+
record.namespace !== authority.namespace ||
|
|
443
|
+
record.target_syntax !== "-L" ||
|
|
444
|
+
record.executable_path !== authority.command ||
|
|
445
|
+
record.executable_identity !== authority.executableIdentity
|
|
446
|
+
)
|
|
447
|
+
throw new Error("gjc_tmux_provider_authority_mismatch");
|
|
448
|
+
const identity = resolveGjcTmuxExecutableIdentity(authority.command);
|
|
449
|
+
if (!identity || identity !== authority.executableIdentity)
|
|
450
|
+
throw new Error("gjc_tmux_provider_authority_executable_changed");
|
|
451
|
+
}
|