@gajae-code/coding-agent 0.9.6 → 0.10.0

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.
Files changed (193) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/types/cli/classify-install-target.d.ts +0 -10
  3. package/dist/types/cli/daemon-cli.d.ts +2 -0
  4. package/dist/types/cli/initial-message.d.ts +1 -1
  5. package/dist/types/cli/malloc-env-guard.d.ts +65 -0
  6. package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
  7. package/dist/types/cli/notify-cli.d.ts +3 -1
  8. package/dist/types/cli/session-picker.d.ts +4 -3
  9. package/dist/types/cli/update-cli.d.ts +22 -3
  10. package/dist/types/cli.d.ts +4 -0
  11. package/dist/types/commands/daemon.d.ts +6 -2
  12. package/dist/types/commands/launch.d.ts +1 -0
  13. package/dist/types/commands/notify.d.ts +6 -0
  14. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  15. package/dist/types/commit/analysis/summary.d.ts +1 -1
  16. package/dist/types/commit/changelog/generate.d.ts +1 -1
  17. package/dist/types/commit/changelog/index.d.ts +2 -2
  18. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  19. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  20. package/dist/types/config/keybindings.d.ts +5 -0
  21. package/dist/types/config/model-profile-activation.d.ts +1 -0
  22. package/dist/types/config/settings-schema.d.ts +47 -7
  23. package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
  24. package/dist/types/coordinator-mcp/server.d.ts +67 -6
  25. package/dist/types/daemon/control-types.d.ts +10 -0
  26. package/dist/types/daemon/operator-contract.d.ts +47 -0
  27. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
  28. package/dist/types/discovery/helpers.d.ts +1 -1
  29. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  30. package/dist/types/gjc-runtime/launch-tmux.d.ts +26 -7
  31. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +34 -3
  32. package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
  33. package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +309 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +49 -17
  36. package/dist/types/internal-urls/local-protocol.d.ts +13 -9
  37. package/dist/types/main.d.ts +66 -2
  38. package/dist/types/modes/acp/acp-agent.d.ts +19 -2
  39. package/dist/types/modes/acp/acp-mode.d.ts +12 -0
  40. package/dist/types/modes/components/assistant-message.d.ts +1 -1
  41. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  42. package/dist/types/modes/components/gajae-pet-widget.d.ts +59 -0
  43. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  44. package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
  45. package/dist/types/modes/components/pet-selector.d.ts +11 -0
  46. package/dist/types/modes/components/session-selector.d.ts +22 -9
  47. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  48. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  49. package/dist/types/modes/components/user-message.d.ts +2 -1
  50. package/dist/types/modes/controllers/event-controller.d.ts +4 -0
  51. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  52. package/dist/types/modes/interactive-mode.d.ts +24 -2
  53. package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
  54. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
  55. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
  56. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
  57. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
  58. package/dist/types/modes/theme/defaults/index.d.ts +568 -568
  59. package/dist/types/modes/types.d.ts +28 -2
  60. package/dist/types/modes/utils/abort-message.d.ts +1 -1
  61. package/dist/types/modes/utils/irc-message.d.ts +38 -0
  62. package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
  63. package/dist/types/notifications/config.d.ts +34 -0
  64. package/dist/types/notifications/daemon-paths.d.ts +7 -0
  65. package/dist/types/notifications/index.d.ts +27 -0
  66. package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
  67. package/dist/types/notifications/notification-service.d.ts +144 -0
  68. package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
  69. package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
  70. package/dist/types/notifications/telegram-daemon.d.ts +130 -3
  71. package/dist/types/notifications/telegram-reference.d.ts +19 -2
  72. package/dist/types/notifications/topic-registry.d.ts +24 -4
  73. package/dist/types/registry/agent-registry.d.ts +2 -0
  74. package/dist/types/rlm/index.d.ts +1 -1
  75. package/dist/types/sdk.d.ts +2 -0
  76. package/dist/types/secrets/obfuscator.d.ts +1 -1
  77. package/dist/types/session/agent-session.d.ts +26 -2
  78. package/dist/types/session/cache-economics.d.ts +79 -0
  79. package/dist/types/session/provider-safety-stop.d.ts +5 -0
  80. package/dist/types/session/session-manager.d.ts +124 -4
  81. package/dist/types/session/session-storage.d.ts +155 -13
  82. package/dist/types/setup/credential-auto-import.d.ts +2 -2
  83. package/dist/types/task/executor.d.ts +3 -0
  84. package/dist/types/task/receipt.d.ts +3 -1
  85. package/dist/types/task/types.d.ts +8 -0
  86. package/dist/types/tools/ask-answer-registry.d.ts +5 -0
  87. package/dist/types/tools/ask.d.ts +25 -1
  88. package/dist/types/tools/bash.d.ts +1 -1
  89. package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
  90. package/dist/types/tools/index.d.ts +58 -6
  91. package/dist/types/tools/telegram-send.d.ts +7 -0
  92. package/dist/types/utils/external-editor.d.ts +2 -0
  93. package/package.json +9 -9
  94. package/scripts/g004-tmux-smoke.ts +196 -36
  95. package/src/cli/daemon-cli.ts +19 -29
  96. package/src/cli/malloc-env-guard.test.ts +104 -0
  97. package/src/cli/malloc-env-guard.ts +149 -0
  98. package/src/cli/notify-cli.ts +78 -14
  99. package/src/cli/session-picker.ts +23 -40
  100. package/src/cli/update-cli.ts +56 -37
  101. package/src/cli.ts +50 -10
  102. package/src/commands/daemon.ts +19 -5
  103. package/src/commands/harness.ts +515 -47
  104. package/src/commands/launch.ts +11 -3
  105. package/src/commands/notify.ts +5 -1
  106. package/src/commands/session.ts +6 -1
  107. package/src/config/keybindings.ts +5 -0
  108. package/src/config/model-profile-activation.ts +1 -1
  109. package/src/config/model-profiles.ts +14 -14
  110. package/src/config/settings-schema.ts +41 -1
  111. package/src/coordinator-mcp/model-preset.ts +100 -0
  112. package/src/coordinator-mcp/server.ts +2653 -425
  113. package/src/daemon/control-types.ts +11 -0
  114. package/src/daemon/operator-contract.ts +121 -0
  115. package/src/deep-interview/render-middleware.ts +67 -2
  116. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  117. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  118. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  119. package/src/gjc-runtime/launch-tmux.ts +702 -135
  120. package/src/gjc-runtime/session-state-sidecar.ts +756 -135
  121. package/src/gjc-runtime/team-runtime.ts +6 -0
  122. package/src/gjc-runtime/tmux-common.ts +26 -7
  123. package/src/gjc-runtime/tmux-gc.ts +52 -9
  124. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +185 -0
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +2060 -0
  126. package/src/gjc-runtime/tmux-sessions.ts +871 -68
  127. package/src/internal-urls/docs-index.generated.ts +9 -8
  128. package/src/internal-urls/local-protocol.ts +29 -10
  129. package/src/main.ts +298 -75
  130. package/src/modes/acp/acp-agent.ts +728 -98
  131. package/src/modes/acp/acp-mode.ts +29 -2
  132. package/src/modes/components/assistant-message.ts +32 -3
  133. package/src/modes/components/bash-execution.ts +43 -13
  134. package/src/modes/components/custom-editor.ts +5 -0
  135. package/src/modes/components/gajae-pet-widget.ts +360 -0
  136. package/src/modes/components/hook-selector.ts +12 -0
  137. package/src/modes/components/irc-sidebar.ts +206 -0
  138. package/src/modes/components/pet-selector.ts +55 -0
  139. package/src/modes/components/session-selector.ts +222 -221
  140. package/src/modes/components/settings-selector.ts +10 -0
  141. package/src/modes/components/tool-execution.ts +47 -6
  142. package/src/modes/components/user-message.ts +26 -9
  143. package/src/modes/controllers/command-controller.ts +28 -2
  144. package/src/modes/controllers/event-controller.ts +159 -36
  145. package/src/modes/controllers/extension-ui-controller.ts +36 -17
  146. package/src/modes/controllers/input-controller.ts +8 -2
  147. package/src/modes/controllers/selector-controller.ts +62 -15
  148. package/src/modes/interactive-mode.ts +183 -7
  149. package/src/modes/irc-observation-ledger.ts +127 -0
  150. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  151. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  152. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  153. package/src/modes/types.ts +34 -2
  154. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  155. package/src/modes/utils/irc-message.ts +138 -0
  156. package/src/modes/utils/ui-helpers.ts +216 -50
  157. package/src/notifications/config.ts +44 -0
  158. package/src/notifications/daemon-paths.ts +8 -0
  159. package/src/notifications/index.ts +535 -75
  160. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  161. package/src/notifications/notification-service.ts +710 -0
  162. package/src/notifications/rate-limit-pool.ts +46 -12
  163. package/src/notifications/telegram-daemon-cli.ts +2 -0
  164. package/src/notifications/telegram-daemon-control.ts +27 -2
  165. package/src/notifications/telegram-daemon.ts +635 -94
  166. package/src/notifications/telegram-reference.ts +73 -13
  167. package/src/notifications/topic-registry.ts +86 -9
  168. package/src/prompts/system/irc-peer-roster.md +1 -0
  169. package/src/prompts/system/subagent-system-prompt.md +1 -0
  170. package/src/prompts/system/system-prompt.md +1 -0
  171. package/src/prompts/tools/irc.md +1 -1
  172. package/src/registry/agent-registry.ts +3 -0
  173. package/src/sdk.ts +52 -1
  174. package/src/secrets/obfuscator.ts +4 -2
  175. package/src/session/agent-session.ts +396 -145
  176. package/src/session/cache-economics.ts +275 -0
  177. package/src/session/provider-safety-stop.ts +9 -0
  178. package/src/session/session-dump-format.ts +6 -0
  179. package/src/session/session-manager.ts +809 -39
  180. package/src/session/session-storage.ts +582 -38
  181. package/src/slash-commands/builtin-registry.ts +143 -9
  182. package/src/task/executor.ts +12 -3
  183. package/src/task/index.ts +8 -0
  184. package/src/task/receipt.ts +7 -2
  185. package/src/task/types.ts +26 -0
  186. package/src/tools/ask-answer-registry.ts +25 -0
  187. package/src/tools/ask.ts +355 -33
  188. package/src/tools/browser/tab-supervisor.ts +63 -6
  189. package/src/tools/browser.ts +8 -3
  190. package/src/tools/index.ts +81 -6
  191. package/src/tools/sqlite-reader.ts +8 -4
  192. package/src/tui/output-block.ts +18 -6
  193. package/src/utils/external-editor.ts +6 -1
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
4
  import { getWorktreesDir } from "@gajae-code/utils/dirs";
