@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.
Files changed (248) hide show
  1. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  2. package/dist/types/autoresearch/tools/log-experiment.d.ts +2 -2
  3. package/dist/types/cli/classify-install-target.d.ts +0 -10
  4. package/dist/types/cli/daemon-cli.d.ts +2 -0
  5. package/dist/types/cli/initial-message.d.ts +1 -1
  6. package/dist/types/cli/malloc-env-guard.d.ts +65 -0
  7. package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
  8. package/dist/types/cli/notify-cli.d.ts +3 -1
  9. package/dist/types/cli/session-picker.d.ts +4 -3
  10. package/dist/types/cli/update-cli.d.ts +22 -3
  11. package/dist/types/cli.d.ts +5 -0
  12. package/dist/types/commands/daemon.d.ts +6 -2
  13. package/dist/types/commands/launch.d.ts +1 -0
  14. package/dist/types/commands/notify.d.ts +6 -0
  15. package/dist/types/commands/web-search.d.ts +1 -1
  16. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +4 -4
  17. package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -9
  18. package/dist/types/commit/agentic/tools/schemas.d.ts +9 -9
  19. package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -9
  20. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  21. package/dist/types/commit/analysis/summary.d.ts +1 -1
  22. package/dist/types/commit/changelog/generate.d.ts +3 -3
  23. package/dist/types/commit/changelog/index.d.ts +2 -2
  24. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  25. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  26. package/dist/types/commit/shared-llm.d.ts +9 -9
  27. package/dist/types/config/keybindings.d.ts +134 -129
  28. package/dist/types/config/model-profile-activation.d.ts +1 -0
  29. package/dist/types/config/model-registry.d.ts +35 -35
  30. package/dist/types/config/models-config-schema.d.ts +111 -111
  31. package/dist/types/config/settings-schema.d.ts +56 -16
  32. package/dist/types/config/settings.d.ts +2 -0
  33. package/dist/types/coordinator/contract.d.ts +1 -1
  34. package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
  35. package/dist/types/coordinator-mcp/policy.d.ts +3 -0
  36. package/dist/types/coordinator-mcp/server.d.ts +79 -6
  37. package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
  38. package/dist/types/daemon/control-types.d.ts +10 -0
  39. package/dist/types/daemon/operator-contract.d.ts +47 -0
  40. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
  41. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +12 -12
  42. package/dist/types/discovery/helpers.d.ts +1 -1
  43. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  44. package/dist/types/edit/modes/patch.d.ts +2 -2
  45. package/dist/types/extensibility/extensions/types.d.ts +3 -1
  46. package/dist/types/goals/tools/goal-tool.d.ts +6 -6
  47. package/dist/types/i18n/messages/en.d.ts +8 -8
  48. package/dist/types/internal-urls/local-protocol.d.ts +13 -9
  49. package/dist/types/lsp/index.d.ts +9 -9
  50. package/dist/types/lsp/types.d.ts +9 -9
  51. package/dist/types/main.d.ts +66 -2
  52. package/dist/types/modes/acp/acp-agent.d.ts +19 -2
  53. package/dist/types/modes/acp/acp-mode.d.ts +12 -0
  54. package/dist/types/modes/components/assistant-message.d.ts +1 -1
  55. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  56. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  57. package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
  58. package/dist/types/modes/components/pet-selector.d.ts +11 -0
  59. package/dist/types/modes/components/sayknow-pet-widget.d.ts +59 -0
  60. package/dist/types/modes/components/session-selector.d.ts +22 -9
  61. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  62. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  63. package/dist/types/modes/components/status-line.d.ts +0 -10
  64. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  65. package/dist/types/modes/components/user-message.d.ts +2 -1
  66. package/dist/types/modes/controllers/event-controller.d.ts +4 -0
  67. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  68. package/dist/types/modes/interactive-mode.d.ts +24 -2
  69. package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
  70. package/dist/types/modes/print-mode.d.ts +3 -4
  71. package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
  72. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
  73. package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
  74. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
  75. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
  76. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
  77. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
  78. package/dist/types/modes/theme/defaults/index.d.ts +568 -568
  79. package/dist/types/modes/types.d.ts +28 -2
  80. package/dist/types/modes/utils/abort-message.d.ts +1 -1
  81. package/dist/types/modes/utils/context-usage.d.ts +4 -19
  82. package/dist/types/modes/utils/irc-message.d.ts +38 -0
  83. package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
  84. package/dist/types/notifications/config.d.ts +34 -0
  85. package/dist/types/notifications/daemon-paths.d.ts +7 -0
  86. package/dist/types/notifications/index.d.ts +27 -0
  87. package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
  88. package/dist/types/notifications/notification-service.d.ts +144 -0
  89. package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
  90. package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
  91. package/dist/types/notifications/telegram-daemon.d.ts +130 -3
  92. package/dist/types/notifications/telegram-reference.d.ts +19 -2
  93. package/dist/types/notifications/topic-registry.d.ts +24 -4
  94. package/dist/types/registry/agent-registry.d.ts +2 -0
  95. package/dist/types/rlm/index.d.ts +1 -1
  96. package/dist/types/sdk.d.ts +2 -0
  97. package/dist/types/secrets/obfuscator.d.ts +1 -1
  98. package/dist/types/session/agent-session.d.ts +40 -3
  99. package/dist/types/session/cache-economics.d.ts +79 -0
  100. package/dist/types/session/context-estimation.d.ts +31 -0
  101. package/dist/types/session/provider-safety-stop.d.ts +5 -0
  102. package/dist/types/session/session-manager.d.ts +124 -4
  103. package/dist/types/session/session-storage.d.ts +155 -13
  104. package/dist/types/setup/credential-auto-import.d.ts +2 -2
  105. package/dist/types/skc-runtime/session-state-sidecar.d.ts +34 -3
  106. package/dist/types/skc-runtime/state-schema.d.ts +19 -19
  107. package/dist/types/skc-runtime/tmux-common.d.ts +6 -0
  108. package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
  109. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +309 -0
  110. package/dist/types/skc-runtime/tmux-sessions.d.ts +49 -17
  111. package/dist/types/task/agents.d.ts +2 -2
  112. package/dist/types/task/executor.d.ts +3 -0
  113. package/dist/types/task/receipt.d.ts +3 -1
  114. package/dist/types/task/types.d.ts +35 -27
  115. package/dist/types/tools/ask-answer-registry.d.ts +5 -0
  116. package/dist/types/tools/bash.d.ts +1 -1
  117. package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
  118. package/dist/types/tools/browser.d.ts +28 -28
  119. package/dist/types/tools/computer.d.ts +20 -20
  120. package/dist/types/tools/cron.d.ts +2 -2
  121. package/dist/types/tools/debug.d.ts +40 -40
  122. package/dist/types/tools/gh.d.ts +10 -10
  123. package/dist/types/tools/image-gen.d.ts +2 -2
  124. package/dist/types/tools/index.d.ts +58 -6
  125. package/dist/types/tools/monitor.d.ts +6 -6
  126. package/dist/types/tools/skill-discovery.d.ts +4 -4
  127. package/dist/types/tools/subagent.d.ts +12 -12
  128. package/dist/types/tools/telegram-send.d.ts +7 -0
  129. package/dist/types/tools/todo-write.d.ts +8 -8
  130. package/dist/types/utils/external-editor.d.ts +2 -0
  131. package/dist/types/web/insane/bridge.d.ts +9 -9
  132. package/dist/types/web/search/index.d.ts +4 -4
  133. package/package.json +8 -8
  134. package/scripts/g004-tmux-smoke.ts +196 -36
  135. package/src/cli/daemon-cli.ts +19 -29
  136. package/src/cli/malloc-env-guard.test.ts +104 -0
  137. package/src/cli/malloc-env-guard.ts +149 -0
  138. package/src/cli/notify-cli.ts +78 -14
  139. package/src/cli/session-picker.ts +23 -40
  140. package/src/cli/update-cli.ts +56 -37
  141. package/src/cli.ts +51 -10
  142. package/src/commands/daemon.ts +19 -5
  143. package/src/commands/harness.ts +515 -47
  144. package/src/commands/launch.ts +11 -3
  145. package/src/commands/notify.ts +5 -1
  146. package/src/commands/session.ts +6 -1
  147. package/src/config/keybindings.ts +5 -0
  148. package/src/config/model-profile-activation.ts +1 -1
  149. package/src/config/model-profiles.ts +14 -14
  150. package/src/config/settings-schema.ts +41 -1
  151. package/src/config/settings.ts +96 -45
  152. package/src/coordinator/contract.ts +1 -0
  153. package/src/coordinator-mcp/model-preset.ts +100 -0
  154. package/src/coordinator-mcp/policy.ts +30 -0
  155. package/src/coordinator-mcp/server.ts +2958 -430
  156. package/src/coordinator-mcp/session-reaper.ts +129 -0
  157. package/src/daemon/control-types.ts +11 -0
  158. package/src/daemon/operator-contract.ts +121 -0
  159. package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  160. package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  161. package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  162. package/src/extensibility/extensions/runner.ts +1 -1
  163. package/src/extensibility/extensions/types.ts +3 -1
  164. package/src/internal-urls/docs-index.generated.ts +10 -9
  165. package/src/internal-urls/local-protocol.ts +29 -10
  166. package/src/main.ts +301 -80
  167. package/src/modes/acp/acp-agent.ts +730 -100
  168. package/src/modes/acp/acp-mode.ts +29 -2
  169. package/src/modes/components/assistant-message.ts +32 -3
  170. package/src/modes/components/bash-execution.ts +43 -13
  171. package/src/modes/components/custom-editor.ts +5 -0
  172. package/src/modes/components/footer.ts +3 -3
  173. package/src/modes/components/hook-selector.ts +12 -0
  174. package/src/modes/components/irc-sidebar.ts +206 -0
  175. package/src/modes/components/pet-selector.ts +55 -0
  176. package/src/modes/components/sayknow-pet-widget.ts +360 -0
  177. package/src/modes/components/session-selector.ts +222 -221
  178. package/src/modes/components/settings-selector.ts +10 -0
  179. package/src/modes/components/status-line/segments.ts +14 -7
  180. package/src/modes/components/status-line/types.ts +1 -1
  181. package/src/modes/components/status-line.ts +4 -171
  182. package/src/modes/components/tool-execution.ts +47 -6
  183. package/src/modes/components/user-message.ts +26 -9
  184. package/src/modes/controllers/command-controller.ts +28 -2
  185. package/src/modes/controllers/event-controller.ts +159 -36
  186. package/src/modes/controllers/extension-ui-controller.ts +37 -18
  187. package/src/modes/controllers/input-controller.ts +8 -2
  188. package/src/modes/controllers/selector-controller.ts +62 -15
  189. package/src/modes/interactive-mode.ts +199 -7
  190. package/src/modes/irc-observation-ledger.ts +127 -0
  191. package/src/modes/print-mode.ts +228 -92
  192. package/src/modes/rpc/rpc-client.ts +58 -1
  193. package/src/modes/rpc/rpc-mode.ts +286 -85
  194. package/src/modes/rpc/rpc-types.ts +15 -1
  195. package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
  196. package/src/modes/shared/agent-wire/command-validation.ts +10 -0
  197. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  198. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  199. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  200. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  201. package/src/modes/types.ts +34 -2
  202. package/src/modes/utils/context-usage.ts +56 -92
  203. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  204. package/src/modes/utils/irc-message.ts +138 -0
  205. package/src/modes/utils/ui-helpers.ts +216 -50
  206. package/src/notifications/config.ts +44 -0
  207. package/src/notifications/daemon-paths.ts +8 -0
  208. package/src/notifications/index.ts +535 -75
  209. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  210. package/src/notifications/notification-service.ts +710 -0
  211. package/src/notifications/rate-limit-pool.ts +46 -12
  212. package/src/notifications/telegram-daemon-control.ts +27 -2
  213. package/src/notifications/telegram-daemon.ts +635 -94
  214. package/src/notifications/telegram-reference.ts +73 -13
  215. package/src/notifications/topic-registry.ts +86 -9
  216. package/src/prompts/system/irc-peer-roster.md +1 -0
  217. package/src/prompts/system/subagent-system-prompt.md +1 -0
  218. package/src/prompts/system/system-prompt.md +1 -0
  219. package/src/prompts/tools/irc.md +1 -1
  220. package/src/registry/agent-registry.ts +3 -0
  221. package/src/sdk.ts +52 -1
  222. package/src/secrets/obfuscator.ts +4 -2
  223. package/src/session/agent-session.ts +670 -217
  224. package/src/session/cache-economics.ts +275 -0
  225. package/src/session/context-estimation.ts +91 -0
  226. package/src/session/provider-safety-stop.ts +9 -0
  227. package/src/session/session-dump-format.ts +6 -0
  228. package/src/session/session-manager.ts +809 -39
  229. package/src/session/session-storage.ts +582 -38
  230. package/src/skc-runtime/session-state-sidecar.ts +756 -135
  231. package/src/skc-runtime/tmux-common.ts +26 -7
  232. package/src/skc-runtime/tmux-gc.ts +52 -9
  233. package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
  234. package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
  235. package/src/skc-runtime/tmux-sessions.ts +871 -68
  236. package/src/slash-commands/builtin-registry.ts +153 -9
  237. package/src/slash-commands/helpers/context-report.ts +26 -4
  238. package/src/task/executor.ts +12 -3
  239. package/src/task/index.ts +8 -0
  240. package/src/task/receipt.ts +7 -2
  241. package/src/task/types.ts +26 -0
  242. package/src/tools/ask-answer-registry.ts +25 -0
  243. package/src/tools/browser/tab-supervisor.ts +63 -6
  244. package/src/tools/browser.ts +8 -3
  245. package/src/tools/index.ts +81 -6
  246. package/src/tools/sqlite-reader.ts +8 -4
  247. package/src/tui/output-block.ts +18 -6
  248. package/src/utils/external-editor.ts +6 -1
