@timurproko/a1 0.2.7-dev.788 → 0.2.7-dev.794

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 (135) hide show
  1. package/dist/app/session-shell/session-shell-root.d.ts +2 -1
  2. package/dist/app/session-shell/session-shell-root.js +41 -15
  3. package/dist/app/session-shell/session-shell.d.ts +1 -0
  4. package/dist/app/session-shell/session-shell.js +15 -4
  5. package/dist/app/session-shell/session-viewport-controller.d.ts +2 -2
  6. package/dist/app/session-shell/session-viewport-controller.js +24 -12
  7. package/dist/cli/tabs.js +1 -1
  8. package/dist/composition/owned-ui.d.ts +2 -0
  9. package/dist/composition/owned-ui.js +42 -4
  10. package/dist/contracts/owned-ui/index.d.ts +1 -1
  11. package/dist/contracts/owned-ui/model.d.ts +3 -0
  12. package/dist/features/owned-ui/settings-app.js +9 -6
  13. package/dist/features/resident-tab/index.d.ts +3 -1
  14. package/dist/features/resident-tab/index.js +5 -1
  15. package/dist/features/resident-tab/shortcut-bindings-file.d.ts +8 -0
  16. package/dist/features/resident-tab/shortcut-bindings-file.js +40 -0
  17. package/dist/features/resident-tab/shortcut-declarations.d.ts +13 -0
  18. package/dist/features/resident-tab/{shortcuts.js → shortcut-declarations.js} +6 -17
  19. package/dist/features/resident-tab/shortcut-projection.d.ts +29 -0
  20. package/dist/features/resident-tab/shortcut-projection.js +64 -0
  21. package/dist/foundation/launch-guardian/exit-watcher.d.ts +14 -0
  22. package/dist/foundation/launch-guardian/exit-watcher.js +94 -0
  23. package/dist/foundation/launch-guardian/main.d.ts +2 -1
  24. package/dist/foundation/launch-guardian/main.js +58 -6
  25. package/dist/foundation/process-containment/{native-guardian-containment.d.ts → addon-containment.d.ts} +7 -5
  26. package/dist/foundation/process-containment/addon-containment.js +139 -0
  27. package/dist/foundation/process-containment/addon-process-inspector.d.ts +11 -0
  28. package/dist/foundation/process-containment/addon-process-inspector.js +36 -0
  29. package/dist/foundation/process-containment/addon.d.ts +42 -0
  30. package/dist/foundation/process-containment/addon.js +57 -0
  31. package/dist/foundation/process-containment/artifact.d.ts +1 -1
  32. package/dist/foundation/process-containment/artifact.js +6 -5
  33. package/dist/foundation/process-containment/contracts.d.ts +2 -0
  34. package/dist/foundation/process-containment/index.d.ts +4 -4
  35. package/dist/foundation/process-containment/index.js +3 -3
  36. package/dist/foundation/process-containment/native-process-inspector.d.ts +2 -1
  37. package/dist/foundation/process-containment/native-process-inspector.js +10 -10
  38. package/dist/foundation/release/bootstrap.js +54 -16
  39. package/dist/foundation/release/index.d.ts +1 -1
  40. package/dist/foundation/release/index.js +1 -1
  41. package/dist/foundation/release/release-store.d.ts +12 -0
  42. package/dist/foundation/release/release-store.js +124 -2
  43. package/dist/foundation/resident-tabs/bounded-host-command.d.ts +3 -1
  44. package/dist/foundation/resident-tabs/bounded-host-command.js +6 -2
  45. package/dist/foundation/resident-tabs/bridge-client.d.ts +7 -0
  46. package/dist/foundation/resident-tabs/bridge-client.js +30 -1
  47. package/dist/foundation/resident-tabs/doctor.d.ts +0 -1
  48. package/dist/foundation/resident-tabs/doctor.js +4 -4
  49. package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +7 -2
  50. package/dist/foundation/resident-tabs/ensure-terminal-host.js +15 -11
  51. package/dist/foundation/resident-tabs/host-entry.d.ts +1 -0
  52. package/dist/foundation/resident-tabs/host-entry.js +17 -0
  53. package/dist/foundation/resident-tabs/index.d.ts +2 -2
  54. package/dist/foundation/resident-tabs/index.js +1 -1
  55. package/dist/foundation/resident-tabs/maintenance.d.ts +5 -4
  56. package/dist/foundation/resident-tabs/maintenance.js +32 -17
  57. package/dist/foundation/resident-tabs/prompt-journal.d.ts +0 -2
  58. package/dist/foundation/resident-tabs/prompt-journal.js +0 -1
  59. package/dist/foundation/resident-tabs/release-holds.d.ts +3 -2
  60. package/dist/foundation/resident-tabs/release-holds.js +16 -7
  61. package/dist/foundation/resident-tabs/settings.d.ts +0 -1
  62. package/dist/foundation/resident-tabs/settings.js +1 -7
  63. package/dist/foundation/resident-tabs/terminal-host-artifact.d.ts +11 -15
  64. package/dist/foundation/resident-tabs/terminal-host-artifact.js +57 -32
  65. package/dist/foundation/session-writer-lock/index.d.ts +1 -3
  66. package/dist/foundation/session-writer-lock/index.js +8 -6
  67. package/dist/integrations/pi/components/owned-editor-ux.d.ts +2 -0
  68. package/dist/integrations/pi/components/owned-editor-ux.js +13 -0
  69. package/dist/integrations/pi/components/shell-editor-autocomplete.js +3 -1
  70. package/dist/integrations/pi/components/shell-presenters-info.d.ts +1 -0
  71. package/dist/integrations/pi/components/shell-presenters-info.js +10 -30
  72. package/dist/integrations/pi/components/shell-shared-facade.d.ts +4 -0
  73. package/dist/native/darwin-arm64/manifest.json +10 -7
  74. package/dist/native/darwin-arm64/process-guardian.node +0 -0
  75. package/dist/native/darwin-arm64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
  76. package/dist/native/darwin-arm64/terminal-host.manifest.json +16 -14
  77. package/dist/native/darwin-arm64/terminal-host.node +0 -0
  78. package/dist/native/darwin-x64/manifest.json +10 -7
  79. package/dist/native/darwin-x64/process-guardian.node +0 -0
  80. package/dist/native/darwin-x64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
  81. package/dist/native/darwin-x64/terminal-host.manifest.json +16 -14
  82. package/dist/native/darwin-x64/terminal-host.node +0 -0
  83. package/dist/native/linux-x64/manifest.json +10 -7
  84. package/dist/native/linux-x64/process-guardian.node +0 -0
  85. package/dist/native/linux-x64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
  86. package/dist/native/linux-x64/terminal-host.manifest.json +16 -14
  87. package/dist/native/linux-x64/terminal-host.node +0 -0
  88. package/dist/native/win32-x64/manifest.json +10 -7
  89. package/dist/native/win32-x64/process-guardian.node +0 -0
  90. package/dist/native/win32-x64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
  91. package/dist/native/win32-x64/terminal-host.manifest.json +16 -14
  92. package/dist/native/win32-x64/terminal-host.node +0 -0
  93. package/dist/product-identity.d.ts +1 -1
  94. package/dist/product-identity.js +1 -1
  95. package/dist/product-identity.json +3 -2
  96. package/dist/runtime-payload-inventory.json +1 -0
  97. package/dist/ui/components/index.d.ts +2 -2
  98. package/dist/ui/components/index.js +1 -1
  99. package/dist/ui/components/selection-copy.d.ts +2 -0
  100. package/dist/ui/components/selection-copy.js +13 -0
  101. package/dist/ui/components/submitted-prompt.d.ts +7 -4
  102. package/dist/ui/components/submitted-prompt.js +18 -10
  103. package/dist/ui/components/transcript-viewport.d.ts +10 -6
  104. package/dist/ui/components/transcript-viewport.js +28 -13
  105. package/dist/ui/settings/declarations.d.ts +13 -13
  106. package/dist/ui/settings/declarations.js +12 -11
  107. package/dist/ui/settings/migrations.js +7 -0
  108. package/docs/architecture/boundaries.md +10 -9
  109. package/docs/architecture/evidence/resident-tabs/macos-x64.md +1 -1
  110. package/docs/architecture/process-guardian-provenance.md +17 -11
  111. package/docs/architecture/project-structure.md +8 -0
  112. package/docs/architecture/resident-tabs.md +54 -36
  113. package/docs/architecture/terminal-host-provenance.md +6 -4
  114. package/docs/architecture/toolchain.md +6 -6
  115. package/docs/openspec-archive-automation.md +3 -3
  116. package/docs/validation.md +7 -3
  117. package/package.json +1 -1
  118. package/dist/features/resident-tab/shortcuts.d.ts +0 -10
  119. package/dist/foundation/process-containment/darwin-process-inspector.d.ts +0 -5
  120. package/dist/foundation/process-containment/darwin-process-inspector.js +0 -7
  121. package/dist/foundation/process-containment/native-guardian-containment.js +0 -152
  122. package/dist/foundation/process-containment/windows-process-inspector.d.ts +0 -19
  123. package/dist/foundation/process-containment/windows-process-inspector.js +0 -56
  124. package/dist/native/darwin-arm64/a1_session_lock.node +0 -0
  125. package/dist/native/darwin-arm64/process-guardian +0 -0
  126. package/dist/native/darwin-arm64/terminal-host +0 -0
  127. package/dist/native/darwin-x64/a1_session_lock.node +0 -0
  128. package/dist/native/darwin-x64/process-guardian +0 -0
  129. package/dist/native/darwin-x64/terminal-host +0 -0
  130. package/dist/native/linux-x64/a1_session_lock.node +0 -0
  131. package/dist/native/linux-x64/process-guardian +0 -0
  132. package/dist/native/linux-x64/terminal-host +0 -0
  133. package/dist/native/win32-x64/a1_session_lock.node +0 -0
  134. package/dist/native/win32-x64/process-guardian.exe +0 -0
  135. package/dist/native/win32-x64/terminal-host.exe +0 -0