5
+ import { SPAWN_PROVENANCE_ENV } from "../notifications/config";
5
6
  import type { WorkflowHudSummary } from "../skill-state/active-state";
6
7
  import { buildTeamHudSummary as buildWorkflowTeamHudSummary } from "../skill-state/workflow-hud";
7
8
  import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
@@ -2083,6 +2084,11 @@ export function buildWorkerCommand(
2083
2084
  envAssignment("GJC_TEAM_STATE_ROOT", config.state_root),
2084
2085
  envAssignment("GJC_TEAM_LEADER_CWD", config.leader.cwd),
2085
2086
  envAssignment("GJC_TEAM_DISPLAY_NAME", config.display_name),
2087
+ // Canonical GJC spawn-provenance marker so `notifications.sessionScope =
2088
+ // "primary"` suppresses the worker's own notification endpoint/topic. The
2089
+ // value is informational (leader session id, falling back to the team name
2090
+ // so it is always non-blank); presence is what marks the worker.
2091
+ envAssignment(SPAWN_PROVENANCE_ENV, config.leader.session_id.trim() || config.team_name),
2086
2092
  ...(worker.worktree_path ? [envAssignment("GJC_TEAM_WORKTREE_PATH", worker.worktree_path)] : []),
2087
2093
  ];
2088
2094
  const joined = platform === "win32" ? envLines.join(" ") : envLines.join(" ");