@@ -14,6 +14,9 @@ export const SKC_TMUX_BRANCH_SLUG_OPTION = "@skc-branch-slug";
14
14
  export const SKC_TMUX_PROJECT_OPTION = "@skc-project";
15
15
  export const SKC_TMUX_SESSION_ID_OPTION = "@skc-session-id";
16
16
  export const SKC_TMUX_SESSION_STATE_FILE_OPTION = "@skc-session-state-file";
17
+ export const SKC_TMUX_OWNER_GENERATION_OPTION = "@skc-owner-generation";
18
+ export const SKC_TMUX_OWNER_SERVER_KEY_OPTION = "@skc-owner-server-key";
19
+
17
20
  export const SKC_TMUX_VERSION_OPTION = "@skc-version";
18
21
  export const SKC_PSMUX_PROFILE_FORCE_ENV = "SKC_PSMUX_PROFILE_FORCE";
19
22
 
@@ -158,15 +161,13 @@ export function buildSkcTmuxRequiredProfileCommands(
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
  ): SkcTmuxProfileCommand[] {
164
- const commands: SkcTmuxProfileCommand[] = [
165
- {
166
- description: "mark SKC tmux ownership",
167
- args: ["set-option", "-t", target, SKC_TMUX_PROFILE_OPTION, SKC_TMUX_PROFILE_VALUE],
168
- },
169
- ];
169
+ const commands: SkcTmuxProfileCommand[] = [];
170
+
170
171
  if (metadata.branch)
171
172
  commands.push({
172
173
  description: "record SKC branch identity",
@@ -192,11 +193,26 @@ export function buildSkcTmuxRequiredProfileCommands(
192
193
  description: "record SKC session state marker",
193
194
  args: ["set-option", "-t", target, SKC_TMUX_SESSION_STATE_FILE_OPTION, metadata.sessionStateFile],
194
195
  });
196
+ if (metadata.ownerGeneration)
197
+ commands.push({
198
+ description: "record SKC owner generation",
199
+ args: ["set-option", "-t", target, SKC_TMUX_OWNER_GENERATION_OPTION, metadata.ownerGeneration],
200
+ });
201
+ if (metadata.ownerServerKey)
202
+ commands.push({
203
+ description: "record SKC owner server key",
204
+ args: ["set-option", "-t", target, SKC_TMUX_OWNER_SERVER_KEY_OPTION, metadata.ownerServerKey],
205
+ });
206
+
195
207
  if (metadata.version)
196
208
  commands.push({
197
209
  description: "record SKC version identity",
198
210
  args: ["set-option", "-t", target, SKC_TMUX_VERSION_OPTION, metadata.version],
199
211
  });
212
+ commands.push({
213
+ description: "mark SKC tmux ownership",
214
+ args: ["set-option", "-t", target, SKC_TMUX_PROFILE_OPTION, SKC_TMUX_PROFILE_VALUE],
215
+ });
200
216
  return commands;
201
217
  }
202
218
 
@@ -219,6 +235,8 @@ export function buildSkcTmuxProfileCommands(
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 buildSkcTmuxProfileCommands(
260
278
  tmuxName.endsWith("/pmux") ||
261
279
  tmuxName.endsWith("\\psmux") ||
262
280
  tmuxName.endsWith("\\pmux");
263
- const dropUx = isPsmuxClass && !envDisabled(env[SKC_PSMUX_PROFILE_FORCE_ENV]);
281
+ const forcePsmuxProfile = env[SKC_PSMUX_PROFILE_FORCE_ENV] === "true" || env[SKC_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: SkcTmuxSessionStatus): 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: SkcTmuxSessionStatus): 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 !== SKC_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
- removeSkcTmuxSession(record.id, ctx.env);
276
+ removeSkcTmuxSession(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
+ }