@@ -1,14 +1,19 @@
1
- import { GLOBAL_SCOPE, ShortcutRegistry } from "../../ui/components/index.js";
2
1
  const jumps = Array.from({ length: 10 }, (_, index) => ({
3
2
  id: `tabs.jump${index + 1}`,
4
3
  defaultKey: `alt+${(index + 1) % 10}`,
5
4
  scope: "global",
6
5
  description: `View A1 tab ${index + 1}`,
7
6
  }));
7
+ /**
8
+ * The tab shortcut declarations, in the order that decides which action keeps a duplicated key.
9
+ * This module stays dependency-free so startup presentation and the resident projection can
10
+ * read it without loading the component registry.
11
+ */
8
12
  export const RESIDENT_TAB_SHORTCUTS = Object.freeze([
9
13
  { id: "tabs.new", defaultKey: "alt+a", scope: "global", description: "New A1 tab" },
10
14
  { id: "tabs.close", defaultKey: "alt+w", scope: "global", description: "Close A1 tab" },
11
15
  { id: "tabs.rename", defaultKey: "f2", scope: "global", description: "Rename A1 tab" },
16
+ { id: "tabs.settings", defaultKey: "alt+.", scope: "global", description: "Open Settings tab" },
12
17
  ...jumps,
13
18
  { id: "tabs.next", defaultKey: null, scope: "global", description: "View next A1 tab" },
14
19
  { id: "tabs.previous", defaultKey: null, scope: "global", description: "View previous A1 tab" },
@@ -17,19 +22,3 @@ export const RESIDENT_TAB_SHORTCUTS = Object.freeze([
17
22
  { id: "tabs.retry", defaultKey: "r", scope: "failed-banner", description: "Retry failed A1 tab" },
18
23
  { id: "tabs.fresh", defaultKey: "f", scope: "failed-banner", description: "Start failed A1 tab fresh" },
19
24
  ]);
20
- export function residentTabShortcutRegistry(assignments = {}) {
21
- const registry = new ShortcutRegistry();
22
- for (const shortcut of RESIDENT_TAB_SHORTCUTS) {
23
- const key = assignments[shortcut.id] === undefined ? shortcut.defaultKey : assignments[shortcut.id];
24
- if (!key)
25
- continue;
26
- const declaration = {
27
- key,
28
- scope: shortcut.scope === "global" ? GLOBAL_SCOPE : shortcut.scope,
29
- section: "Tabs",
30
- description: shortcut.description,
31
- };
32
- registry.declare(declaration, shortcut.id);
33
- }
34
- return registry;
35
- }
@@ -0,0 +1,29 @@
1
+ import { type ResidentTabAction } from "./shortcut-declarations.js";
2
+ /** Key assignments as `keybindings.json` and the keybinding manager express them. */
3
+ export type ShortcutBindings = Readonly<Record<string, string | readonly string[] | null | undefined>>;
4
+ export type ResidentTabGlobalAction = Exclude<ResidentTabAction, "tabs.retry" | "tabs.fresh">;
5
+ export interface ResidentTabShortcutConflict {
6
+ readonly id: ResidentTabGlobalAction;
7
+ readonly key: string;
8
+ /** `reserved` keys belong to an A1 action; `duplicate` keys were claimed by an earlier tab action. */
9
+ readonly reason: "reserved" | "duplicate";
10
+ }
11
+ /** The effective tab shortcuts the resident shell dispatches and every presentation shows. */
12
+ export interface ResidentTabShortcutProjection {
13
+ readonly assignments: Readonly<Record<ResidentTabGlobalAction, string | null>>;
14
+ readonly conflicts: readonly ResidentTabShortcutConflict[];
15
+ }
16
+ export interface ResidentTabShortcutInput {
17
+ /** The user's `keybindings.json`, including any `tabs.*` entries; `null` unbinds an action. */
18
+ readonly configured?: ShortcutBindings | undefined;
19
+ /** The effective A1 declarations (defaults plus customization) whose keys tab actions may not take. */
20
+ readonly reserved?: ShortcutBindings | undefined;
21
+ }
22
+ /** The declarations the projection covers, in the order that decides duplicate claims. */
23
+ export declare const RESIDENT_TAB_GLOBAL_SHORTCUTS: import("./shortcut-declarations.js").ResidentTabShortcut[];
24
+ /**
25
+ * Projects the effective A1 declarations and the user's customization into one tab shortcut
26
+ * assignment per action. A key an A1 action holds after customization never becomes a tab
27
+ * shortcut; between tab actions the earlier declaration keeps a duplicated key.
28
+ */
29
+ export declare function projectResidentTabShortcuts(input?: ResidentTabShortcutInput): ResidentTabShortcutProjection;
@@ -0,0 +1,64 @@
1
+ import { RESIDENT_TAB_SHORTCUTS } from "./shortcut-declarations.js";
2
+ /** The declarations the projection covers, in the order that decides duplicate claims. */
3
+ export const RESIDENT_TAB_GLOBAL_SHORTCUTS = RESIDENT_TAB_SHORTCUTS.filter(shortcut => shortcut.scope === "global");
4
+ function normalizeKey(key) {
5
+ return key.trim().toLowerCase();
6
+ }
7
+ function keysOf(value) {
8
+ if (value === null || value === undefined)
9
+ return value;
10
+ if (typeof value === "string")
11
+ return [value];
12
+ return value.filter((key) => typeof key === "string");
13
+ }
14
+ /**
15
+ * Projects the effective A1 declarations and the user's customization into one tab shortcut
16
+ * assignment per action. A key an A1 action holds after customization never becomes a tab
17
+ * shortcut; between tab actions the earlier declaration keeps a duplicated key.
18
+ */
19
+ export function projectResidentTabShortcuts(input = {}) {
20
+ const configured = input.configured ?? {};
21
+ const claimedByOwnedActions = new Map();
22
+ for (const [action, value] of Object.entries(input.reserved ?? {})) {
23
+ if (action.startsWith("tabs."))
24
+ continue;
25
+ claimedByOwnedActions.set(action, keysOf(value) ?? []);
26
+ }
27
+ for (const [action, value] of Object.entries(configured)) {
28
+ if (action.startsWith("tabs.") || action === "$schema")
29
+ continue;
30
+ claimedByOwnedActions.set(action, keysOf(value) ?? []);
31
+ }
32
+ const reserved = new Set();
33
+ for (const keys of claimedByOwnedActions.values())
34
+ for (const key of keys)
35
+ reserved.add(normalizeKey(key));
36
+ const claimed = new Set();
37
+ const conflicts = [];
38
+ const assignments = {};
39
+ for (const shortcut of RESIDENT_TAB_GLOBAL_SHORTCUTS) {
40
+ const id = shortcut.id;
41
+ const custom = configured[id];
42
+ const candidates = custom === undefined
43
+ ? (shortcut.defaultKey === null ? [] : [shortcut.defaultKey])
44
+ : (keysOf(custom) ?? []);
45
+ let assigned = null;
46
+ for (const candidate of candidates.map(normalizeKey)) {
47
+ if (candidate.length === 0)
48
+ continue;
49
+ if (reserved.has(candidate)) {
50
+ conflicts.push({ id, key: candidate, reason: "reserved" });
51
+ continue;
52
+ }
53
+ if (claimed.has(candidate)) {
54
+ conflicts.push({ id, key: candidate, reason: "duplicate" });
55
+ continue;
56
+ }
57
+ assigned = candidate;
58
+ claimed.add(candidate);
59
+ break;
60
+ }
61
+ assignments[id] = assigned;
62
+ }
63
+ return { assignments, conflicts };
64
+ }
@@ -0,0 +1,14 @@
1
+ import { type ProcessGuardianAddon } from "../process-containment/index.js";
2
+ interface ExitWatcherOptions {
3
+ readonly guardianPid: number;
4
+ readonly noticePath: string;
5
+ readonly encodedModes: string;
6
+ readonly addonPath: string;
7
+ readonly runtimePid: number;
8
+ readonly processGroup: number | null;
9
+ readonly foregroundGroup: number | null;
10
+ }
11
+ /** Waits outside launch containment, then performs the cleanup its dead guardian could not. */
12
+ export declare function runExitWatcher(arguments_: readonly string[], platform?: NodeJS.Platform, loadAddon?: (path: string) => ProcessGuardianAddon): Promise<number>;
13
+ export declare function exitWatcherArguments(options: ExitWatcherOptions): string[];
14
+ export {};
@@ -0,0 +1,94 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { resolve } from "node:path";
3
+ import { loadProcessGuardianAddon } from "../process-containment/index.js";
4
+ /** Waits outside launch containment, then performs the cleanup its dead guardian could not. */
5
+ export async function runExitWatcher(arguments_, platform = process.platform, loadAddon = loadProcessGuardianAddon) {
6
+ const options = parseArguments(arguments_);
7
+ const addon = loadAddon(options.addonPath);
8
+ if (platform === "win32")
9
+ addon.ignoreConsoleControl();
10
+ else {
11
+ process.on("SIGINT", ignoreSignal);
12
+ process.on("SIGTTOU", ignoreSignal);
13
+ }
14
+ try {
15
+ await addon.waitForProcessExit(options.guardianPid);
16
+ if (platform === "win32") {
17
+ // Rationale: job termination is asynchronous; keep the runtime's final frame ahead of the notice.
18
+ await Promise.race([
19
+ addon.waitForProcessExit(options.runtimePid),
20
+ delay(2_000),
21
+ ]);
22
+ }
23
+ else if (options.processGroup !== null) {
24
+ addon.stopProcessGroup(options.processGroup);
25
+ if (options.foregroundGroup !== null)
26
+ addon.restoreForegroundGroup(options.foregroundGroup);
27
+ }
28
+ addon.deliverExitNotice(options.noticePath, options.encodedModes);
29
+ return 0;
30
+ }
31
+ finally {
32
+ if (platform !== "win32") {
33
+ process.off("SIGINT", ignoreSignal);
34
+ process.off("SIGTTOU", ignoreSignal);
35
+ }
36
+ }
37
+ }
38
+ export function exitWatcherArguments(options) {
39
+ return [
40
+ "--guardian-pid", String(options.guardianPid),
41
+ "--notice", options.noticePath,
42
+ "--modes", options.encodedModes,
43
+ "--addon", options.addonPath,
44
+ "--runtime-pid", String(options.runtimePid),
45
+ ...(options.processGroup === null ? [] : ["--process-group", String(options.processGroup)]),
46
+ ...(options.foregroundGroup === null ? [] : ["--foreground-group", String(options.foregroundGroup)]),
47
+ ];
48
+ }
49
+ function parseArguments(arguments_) {
50
+ const values = new Map();
51
+ for (let index = 0; index < arguments_.length; index += 2) {
52
+ const name = arguments_[index];
53
+ const value = arguments_[index + 1];
54
+ if (!name?.startsWith("--") || value === undefined || values.has(name))
55
+ throw new TypeError("exit watcher arguments are malformed");
56
+ values.set(name, value);
57
+ }
58
+ const guardianPid = positiveInteger(values.get("--guardian-pid"), "guardian PID");
59
+ const runtimePid = positiveInteger(values.get("--runtime-pid"), "runtime PID");
60
+ const noticePath = boundedValue(values.get("--notice"), "notice path");
61
+ const encodedModes = boundedValue(values.get("--modes"), "terminal modes", 32_768);
62
+ const addonPath = boundedValue(values.get("--addon"), "addon path");
63
+ const processGroup = optionalPositiveInteger(values.get("--process-group"), "process group");
64
+ const foregroundGroup = optionalPositiveInteger(values.get("--foreground-group"), "foreground group");
65
+ const expected = 5 + (processGroup === null ? 0 : 1) + (foregroundGroup === null ? 0 : 1);
66
+ if (values.size !== expected)
67
+ throw new TypeError("exit watcher received an unknown argument");
68
+ return { guardianPid, noticePath, encodedModes, addonPath, runtimePid, processGroup, foregroundGroup };
69
+ }
70
+ function positiveInteger(value, name) {
71
+ const parsed = Number(value);
72
+ if (!Number.isSafeInteger(parsed) || parsed <= 0)
73
+ throw new TypeError(`exit watcher ${name} must be a positive safe integer`);
74
+ return parsed;
75
+ }
76
+ function optionalPositiveInteger(value, name) {
77
+ return value === undefined ? null : positiveInteger(value, name);
78
+ }
79
+ function boundedValue(value, name, limit = 4_096) {
80
+ if (!value || value.length > limit || value.includes("\0"))
81
+ throw new TypeError(`exit watcher ${name} is invalid`);
82
+ return value;
83
+ }
84
+ function delay(milliseconds) {
85
+ return new Promise(resolvePromise => setTimeout(resolvePromise, milliseconds));
86
+ }
87
+ function ignoreSignal() { }
88
+ const invokedPath = process.argv[1] ? resolve(process.argv[1]) : null;
89
+ if (invokedPath === fileURLToPath(import.meta.url)) {
90
+ void runExitWatcher(process.argv.slice(2)).then(code => process.exit(code), error => {
91
+ process.stderr.write(`a1 exit watcher: ${error instanceof Error ? error.message : String(error)}\n`);
92
+ process.exit(2);
93
+ });
94
+ }
@@ -1,6 +1,6 @@
1
1
  import type { LaunchInstanceStopIntent, LaunchProfileId, SupervisorCommand } from "../lifecycle/index.js";
2
2
  import { type SessionSelection } from "../lifecycle/index.js";
3
- import { type NativeProcessInspector, type ProcessContainment } from "../process-containment/index.js";
3
+ import { type NativeProcessInspector, type ProcessContainment, type ProcessGuardianAddon } from "../process-containment/index.js";
4
4
  interface GuardianControl {
5
5
  connect(endpoint: string, timeoutMs?: number): Promise<unknown>;
6
6
  command(command: SupervisorCommand): Promise<{
@@ -32,6 +32,7 @@ export interface LaunchGuardianOptions {
32
32
  readonly inspector?: NativeProcessInspector;
33
33
  readonly control?: GuardianControl;
34
34
  readonly containment?: ProcessContainment;
35
+ readonly addon?: ProcessGuardianAddon;
35
36
  readonly ensureHelper?: (path: string) => Promise<void>;
36
37
  }
37
38
  export declare function runLaunchGuardian(options: LaunchGuardianOptions): Promise<number>;
@@ -1,11 +1,14 @@
1
+ import { spawn } from "node:child_process";
1
2
  import { randomUUID } from "node:crypto";
3
+ import { fileURLToPath } from "node:url";
2
4
  import { PRODUCT_TEXT } from "../../product-identity.js";
3
5
  import { readLaunchContext } from "../launch-context/index.js";
4
6
  import { resolve } from "node:path";
5
7
  import { assertLaunchProfileId, resolveCohortEndpoint, resolveProductPaths, sessionSelectionArguments } from "../lifecycle/index.js";
6
- import { closeVerifiedContainment, DarwinNativeProcessInspector, LinuxNativeProcessInspector, NativeGuardianContainment, resolveProcessGuardianPath, verifyProcessGuardianArtifact, WindowsNativeProcessInspector, } from "../process-containment/index.js";
8
+ import { AddonContainment, AddonProcessInspector, closeVerifiedContainment, LinuxNativeProcessInspector, loadProcessGuardianAddon, resolveProcessGuardianPath, verifyProcessGuardianArtifact, } from "../process-containment/index.js";
7
9
  import { SupervisorClient } from "../protocol/index.js";
8
10
  import { markStartupPhase } from "../startup/index.js";
11
+ import { exitWatcherArguments } from "./exit-watcher.js";
9
12
  export async function runLaunchGuardian(options) {
10
13
  assertLaunchProfileId(options.profileId);
11
14
  const sessionArgs = sessionSelectionArguments(options.sessionSelection);
@@ -15,8 +18,11 @@ export async function runLaunchGuardian(options) {
15
18
  const paths = resolveProductPaths(environment);
16
19
  const instanceId = randomUUID();
17
20
  const helperPath = options.helperPath ?? resolveProcessGuardianPath(options.releaseRoot, environment);
18
- const inspector = options.inspector ?? platformInspector(helperPath);
19
21
  await (options.ensureHelper ?? verifyProcessGuardianArtifact)(helperPath);
22
+ // Rationale: tests that inject both process boundaries need no native module; every production
23
+ // path loads the exact artifact only after its manifest and digest have been verified.
24
+ const addon = options.addon ?? (options.containment && options.inspector ? null : loadProcessGuardianAddon(helperPath));
25
+ const inspector = options.inspector ?? platformInspector(requireAddon(addon));
20
26
  const guardianIdentity = await inspector.observe(process.pid);
21
27
  if (!guardianIdentity)
22
28
  throw diagnosticError("launch guardian cannot verify its own native process identity", "PROCESS_IDENTITY_UNAVAILABLE");
@@ -30,7 +36,8 @@ export async function runLaunchGuardian(options) {
30
36
  : paths.endpoint;
31
37
  await client.connect(endpoint);
32
38
  await markStartupPhase(environment, "guardian-connected");
33
- const containment = options.containment ?? new NativeGuardianContainment(instanceId, helperPath, resolve(paths.runtimeDir, "launch-instances", `${instanceId}.ready.json`), resolve(paths.runtimeDir, "launch-instances", `${instanceId}.exit-notice`));
39
+ const exitNoticePath = resolve(paths.runtimeDir, "launch-instances", `${instanceId}.exit-notice`);
40
+ const containment = options.containment ?? new AddonContainment(instanceId, helperPath, exitNoticePath, requireAddon(addon));
34
41
  const stop = stopRequests(client, instanceId);
35
42
  let rootIdentity = null;
36
43
  try {
@@ -43,14 +50,26 @@ export async function runLaunchGuardian(options) {
43
50
  guardianIdentity,
44
51
  });
45
52
  let handle;
53
+ let encodedModes = null;
46
54
  try {
47
55
  const rootCommand = options.rootCommand ?? { executable: process.execPath, arguments: [options.uiEntry, ...sessionArgs] };
56
+ encodedModes = addon?.captureTerminalModes() ?? null;
48
57
  handle = await containment.spawn(rootCommand.executable, [...rootCommand.arguments], {
49
58
  cwd: options.cwd ?? process.cwd(),
50
59
  environment: environmentEntries(environment),
51
60
  ...(environment.TERM ? { terminalType: environment.TERM } : {}),
52
61
  });
53
62
  rootIdentity = handle.identity;
63
+ if (addon && encodedModes !== null) {
64
+ await startExitWatcher(addon, {
65
+ addonPath: helperPath,
66
+ noticePath: exitNoticePath,
67
+ encodedModes,
68
+ runtimePid: handle.identity.pid,
69
+ processGroup: handle.processGroup ?? null,
70
+ foregroundGroup: handle.foregroundGroup ?? null,
71
+ });
72
+ }
54
73
  await requireAccepted(client, {
55
74
  type: "activate-launch-instance",
56
75
  requestId: randomUUID(),
@@ -89,6 +108,10 @@ export async function runLaunchGuardian(options) {
89
108
  return result.outcome;
90
109
  }),
91
110
  ]);
111
+ // Invariant: the root removes a clean notice. If it left the notice armed, the guardian claims,
112
+ // restores, and prints it before closing the native containment handle.
113
+ if (addon && encodedModes !== null)
114
+ addon.deliverExitNotice(exitNoticePath, encodedModes);
92
115
  await containment.close();
93
116
  if (outcome.kind !== "interrupted" || outcome.reason !== "supervisor-disconnect") {
94
117
  await completeBestEffort(client, instanceId, outcome.kind === "interrupted" || outcome.kind === "cleanup-error" ? "interrupted" : "completed", outcome);
@@ -104,15 +127,44 @@ export async function runLaunchGuardian(options) {
104
127
  client.close();
105
128
  }
106
129
  }
107
- function platformInspector(helperPath) {
130
+ async function startExitWatcher(addon, options) {
131
+ const entry = fileURLToPath(new URL("./exit-watcher.js", import.meta.url));
132
+ const arguments_ = [entry, ...exitWatcherArguments({
133
+ guardianPid: process.pid,
134
+ ...options,
135
+ })];
136
+ if (process.platform === "win32") {
137
+ addon.spawnWatcher({ executable: process.execPath, arguments: arguments_ });
138
+ return;
139
+ }
140
+ await new Promise((resolvePromise, rejectPromise) => {
141
+ const watcher = spawn(process.execPath, arguments_, {
142
+ stdio: "inherit",
143
+ detached: false,
144
+ windowsHide: false,
145
+ });
146
+ watcher.once("error", rejectPromise);
147
+ watcher.once("spawn", () => {
148
+ watcher.off("error", rejectPromise);
149
+ watcher.unref();
150
+ resolvePromise();
151
+ });
152
+ });
153
+ }
154
+ function platformInspector(addon) {
108
155
  if (process.platform === "win32")
109
- return new WindowsNativeProcessInspector(helperPath);
156
+ return new AddonProcessInspector(addon, "windows-filetime:");
110
157
  if (process.platform === "linux")
111
158
  return new LinuxNativeProcessInspector();
112
159
  if (process.platform === "darwin")
113
- return new DarwinNativeProcessInspector(helperPath);
160
+ return new AddonProcessInspector(addon, "darwin-proc-start:");
114
161
  throw diagnosticError(`launch-instance process containment is unsupported on ${process.platform}`, "CONTAINMENT_UNSUPPORTED");
115
162
  }
163
+ function requireAddon(addon) {
164
+ if (!addon)
165
+ throw diagnosticError("process guardian addon is unavailable", "CONTAINMENT_ARTIFACT_MISSING");
166
+ return addon;
167
+ }
116
168
  function stopRequests(client, instanceId) {
117
169
  let resolveStop;
118
170
  let settled = false;
@@ -1,13 +1,15 @@
1
1
  import type { NativeProcessIdentity, ProcessContainmentIdentity } from "../lifecycle/index.js";
2
+ import { type ProcessGuardianAddon } from "./addon.js";
2
3
  import type { ContainedProcessHandle, ContainedSpawnOptions, ProcessContainment } from "./contracts.js";
3
- /** Delegates one root runtime to the native guardian and exposes its verified containment identity. */
4
- export declare class NativeGuardianContainment implements ProcessContainment {
4
+ /** Owns one native containment object in the launch guardian's Node process. */
5
+ export declare class AddonContainment implements ProcessContainment {
5
6
  #private;
6
7
  readonly instanceId: string;
7
- readonly helperPath: string;
8
- readonly statusPath: string;
8
+ readonly addonPath: string;
9
9
  readonly exitNoticePath: string | null;
10
- constructor(instanceId: string, helperPath: string, statusPath: string, exitNoticePath?: string | null);
10
+ readonly addon: ProcessGuardianAddon;
11
+ private readonly platform;
12
+ constructor(instanceId: string, addonPath: string, exitNoticePath?: string | null, addon?: ProcessGuardianAddon, platform?: NodeJS.Platform);
11
13
  get identity(): ProcessContainmentIdentity;
12
14
  spawn(executable: string, arguments_: readonly string[], options: ContainedSpawnOptions): Promise<ContainedProcessHandle>;
13
15
  contains(identity: NativeProcessIdentity): Promise<boolean>;
@@ -0,0 +1,139 @@
1
+ import { constants } from "node:os";
2
+ import { mkdir, rm } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
5
+ import { loadProcessGuardianAddon } from "./addon.js";
6
+ /** Owns one native containment object in the launch guardian's Node process. */
7
+ export class AddonContainment {
8
+ #identity;
9
+ #rootIdentity = null;
10
+ #runtime = null;
11
+ #outcome = null;
12
+ #settled = false;
13
+ instanceId;
14
+ addonPath;
15
+ exitNoticePath;
16
+ addon;
17
+ platform;
18
+ constructor(instanceId, addonPath, exitNoticePath = null, addon = loadProcessGuardianAddon(addonPath), platform = process.platform) {
19
+ this.instanceId = instanceId;
20
+ this.addonPath = addonPath;
21
+ this.exitNoticePath = exitNoticePath;
22
+ this.addon = addon;
23
+ this.platform = platform;
24
+ this.#identity = { provider: "native-addon-pending", token: instanceId };
25
+ }
26
+ get identity() {
27
+ return this.#identity;
28
+ }
29
+ async spawn(executable, arguments_, options) {
30
+ if (this.#runtime)
31
+ throw new Error("process containment already has a root runtime");
32
+ if (this.exitNoticePath) {
33
+ await mkdir(dirname(this.exitNoticePath), { recursive: true, mode: 0o700 });
34
+ await rm(this.exitNoticePath, { force: true });
35
+ }
36
+ const runtime = this.addon.spawnContained({
37
+ executable,
38
+ arguments: [...arguments_],
39
+ cwd: options.cwd,
40
+ environment: {
41
+ ...options.environment,
42
+ ...(options.terminalType ? { TERM: options.terminalType } : {}),
43
+ ...(this.exitNoticePath ? { [PRODUCT_IDENTITY.environment.exitNoticePath]: this.exitNoticePath } : {}),
44
+ },
45
+ instanceId: this.instanceId,
46
+ });
47
+ assertRuntime(runtime);
48
+ this.#runtime = runtime;
49
+ this.#rootIdentity = { pid: runtime.pid, startIdentity: runtime.startIdentity };
50
+ this.#identity = { provider: runtime.containmentProvider, token: runtime.containmentToken };
51
+ const outcome = runtime.wait().then(exitCode => outcomeFromExitCode(exitCode, this.platform), error => ({
52
+ kind: "guardian-error",
53
+ message: errorMessage(error),
54
+ code: errorCode(error),
55
+ })).finally(() => { this.#settled = true; });
56
+ this.#outcome = outcome;
57
+ return {
58
+ identity: this.#rootIdentity,
59
+ outcome,
60
+ ...(runtime.processGroup === null ? {} : { processGroup: runtime.processGroup }),
61
+ ...(runtime.foregroundGroup === null ? {} : { foregroundGroup: runtime.foregroundGroup }),
62
+ };
63
+ }
64
+ async contains(identity) {
65
+ return this.#rootIdentity !== null
66
+ && identity.pid === this.#rootIdentity.pid
67
+ && identity.startIdentity === this.#rootIdentity.startIdentity;
68
+ }
69
+ async stop(force) {
70
+ const runtime = this.#runtime;
71
+ if (!runtime || this.#settled)
72
+ return;
73
+ try {
74
+ if (force)
75
+ runtime.terminate(137);
76
+ else if (this.platform === "win32")
77
+ process.kill(runtime.pid, "SIGTERM");
78
+ else
79
+ runtime.stop();
80
+ }
81
+ catch (error) {
82
+ if (errorCode(error) !== "ESRCH")
83
+ throw error;
84
+ }
85
+ }
86
+ async waitForEmpty(timeoutMs) {
87
+ if (this.#settled || !this.#outcome)
88
+ return true;
89
+ return await Promise.race([
90
+ this.#outcome.then(() => true),
91
+ new Promise(resolvePromise => {
92
+ const timer = setTimeout(() => resolvePromise(false), timeoutMs);
93
+ timer.unref();
94
+ }),
95
+ ]);
96
+ }
97
+ async close() {
98
+ const runtime = this.#runtime;
99
+ if (!runtime)
100
+ return;
101
+ runtime.close();
102
+ await this.waitForEmpty(1_500);
103
+ }
104
+ }
105
+ export function resolveProcessGuardianPath(releaseRoot, environment = process.env, platform = process.platform, architecture = process.arch) {
106
+ const override = environment[PRODUCT_IDENTITY.environment.processGuardianPath]?.trim();
107
+ if (override)
108
+ return resolve(override);
109
+ return resolve(releaseRoot, "dist", "native", `${platform}-${architecture}`, `${PRODUCT_IDENTITY.artifacts.processGuardianAddon}.node`);
110
+ }
111
+ function assertRuntime(runtime) {
112
+ if (!Number.isSafeInteger(runtime.pid) || runtime.pid <= 0
113
+ || typeof runtime.startIdentity !== "string" || runtime.startIdentity.length === 0
114
+ || typeof runtime.containmentProvider !== "string" || runtime.containmentProvider.length === 0
115
+ || typeof runtime.containmentToken !== "string" || runtime.containmentToken.length === 0
116
+ || (runtime.processGroup !== null && (!Number.isSafeInteger(runtime.processGroup) || runtime.processGroup <= 0))
117
+ || (runtime.foregroundGroup !== null && (!Number.isSafeInteger(runtime.foregroundGroup) || runtime.foregroundGroup <= 0))) {
118
+ runtime.close();
119
+ throw Object.assign(new Error("process guardian addon returned invalid containment identity"), { code: "CONTAINMENT_ARTIFACT_INCOMPATIBLE" });
120
+ }
121
+ }
122
+ function outcomeFromExitCode(exitCode, platform) {
123
+ if (!Number.isSafeInteger(exitCode) || exitCode < 0 || exitCode > 255) {
124
+ return { kind: "guardian-error", message: "process guardian addon returned an invalid exit code", code: "CONTAINMENT_OUTCOME_INVALID" };
125
+ }
126
+ if (platform !== "win32" && exitCode >= 128) {
127
+ const signalNumber = exitCode - 128;
128
+ const signal = Object.entries(constants.signals).find(([, value]) => value === signalNumber)?.[0];
129
+ if (signal)
130
+ return { kind: "signaled", signal };
131
+ }
132
+ return { kind: "exited", exitCode };
133
+ }
134
+ function errorCode(error) {
135
+ return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : null;
136
+ }
137
+ function errorMessage(error) {
138
+ return error instanceof Error ? error.message : String(error);
139
+ }
@@ -0,0 +1,11 @@
1
+ import type { NativeProcessIdentity } from "../lifecycle/index.js";
2
+ import type { ProcessGuardianAddon } from "./addon.js";
3
+ import type { NativeProcessInspector } from "./contracts.js";
4
+ /** Verifies native process identity through the process-guardian addon's bounded inspection call. */
5
+ export declare class AddonProcessInspector implements NativeProcessInspector {
6
+ readonly addon: Pick<ProcessGuardianAddon, "inspectProcessStart">;
7
+ readonly identityPrefix: string;
8
+ constructor(addon: Pick<ProcessGuardianAddon, "inspectProcessStart">, identityPrefix: string);
9
+ observe(pid: number): Promise<NativeProcessIdentity | null>;
10
+ matches(identity: NativeProcessIdentity): Promise<boolean>;
11
+ }
@@ -0,0 +1,36 @@
1
+ /** Verifies native process identity through the process-guardian addon's bounded inspection call. */
2
+ export class AddonProcessInspector {
3
+ addon;
4
+ identityPrefix;
5
+ constructor(addon, identityPrefix) {
6
+ if (!identityPrefix || identityPrefix.includes("\0"))
7
+ throw new TypeError("process identity prefix is invalid");
8
+ this.addon = addon;
9
+ this.identityPrefix = identityPrefix;
10
+ }
11
+ async observe(pid) {
12
+ if (!Number.isSafeInteger(pid) || pid <= 0)
13
+ throw new TypeError("inspected PID must be a positive safe integer");
14
+ let startIdentity;
15
+ try {
16
+ startIdentity = this.addon.inspectProcessStart(pid);
17
+ }
18
+ catch (error) {
19
+ throw Object.assign(new Error(`cannot inspect native process identity: ${errorMessage(error)}`), { code: "PROCESS_INSPECTION_FAILED" });
20
+ }
21
+ if (startIdentity === null)
22
+ return null;
23
+ if (typeof startIdentity !== "string" || startIdentity.length === 0 || startIdentity.length > 512
24
+ || startIdentity.includes("\0") || !startIdentity.startsWith(this.identityPrefix)) {
25
+ throw Object.assign(new Error("process guardian returned mismatched identity data"), { code: "PROCESS_INSPECTION_INVALID" });
26
+ }
27
+ return { pid, startIdentity };
28
+ }
29
+ async matches(identity) {
30
+ const observed = await this.observe(identity.pid);
31
+ return observed !== null && observed.startIdentity === identity.startIdentity;
32
+ }
33
+ }
34
+ function errorMessage(error) {
35
+ return error instanceof Error ? error.message : String(error);
36
+ }
@@ -0,0 +1,42 @@
1
+ export interface ProcessGuardianVersion {
2
+ readonly crateVersion: string;
3
+ readonly protocolVersion: 2;
4
+ readonly target: string;
5
+ }
6
+ export interface ProcessGuardianRuntime {
7
+ readonly pid: number;
8
+ readonly startIdentity: string;
9
+ readonly containmentProvider: string;
10
+ readonly containmentToken: string;
11
+ readonly processGroup: number | null;
12
+ readonly foregroundGroup: number | null;
13
+ wait(): Promise<number>;
14
+ terminate(code: number): void;
15
+ stop(): void;
16
+ close(): void;
17
+ }
18
+ export interface SpawnContainedOptions {
19
+ readonly executable: string;
20
+ readonly arguments: readonly string[];
21
+ readonly cwd: string;
22
+ readonly environment: Readonly<Record<string, string>>;
23
+ readonly instanceId: string;
24
+ }
25
+ export interface ProcessGuardianAddon {
26
+ version(): ProcessGuardianVersion;
27
+ inspectProcessStart(pid: number): string | null;
28
+ captureTerminalModes(): string;
29
+ restoreTerminalModes(encoded: string): void;
30
+ deliverExitNotice(path: string, encodedModes: string): void;
31
+ spawnContained(options: SpawnContainedOptions): ProcessGuardianRuntime;
32
+ spawnWatcher(options: {
33
+ readonly executable: string;
34
+ readonly arguments: readonly string[];
35
+ }): number;
36
+ waitForProcessExit(pid: number): Promise<void>;
37
+ stopProcessGroup(processGroup: number): void;
38
+ restoreForegroundGroup(processGroup: number): void;
39
+ ignoreConsoleControl(): void;
40
+ }
41
+ /** Loads one verified Node-API guardian instance per exact addon path. */
42
+ export declare function loadProcessGuardianAddon(path: string): ProcessGuardianAddon;