@@ -14,6 +14,9 @@ export const GJC_TMUX_BRANCH_SLUG_OPTION = "@gjc-branch-slug";
14
14
  export const GJC_TMUX_PROJECT_OPTION = "@gjc-project";
15
15
  export const GJC_TMUX_SESSION_ID_OPTION = "@gjc-session-id";
16
16
  export const GJC_TMUX_SESSION_STATE_FILE_OPTION = "@gjc-session-state-file";
17
+ export const GJC_TMUX_OWNER_GENERATION_OPTION = "@gjc-owner-generation";
18
+ export const GJC_TMUX_OWNER_SERVER_KEY_OPTION = "@gjc-owner-server-key";
19
+
17
20
  export const GJC_TMUX_VERSION_OPTION = "@gjc-version";
18
21
  export const GJC_PSMUX_PROFILE_FORCE_ENV = "GJC_PSMUX_PROFILE_FORCE";
19
22
 
@@ -158,15 +161,13 @@ export function buildGjcTmuxRequiredProfileCommands(
158
161
  project?: string | null;
159
162
  sessionId?: string | null;
160
163
  sessionStateFile?: string | null;
164
+ ownerGeneration?: string | null;
165
+ ownerServerKey?: string | null;
161
166
  version?: string | null;
162
167
  } = {},
163
168
  ): GjcTmuxProfileCommand[] {
164
- const commands: GjcTmuxProfileCommand[] = [
165
- {
166
- description: "mark GJC tmux ownership",
167
- args: ["set-option", "-t", target, GJC_TMUX_PROFILE_OPTION, GJC_TMUX_PROFILE_VALUE],
168
- },
169
- ];
169
+ const commands: GjcTmuxProfileCommand[] = [];
170
+
170
171
  if (metadata.branch)
171
172
  commands.push({
172
173
  description: "record GJC branch identity",
@@ -192,11 +193,26 @@ export function buildGjcTmuxRequiredProfileCommands(
192
193
  description: "record GJC session state marker",
193
194
  args: ["set-option", "-t", target, GJC_TMUX_SESSION_STATE_FILE_OPTION, metadata.sessionStateFile],
194
195
  });
196
+ if (metadata.ownerGeneration)
197
+ commands.push({
198
+ description: "record GJC owner generation",
199
+ args: ["set-option", "-t", target, GJC_TMUX_OWNER_GENERATION_OPTION, metadata.ownerGeneration],
200
+ });
201
+ if (metadata.ownerServerKey)
202
+ commands.push({
203
+ description: "record GJC owner server key",
204
+ args: ["set-option", "-t", target, GJC_TMUX_OWNER_SERVER_KEY_OPTION, metadata.ownerServerKey],
205
+ });
206
+
195
207
  if (metadata.version)
196
208
  commands.push({
197
209
  description: "record GJC version identity",
198
210
  args: ["set-option", "-t", target, GJC_TMUX_VERSION_OPTION, metadata.version],
199
211
  });
212
+ commands.push({
213
+ description: "mark GJC tmux ownership",
214
+ args: ["set-option", "-t", target, GJC_TMUX_PROFILE_OPTION, GJC_TMUX_PROFILE_VALUE],
215
+ });
200
216
  return commands;
201
217
  }
202
218
 
@@ -219,6 +235,8 @@ export function buildGjcTmuxProfileCommands(
219
235
  project?: string | null;
220
236
  sessionId?: string | null;
221
237
  sessionStateFile?: string | null;
238
+ ownerGeneration?: string | null;
239
+ ownerServerKey?: string | null;
222
240
  version?: string | null;
223
241
  } = {},
224
242
  opts: { platform?: NodeJS.Platform; tmuxCommand?: string } = {},
@@ -260,7 +278,8 @@ export function buildGjcTmuxProfileCommands(
260
278
  tmuxName.endsWith("/pmux") ||
261
279
  tmuxName.endsWith("\\psmux") ||
262
280
  tmuxName.endsWith("\\pmux");
263
- const dropUx = isPsmuxClass && !envDisabled(env[GJC_PSMUX_PROFILE_FORCE_ENV]);
281
+ const forcePsmuxProfile = env[GJC_PSMUX_PROFILE_FORCE_ENV] === "true" || env[GJC_PSMUX_PROFILE_FORCE_ENV] === "1";
282
+ const dropUx = isPsmuxClass && !forcePsmuxProfile;
264
283
  if (dropUx) {
265
284
  return commands.filter(command => {
266
285
  const flag = command.args[0];
@@ -22,6 +22,37 @@ const STORE = "tmux_sessions" as const;
22
22
  const TOCTOU_SKIP = "tmux_revalidation_failed_or_became_live";
23
23
  const ORPHAN_MAX_AGE_MS = 24 * 60 * 60 * 1000;
24
24
 
25
+ type CollectedTmuxIdentity = {
26
+ nativeSessionId: string;
27
+ ownerGeneration: string;
28
+ sessionId: string;
29
+ sessionStateFile: string;
30
+ project: string;
31
+ createdAt: string;
32
+ };
33
+
34
+ const collectedIdentities = new WeakMap<GcRecord, CollectedTmuxIdentity>();
35
+
36
+ function collectedIdentity(session: GjcTmuxSessionStatus): CollectedTmuxIdentity | undefined {
37
+ if (
38
+ !session.nativeSessionId ||
39
+ !session.ownerGeneration ||
40
+ !session.sessionId ||
41
+ !session.sessionStateFile ||
42
+ !session.project ||
43
+ !session.createdAt
44
+ )
45
+ return undefined;
46
+ return {
47
+ nativeSessionId: session.nativeSessionId,
48
+ ownerGeneration: session.ownerGeneration,
49
+ sessionId: session.sessionId,
50
+ sessionStateFile: session.sessionStateFile,
51
+ project: session.project,
52
+ createdAt: session.createdAt,
53
+ };
54
+ }
55
+
25
56
  function pathExists(path: string): boolean {
26
57
  try {
27
58
  return fs.existsSync(path);
@@ -168,15 +199,21 @@ function classifyUntaggedSession(session: GjcTmuxSessionStatus): GcRecord {
168
199
  return unclassifiedRecord(session.name, "untagged_tmux_session");
169
200
  }
170
201
 
171
- async function revalidateRemovable(record: GcRecord, env: NodeJS.ProcessEnv): Promise<boolean> {
202
+ async function revalidateRemovable(
203
+ record: GcRecord,
204
+ identity: CollectedTmuxIdentity,
205
+ env: NodeJS.ProcessEnv,
206
+ ): Promise<boolean> {
172
207
  const tags = readTmuxSessionTagsForGc(record.id, env);
173
208
  if (
174
- tags.createdAt &&
175
- record.detail?.includes("createdAt=") &&
176
- !record.detail.includes(`createdAt=${tags.createdAt}`)
177
- ) {
209
+ tags.nativeSessionId !== identity.nativeSessionId ||
210
+ tags.ownerGeneration !== identity.ownerGeneration ||
211
+ tags.sessionId !== identity.sessionId ||
212
+ tags.sessionStateFile !== identity.sessionStateFile ||
213
+ tags.project !== identity.project ||
214
+ tags.createdAt !== identity.createdAt
215
+ )
178
216
  return false;
179
- }
180
217
  if (tags.attached || (tags.panePids?.length ?? 0) > 0) return false;
181
218
  if (tags.profile !== GJC_TMUX_PROFILE_VALUE) return false;
182
219
  return await hasTerminalRuntimeMarker(tags);
@@ -205,7 +242,12 @@ export const tmuxSessionsGcAdapter: GcStoreAdapter = {
205
242
 
206
243
  for (const session of sessions.tagged) {
207
244
  try {
208
- records.push(await classifyTaggedSession(session));
245
+ const record = await classifyTaggedSession(session);
246
+ records.push(record);
247
+ if (record.removable) {
248
+ const identity = collectedIdentity(session);
249
+ if (identity) collectedIdentities.set(record, identity);
250
+ }
209
251
  } catch (error) {
210
252
  errors.push({
211
253
  store: STORE,
@@ -227,10 +269,11 @@ export const tmuxSessionsGcAdapter: GcStoreAdapter = {
227
269
  return { removed: false, skipped: "not_removable_tmux_session" };
228
270
  }
229
271
  try {
230
- if (!(await revalidateRemovable(record, ctx.env))) {
272
+ const identity = collectedIdentities.get(record);
273
+ if (!identity || !(await revalidateRemovable(record, identity, ctx.env))) {
231
274
  return { removed: false, skipped: TOCTOU_SKIP };
232
275
  }
233
- removeGjcTmuxSession(record.id, ctx.env);
276
+ removeGjcTmuxSession(record.id, ctx.env, identity);
234
277
  return { removed: true };
235
278
  } catch (error) {
236
279
  return { removed: false, error: error instanceof Error ? error.message : String(error) };
@@ -0,0 +1,185 @@
1
+ /** Internal JSON-line facade for the tmux owner-isolation contract. */
2
+ import * as fs from "node:fs/promises";
3
+ import {
4
+ type BootstrapRequest,
5
+ bootstrapTmuxOwnerIsolation,
6
+ classifyCgroup,
7
+ type ObserveTerminalRequest,
8
+ type OwnerIsolationProbe,
9
+ observeOwnerTerminal,
10
+ type PlanRequest,
11
+ type PublishGenerationRequest,
12
+ parseOwnerIsolationRequest,
13
+ planTmuxOwnerIsolation,
14
+ publishOwnerGenerationSync,
15
+ serializeOwnerIsolationResponse,
16
+ TMUX_OWNER_ISOLATION_MAX_LINE_BYTES,
17
+ type TmuxServerProof,
18
+ } from "./tmux-owner-isolation";
19
+
20
+ /** Matches the sole argv shape allowed to enter the owner-isolation JSON-line protocol. */
21
+ export function isTmuxOwnerIsolationCliArgv(argv: readonly string[]): boolean {
22
+ return argv.length === 1 && argv[0] === "--internal-tmux-owner-isolation";
23
+ }
24
+
25
+ async function readCgroup(pid = "self"): Promise<string | null> {
26
+ try {
27
+ return await fs.readFile(`/proc/${pid}/cgroup`, "utf8");
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ async function readProcessStartTime(pid: number): Promise<string | null> {
34
+ try {
35
+ const stat = await fs.readFile(`/proc/${pid}/stat`, "utf8");
36
+ return (
37
+ stat
38
+ .slice(stat.lastIndexOf(")") + 2)
39
+ .trim()
40
+ .split(/\s+/)[19] ?? null
41
+ );
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function isKnownNoServerDiagnostic(stderr: string): boolean {
48
+ const diagnostic = stderr.trim().toLowerCase();
49
+ return (
50
+ diagnostic.length > 0 &&
51
+ diagnostic.length <= 512 &&
52
+ /no server running|failed to connect to server|error connecting to/.test(diagnostic)
53
+ );
54
+ }
55
+
56
+ async function tmuxServerProof(socketKey: string, tmuxControlArgv?: string[]): Promise<TmuxServerProof> {
57
+ void socketKey;
58
+ if (!tmuxControlArgv?.length) return { state: "unverifiable" };
59
+ const controlArgv = tmuxControlArgv;
60
+ const subprocess = Bun.spawnSync([...controlArgv, "list-sessions", "-F", "#{pid}\t#{session_name}"], {
61
+ stdout: "pipe",
62
+ stderr: "pipe",
63
+ });
64
+ const stderr = Buffer.from(subprocess.stderr).toString();
65
+ if (subprocess.exitCode !== 0)
66
+ return isKnownNoServerDiagnostic(stderr) ? { state: "absent" } : { state: "unverifiable" };
67
+ const lines = Buffer.from(subprocess.stdout)
68
+ .toString()
69
+ .split("\n")
70
+ .map(line => line.trim())
71
+ .filter(Boolean);
72
+ const [pidText] = lines[0]?.split("\t") ?? [];
73
+ const pid = Number(pidText);
74
+ const sessionNames = lines.map(line => line.split("\t")[1]).filter((name): name is string => Boolean(name));
75
+ if (!Number.isSafeInteger(pid) || pid <= 0) return { state: "unverifiable" };
76
+ if (process.platform !== "linux") {
77
+ return {
78
+ state: "safe",
79
+ pid,
80
+ startTime: "not_applicable",
81
+ cgroup: { classification: "not_applicable" },
82
+ sessionNames,
83
+ };
84
+ }
85
+ const cgroup = classifyCgroup({ platform: process.platform, cgroupText: await readCgroup(String(pid)) });
86
+ const startTime = await readProcessStartTime(pid);
87
+ if (!startTime) return { state: "unverifiable", pid, cgroup, sessionNames };
88
+ return {
89
+ state:
90
+ cgroup.classification === "safe"
91
+ ? "safe"
92
+ : cgroup.classification === "unsafe_service"
93
+ ? "unsafe"
94
+ : "unverifiable",
95
+ pid,
96
+ startTime,
97
+ cgroup,
98
+ sessionNames,
99
+ };
100
+ }
101
+
102
+ function probe(): OwnerIsolationProbe {
103
+ return {
104
+ readCallerCgroup: () => readCgroup(),
105
+ probeServer: async (socketKey, tmuxArgv) => tmuxServerProof(socketKey, tmuxArgv),
106
+ };
107
+ }
108
+
109
+ function cliFailure(diagnostic: string): string {
110
+ return serializeOwnerIsolationResponse({ schema_version: 1, ok: false, code: "scope_unavailable", diagnostic });
111
+ }
112
+
113
+ /** Reads exactly one bounded JSON line and writes exactly one JSON response line. */
114
+ export async function runTmuxOwnerIsolationCli(stdin: string): Promise<string> {
115
+ const line = stdin.endsWith("\n") ? stdin.slice(0, -1) : stdin;
116
+ if (line.includes("\n")) return cliFailure("invalid_json_line");
117
+ const request = parseOwnerIsolationRequest(line);
118
+ if (!request) return cliFailure("invalid_json_line");
119
+ if (request.op === "plan")
120
+ return serializeOwnerIsolationResponse(await planTmuxOwnerIsolation(request as PlanRequest, probe()));
121
+ if (request.op === "bootstrap") {
122
+ const bootstrap = request as BootstrapRequest;
123
+ return serializeOwnerIsolationResponse(
124
+ await bootstrapTmuxOwnerIsolation(bootstrap, {
125
+ readSelfCgroup: () => readCgroup(),
126
+ spawn: argv => {
127
+ const result = Bun.spawnSync(argv, { stdin: "ignore", stdout: "pipe", stderr: "ignore" });
128
+ return { exitCode: result.exitCode, stdout: Buffer.from(result.stdout).toString() };
129
+ },
130
+ probeServer: async (socketKey, tmuxControlArgv) => tmuxServerProof(socketKey, tmuxControlArgv),
131
+ }),
132
+ );
133
+ }
134
+ if (request.op === "publish_generation") {
135
+ try {
136
+ return serializeOwnerIsolationResponse(publishOwnerGenerationSync(request as PublishGenerationRequest));
137
+ } catch {
138
+ return cliFailure("generation_publication_failed");
139
+ }
140
+ }
141
+ try {
142
+ return serializeOwnerIsolationResponse(await observeOwnerTerminal(request as ObserveTerminalRequest));
143
+ } catch {
144
+ return cliFailure("terminal_observation_failed");
145
+ }
146
+ }
147
+
148
+ const MAX_JSON_LINE_BYTES = TMUX_OWNER_ISOLATION_MAX_LINE_BYTES;
149
+
150
+ async function readOneBoundedJsonLine(): Promise<string | null> {
151
+ const reader = Bun.stdin.stream().getReader();
152
+ const bytes: number[] = [];
153
+ let complete = false;
154
+ try {
155
+ while (true) {
156
+ const { done, value } = await reader.read();
157
+ if (done) break;
158
+ for (const byte of value) {
159
+ if (complete || (byte !== 0x0a && bytes.length >= MAX_JSON_LINE_BYTES)) return null;
160
+ if (byte === 0x0a) {
161
+ complete = true;
162
+ continue;
163
+ }
164
+ bytes.push(byte);
165
+ }
166
+ }
167
+ } finally {
168
+ reader.releaseLock();
169
+ }
170
+ return new TextDecoder().decode(new Uint8Array(bytes));
171
+ }
172
+
173
+ export async function runTmuxOwnerIsolationCliFromStdin(): Promise<void> {
174
+ const stdin = await readOneBoundedJsonLine();
175
+ const output = stdin === null ? cliFailure("invalid_json_line") : await runTmuxOwnerIsolationCli(stdin);
176
+ process.stdout.write(`${output}\n`);
177
+ const request = stdin === null ? null : parseOwnerIsolationRequest(stdin);
178
+ if (request?.op === "bootstrap") {
179
+ try {
180
+ if (JSON.parse(output).ok !== true) process.exitCode = 1;
181
+ } catch {
182
+ process.exitCode = 1;
183
+ }
184
+ }
185
+ }