@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
@@ -0,0 +1,57 @@
1
+ import { createRequire } from "node:module";
2
+ import { resolve } from "node:path";
3
+ const require = createRequire(import.meta.url);
4
+ const cache = new Map();
5
+ /** Loads one verified Node-API guardian instance per exact addon path. */
6
+ export function loadProcessGuardianAddon(path) {
7
+ if (!path || path.includes("\0"))
8
+ throw new TypeError("process guardian addon path is invalid");
9
+ const exactPath = resolve(path);
10
+ const cached = cache.get(exactPath);
11
+ if (cached)
12
+ return cached;
13
+ let value;
14
+ try {
15
+ value = require(exactPath);
16
+ }
17
+ catch (error) {
18
+ const code = errorCode(error) === "MODULE_NOT_FOUND" || errorCode(error) === "ENOENT"
19
+ ? "CONTAINMENT_ARTIFACT_MISSING"
20
+ : "CONTAINMENT_ARTIFACT_INCOMPATIBLE";
21
+ throw diagnosticError(`process guardian addon could not be loaded: ${errorMessage(error)}`, code);
22
+ }
23
+ if (!isProcessGuardianAddon(value)) {
24
+ throw diagnosticError("process guardian addon exports are incompatible", "CONTAINMENT_ARTIFACT_INCOMPATIBLE");
25
+ }
26
+ let report;
27
+ try {
28
+ report = value.version();
29
+ }
30
+ catch (error) {
31
+ throw diagnosticError(`process guardian addon identity failed: ${errorMessage(error)}`, "CONTAINMENT_ARTIFACT_INCOMPATIBLE");
32
+ }
33
+ if (report.protocolVersion !== 2 || typeof report.crateVersion !== "string" || report.crateVersion.length === 0 || typeof report.target !== "string") {
34
+ throw diagnosticError("process guardian addon protocol is incompatible", "CONTAINMENT_ARTIFACT_INCOMPATIBLE");
35
+ }
36
+ cache.set(exactPath, value);
37
+ return value;
38
+ }
39
+ function isProcessGuardianAddon(value) {
40
+ if (typeof value !== "object" || value === null)
41
+ return false;
42
+ const candidate = value;
43
+ return [
44
+ "version", "inspectProcessStart", "captureTerminalModes", "restoreTerminalModes",
45
+ "deliverExitNotice", "spawnContained", "spawnWatcher", "waitForProcessExit",
46
+ "stopProcessGroup", "restoreForegroundGroup", "ignoreConsoleControl",
47
+ ].every(name => typeof candidate[name] === "function");
48
+ }
49
+ function diagnosticError(message, code) {
50
+ return Object.assign(new Error(message), { code });
51
+ }
52
+ function errorCode(error) {
53
+ return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : null;
54
+ }
55
+ function errorMessage(error) {
56
+ return error instanceof Error ? error.message : String(error);
57
+ }
@@ -1 +1 @@
1
- export declare function verifyProcessGuardianArtifact(helperPath: string, platform?: NodeJS.Platform, architecture?: string): Promise<void>;
1
+ export declare function verifyProcessGuardianArtifact(addonPath: string, platform?: NodeJS.Platform, architecture?: string): Promise<void>;
@@ -1,8 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  import { basename, dirname, resolve } from "node:path";
4
- export async function verifyProcessGuardianArtifact(helperPath, platform = process.platform, architecture = process.arch) {
5
- const manifestPath = resolve(dirname(helperPath), "manifest.json");
4
+ export async function verifyProcessGuardianArtifact(addonPath, platform = process.platform, architecture = process.arch) {
5
+ const manifestPath = resolve(dirname(addonPath), "manifest.json");
6
6
  let manifest;
7
7
  try {
8
8
  manifest = JSON.parse(await readFile(manifestPath, "utf8"));
@@ -10,15 +10,16 @@ export async function verifyProcessGuardianArtifact(helperPath, platform = proce
10
10
  catch (error) {
11
11
  throw artifactError(`process guardian manifest is missing or malformed: ${errorMessage(error)}`, "CONTAINMENT_ARTIFACT_MISSING");
12
12
  }
13
- if (manifest.schema !== "a1-process-guardian-artifact-v1" || manifest.protocolVersion !== 1
13
+ if (manifest.schema !== "a1-process-guardian-artifact-v2" || manifest.kind !== "node-addon"
14
+ || manifest.nodeApi !== 9 || manifest.protocolVersion !== 2
14
15
  || manifest.platform !== platform || manifest.architecture !== architecture
15
- || manifest.artifact?.filename !== basename(helperPath)) {
16
+ || manifest.artifact?.filename !== basename(addonPath)) {
16
17
  throw artifactError("process guardian manifest is incompatible with this platform or protocol", "CONTAINMENT_ARTIFACT_INCOMPATIBLE");
17
18
  }
18
19
  if (manifest.capability !== "supported") {
19
20
  throw artifactError(`process containment is not certified for ${platform}-${architecture}`, "CONTAINMENT_UNSUPPORTED");
20
21
  }
21
- const [bytes, metadata] = await Promise.all([readFile(helperPath), stat(helperPath)]).catch(error => {
22
+ const [bytes, metadata] = await Promise.all([readFile(addonPath), stat(addonPath)]).catch(error => {
22
23
  throw artifactError(`process guardian artifact is unavailable: ${errorMessage(error)}`, "CONTAINMENT_ARTIFACT_MISSING");
23
24
  });
24
25
  const digest = createHash("sha256").update(bytes).digest("hex");
@@ -7,6 +7,8 @@ export interface ContainedSpawnOptions {
7
7
  export interface ContainedProcessHandle {
8
8
  readonly identity: NativeProcessIdentity;
9
9
  readonly outcome: Promise<LaunchInstanceOutcome>;
10
+ readonly processGroup?: number;
11
+ readonly foregroundGroup?: number;
10
12
  }
11
13
  export interface NativeProcessInspector {
12
14
  observe(pid: number): Promise<NativeProcessIdentity | null>;
@@ -1,9 +1,9 @@
1
+ export { loadProcessGuardianAddon } from "./addon.js";
2
+ export type { ProcessGuardianAddon, ProcessGuardianRuntime, ProcessGuardianVersion } from "./addon.js";
3
+ export { AddonContainment, resolveProcessGuardianPath } from "./addon-containment.js";
4
+ export { AddonProcessInspector } from "./addon-process-inspector.js";
1
5
  export { verifyProcessGuardianArtifact } from "./artifact.js";
2
6
  export { closeVerifiedContainment } from "./contracts.js";
3
7
  export type { ContainedProcessHandle, ContainedSpawnOptions, ContainmentCloseResult, NativeProcessInspector, ProcessContainment, ProcessContainmentFactory, } from "./contracts.js";
4
- export { DarwinNativeProcessInspector } from "./darwin-process-inspector.js";
5
8
  export { LinuxNativeProcessInspector } from "./linux-process-inspector.js";
6
- export { NativeGuardianContainment, resolveProcessGuardianPath } from "./native-guardian-containment.js";
7
9
  export { createNativeProcessInspector } from "./native-process-inspector.js";
8
- export { WindowsNativeProcessInspector } from "./windows-process-inspector.js";
9
- export type { InspectorCommandResult, InspectorCommandRunner } from "./windows-process-inspector.js";
@@ -1,7 +1,7 @@
1
+ export { loadProcessGuardianAddon } from "./addon.js";
2
+ export { AddonContainment, resolveProcessGuardianPath } from "./addon-containment.js";
3
+ export { AddonProcessInspector } from "./addon-process-inspector.js";
1
4
  export { verifyProcessGuardianArtifact } from "./artifact.js";
2
5
  export { closeVerifiedContainment } from "./contracts.js";
3
- export { DarwinNativeProcessInspector } from "./darwin-process-inspector.js";
4
6
  export { LinuxNativeProcessInspector } from "./linux-process-inspector.js";
5
- export { NativeGuardianContainment, resolveProcessGuardianPath } from "./native-guardian-containment.js";
6
7
  export { createNativeProcessInspector } from "./native-process-inspector.js";
7
- export { WindowsNativeProcessInspector } from "./windows-process-inspector.js";
@@ -1,3 +1,4 @@
1
+ import type { ProcessGuardianAddon } from "./addon.js";
1
2
  import type { NativeProcessInspector } from "./contracts.js";
2
3
  /** Resolve only the platform's exact-process inspector used by cooperative session ownership. */
3
- export declare function createNativeProcessInspector(environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, releaseRoot?: string): Promise<NativeProcessInspector>;
4
+ export declare function createNativeProcessInspector(environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, releaseRoot?: string, loadAddon?: (path: string) => ProcessGuardianAddon): Promise<NativeProcessInspector>;
@@ -1,6 +1,6 @@
1
1
  import { PRODUCT_IDENTITY } from "../../product-identity.js";
2
2
  /** Resolve only the platform's exact-process inspector used by cooperative session ownership. */
3
- export async function createNativeProcessInspector(environment = process.env, platform = process.platform, releaseRoot) {
3
+ export async function createNativeProcessInspector(environment = process.env, platform = process.platform, releaseRoot, loadAddon) {
4
4
  if (platform === "linux") {
5
5
  const { LinuxNativeProcessInspector } = await import("./linux-process-inspector.js");
6
6
  return new LinuxNativeProcessInspector();
@@ -8,14 +8,14 @@ export async function createNativeProcessInspector(environment = process.env, pl
8
8
  if (platform !== "win32" && platform !== "darwin")
9
9
  throw new Error(`native process identity is unsupported on ${platform}`);
10
10
  if (releaseRoot === undefined && !environment[PRODUCT_IDENTITY.environment.processGuardianPath]) {
11
- throw new Error("native process identity helper is unavailable");
11
+ throw new Error("native process identity addon is unavailable");
12
12
  }
13
- const { resolveProcessGuardianPath } = await import("./native-guardian-containment.js");
14
- const helper = resolveProcessGuardianPath(releaseRoot ?? process.cwd(), environment, platform);
15
- if (platform === "win32") {
16
- const { WindowsNativeProcessInspector } = await import("./windows-process-inspector.js");
17
- return new WindowsNativeProcessInspector(helper);
18
- }
19
- const { DarwinNativeProcessInspector } = await import("./darwin-process-inspector.js");
20
- return new DarwinNativeProcessInspector(helper);
13
+ const [{ resolveProcessGuardianPath }, { loadProcessGuardianAddon }, { AddonProcessInspector }] = await Promise.all([
14
+ import("./addon-containment.js"),
15
+ import("./addon.js"),
16
+ import("./addon-process-inspector.js"),
17
+ ]);
18
+ const addonPath = resolveProcessGuardianPath(releaseRoot ?? process.cwd(), environment, platform);
19
+ const addon = (loadAddon ?? loadProcessGuardianAddon)(addonPath);
20
+ return new AddonProcessInspector(addon, platform === "win32" ? "windows-filetime:" : "darwin-proc-start:");
21
21
  }
@@ -12,7 +12,7 @@ import { cleanupProvenIdleOwner, processIsAlive } from "./process-cleanup.js";
12
12
  import { probeOwnership, readEndpointMetadata, removeEndpointArtifacts, sweepDeadEndpoints } from "./endpoints.js";
13
13
  import { UpdateTransactionStore } from "./update-transaction.js";
14
14
  import { selectUpdateLaunchRelease } from "./update-launch.js";
15
- import { consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease } from "./release-store.js";
15
+ import { assertCertifiedFilesPresent, consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, repairMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease } from "./release-store.js";
16
16
  import { scheduleReleaseCleanup } from "./release-gc.js";
17
17
  import { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDocument } from "./restart-certification.js";
18
18
  import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
@@ -61,11 +61,12 @@ async function runBootstrapAttempt(options, retriesRemaining) {
61
61
  });
62
62
  if (resident !== null) {
63
63
  residentContext = resident.status === "ready" ? {
64
- residentRootExecutable: resident.executable,
64
+ residentRootExecutable: resident.executable.executable,
65
65
  residentRootArguments: JSON.stringify([
66
+ resident.executable.entry,
66
67
  "attach", "--endpoint", resident.endpoint, "--cwd", process.cwd(), "--clear-exit-ms", String(CLEAR_EXIT_INTERVAL_MS),
67
- "--tabs-max", String((await residentLaunchSettings).tabsMax),
68
68
  "--profile", environment[PRODUCT_IDENTITY.environment.profileHome] ?? paths.configDir,
69
+ "--settings-file", resolve(agentProfileDir, "settings.json"),
69
70
  "--data-dir", paths.dataDir, "--runtime-dir", paths.runtimeDir,
70
71
  "--release-root", release.releaseRoot, "--node-executable", process.execPath,
71
72
  ]),
@@ -125,7 +126,23 @@ async function runBootstrapAttempt(options, retriesRemaining) {
125
126
  const installedVersion = await readInstalledVersion(options.packageRoot);
126
127
  const activeId = state.references.active;
127
128
  const active = activeId === null ? undefined : state.releases[activeId];
128
- const activeIsLaunchable = active === undefined || (options.releaseIsLaunchable?.(active.releaseRoot) ?? true);
129
+ let activeIsLaunchable = active === undefined || (options.releaseIsLaunchable?.(active.releaseRoot) ?? true);
130
+ let excludedRetainedReleaseId;
131
+ if (activeIsLaunchable && active?.approval === "approved" && active.packageVersion === installedVersion) {
132
+ try {
133
+ await assertCertifiedFilesPresent(active.releaseRoot);
134
+ }
135
+ catch {
136
+ try {
137
+ await repairApprovedRelease(options.packageRoot, active, paths.dataDir);
138
+ }
139
+ catch {
140
+ // Security: a damaged certified release is no longer a fallback when package bytes cannot restore it.
141
+ activeIsLaunchable = false;
142
+ excludedRetainedReleaseId = active.releaseId;
143
+ }
144
+ }
145
+ }
129
146
  if (activeIsLaunchable && active?.approval === "approved" && active.packageVersion === installedVersion) {
130
147
  const endpointMatches = endpoint?.releaseId === active.releaseId
131
148
  && endpoint.releaseRoot === active.releaseRoot
@@ -139,18 +156,27 @@ async function runBootstrapAttempt(options, retriesRemaining) {
139
156
  await removeEndpointArtifacts(endpointPaths.endpointMetadataPath, endpointPaths.endpoint);
140
157
  await markStartupPhase(environment, "durable-validation-start");
141
158
  const retained = await readRestartCertifiedRelease(active, paths.dataDir).catch(async () => {
142
- const verified = await readMaterializedRelease(active.releaseRoot, resolve(paths.dataDir, "releases"));
143
- if (verified.releaseId !== active.releaseId || verified.contentDigest !== active.contentDigest || verified.packageVersion !== active.packageVersion) {
144
- throw new Error("completely verified release differs from the approved active record");
159
+ let verified;
160
+ try {
161
+ verified = await readMaterializedRelease(active.releaseRoot, resolve(paths.dataDir, "releases"));
162
+ }
163
+ catch {
164
+ verified = await repairApprovedRelease(options.packageRoot, active, paths.dataDir);
165
+ return verified;
145
166
  }
167
+ assertApprovedReleaseMatches(verified, active);
146
168
  await certifyMaterializedRelease(verified, paths.dataDir);
147
169
  return verified;
148
- });
149
- await markStartupPhase(environment, "durable-validation-complete");
150
- await markStartupPhase(environment, "replacement-supervisor-start");
151
- await ensureSupervisor(retained, environment);
152
- await markStartupPhase(environment, "replacement-supervisor-ready");
153
- return await launchSelected(retained);
170
+ }).catch(() => null);
171
+ if (retained !== null) {
172
+ await markStartupPhase(environment, "durable-validation-complete");
173
+ await markStartupPhase(environment, "replacement-supervisor-start");
174
+ await ensureSupervisor(retained, environment);
175
+ await markStartupPhase(environment, "replacement-supervisor-ready");
176
+ return await launchSelected(retained);
177
+ }
178
+ activeIsLaunchable = false;
179
+ excludedRetainedReleaseId = active.releaseId;
154
180
  }
155
181
  }
156
182
  // Concurrency: an installation being replaced is not a reason to refuse a launch. While
@@ -162,7 +188,7 @@ async function runBootstrapAttempt(options, retriesRemaining) {
162
188
  candidate = await materializeRelease(options.packageRoot, paths.dataDir);
163
189
  }
164
190
  catch (error) {
165
- const fallback = await launchRetainedActive(state, paths, environment, launchSelected);
191
+ const fallback = await launchRetainedActive(state, paths, environment, launchSelected, excludedRetainedReleaseId);
166
192
  if (fallback === null)
167
193
  throw error;
168
194
  return fallback;
@@ -289,10 +315,10 @@ async function runBootstrapAttempt(options, retriesRemaining) {
289
315
  * certifying anything new, and reports no selection when there is nothing safe
290
316
  * to fall back to.
291
317
  */
292
- async function launchRetainedActive(state, paths, environment, launch) {
318
+ async function launchRetainedActive(state, paths, environment, launch, excludedReleaseId) {
293
319
  const activeId = state.references.active;
294
320
  const active = activeId === null ? undefined : state.releases[activeId];
295
- if (!active || active.approval !== "approved")
321
+ if (!active || active.approval !== "approved" || active.releaseId === excludedReleaseId)
296
322
  return null;
297
323
  try {
298
324
  const retained = await readCertifiedReleaseManifest(active, resolve(paths.dataDir, "releases"));
@@ -303,6 +329,18 @@ async function launchRetainedActive(state, paths, environment, launch) {
303
329
  return null;
304
330
  }
305
331
  }
332
+ async function repairApprovedRelease(packageRoot, active, dataDir) {
333
+ const repaired = await repairMaterializedRelease(packageRoot, active.releaseRoot);
334
+ assertApprovedReleaseMatches(repaired, active);
335
+ await recordParentCertifiedRelease(repaired, dataDir);
336
+ return repaired;
337
+ }
338
+ function assertApprovedReleaseMatches(release, active) {
339
+ if (release.releaseId !== active.releaseId || release.contentDigest !== active.contentDigest
340
+ || (active.packageVersion !== undefined && release.packageVersion !== active.packageVersion)) {
341
+ throw new Error("completely verified release differs from the approved active record");
342
+ }
343
+ }
306
344
  async function readInstalledVersion(packageRoot) {
307
345
  const manifest = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
308
346
  if (typeof manifest.version !== "string" || manifest.version.length === 0) {
@@ -15,7 +15,7 @@ export { PRODUCT_PACKAGE_NAME, createReleaseIdentity, deriveReleaseIdentity, dig
15
15
  export type { DiscoverReleasePayloadOptions, DiscoveredReleasePayload, ReleaseFileIdentity, ReleaseIdentity } from "./release.js";
16
16
  export { collectRelease, prepareReleaseCleanup, runBoundedReleaseCleanup, runReleaseCleanupWorker, scheduleReleaseCleanup } from "./release-gc.js";
17
17
  export type { ReleaseCleanupLimits, ReleaseCleanupOperations, ReleaseCleanupOptions, ReleaseCleanupResult, ReleaseCleanupWorkerOptions } from "./release-gc.js";
18
- export { RELEASE_MANIFEST_FILENAME, assertImmutableExecutionRoot, consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease, } from "./release-store.js";
18
+ export { RELEASE_MANIFEST_FILENAME, ReleaseRepairUnavailableError, assertCertifiedFilesPresent, assertImmutableExecutionRoot, consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, repairMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease, } from "./release-store.js";
19
19
  export type { CertifiedReleaseRecord, MaterializeReleaseOptions, MaterializedRelease, ReadCertifiedReleaseManifestOptions, ReleaseContentOperation, ReleaseContentOperationEvent, VerifyMaterializedReleaseOptions, } from "./release-store.js";
20
20
  export { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDocument, restartSealDigest } from "./restart-certification.js";
21
21
  export type { RestartSeal, RestartValidationEvent } from "./restart-certification.js";
@@ -7,7 +7,7 @@ export { listRecordedEndpoints, liveReleaseIds, probeOwnership, readEndpointMeta
7
7
  export { cleanupProvenIdleOwner, cleanupVerifiedOwner, processIsAlive } from "./process-cleanup.js";
8
8
  export { PRODUCT_PACKAGE_NAME, createReleaseIdentity, deriveReleaseIdentity, digestManifestFiles, discoverReleasePayload, packageRootFromModule, releaseFileIdentity, resolveWithin, } from "./release.js";
9
9
  export { collectRelease, prepareReleaseCleanup, runBoundedReleaseCleanup, runReleaseCleanupWorker, scheduleReleaseCleanup } from "./release-gc.js";
10
- export { RELEASE_MANIFEST_FILENAME, assertImmutableExecutionRoot, consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease, } from "./release-store.js";
10
+ export { RELEASE_MANIFEST_FILENAME, ReleaseRepairUnavailableError, assertCertifiedFilesPresent, assertImmutableExecutionRoot, consumeMaterializationProof, materializeRelease, readCertifiedReleaseManifest, readMaterializedRelease, repairMaterializedRelease, resolveReleaseEntryPoint, verifyMaterializedRelease, } from "./release-store.js";
11
11
  export { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDocument, restartSealDigest } from "./restart-certification.js";
12
12
  export { STABLE_RELEASE_SCHEMA, createStableReleaseEvidence, verifyStableRegistry } from "./stable-release.js";
13
13
  export { PACKAGE_UNLOCK_PATIENCE_MS, PRODUCT_PACKAGE, assertUpdatePerformanceBudget, createNpmProcessRunner, createUpdateLifecycleCoordinator, isPackageLockError, lockedPackageDiagnostic, planUpdateOwnership, renderUpdateProgressBar, runSelfUpdate, } from "./update.js";
@@ -31,12 +31,24 @@ export interface CertifiedReleaseRecord {
31
31
  /** Absent on records written before releases declared a handoff contract. */
32
32
  readonly launchContract?: string;
33
33
  }
34
+ /** Reports that installed package bytes cannot safely reproduce an approved release. */
35
+ export declare class ReleaseRepairUnavailableError extends Error {
36
+ readonly code: "RELEASE_REPAIR_UNAVAILABLE";
37
+ constructor(message: string, options?: ErrorOptions);
38
+ }
34
39
  /** Process-authority compatibility controls for metadata-only release loading. */
35
40
  export type ReadCertifiedReleaseManifestOptions = ReadCertifiedDependencyLayerOptions;
36
41
  export declare function materializeRelease(packageRoot: string, dataDir: string, options?: MaterializeReleaseOptions): Promise<MaterializedRelease>;
37
42
  /** Consume proof that this exact object was freshly materialized or fully verified in this process. */
38
43
  export declare function consumeMaterializationProof(release: MaterializedRelease): boolean;
39
44
  export declare function readMaterializedRelease(releaseRoot: string, selectedStoreRoot?: string): Promise<MaterializedRelease>;
45
+ /** Fast presence check before trusted certification permits a release to skip full verification. */
46
+ export declare function assertCertifiedFilesPresent(releaseRoot: string): Promise<void>;
47
+ /**
48
+ * Restore changed or missing product payload files from the installed package without replacing the
49
+ * immutable release directory. Dependency layers retain their own verification and repair policy.
50
+ */
51
+ export declare function repairMaterializedRelease(packageRoot: string, releaseRoot: string): Promise<MaterializedRelease>;
40
52
  /**
41
53
  * Load metadata for a release whose bytes were already certified by the
42
54
  * current parent process or an authenticated live supervisor. Callers must
@@ -10,6 +10,14 @@ import { dependencyReference, materializeDependencyLayer, readCertifiedDependenc
10
10
  const RELEASE_FILE_IO_CONCURRENCY = 32;
11
11
  const certificationReady = new WeakSet();
12
12
  export const RELEASE_MANIFEST_FILENAME = PRODUCT_IDENTITY.manifest.releaseFilename;
13
+ /** Reports that installed package bytes cannot safely reproduce an approved release. */
14
+ export class ReleaseRepairUnavailableError extends Error {
15
+ code = "RELEASE_REPAIR_UNAVAILABLE";
16
+ constructor(message, options = {}) {
17
+ super(message, options);
18
+ this.name = "ReleaseRepairUnavailableError";
19
+ }
20
+ }
13
21
  export async function materializeRelease(packageRoot, dataDir, options = {}) {
14
22
  const payload = await discoverReleasePayload(packageRoot, {
15
23
  onSourceRead: (path, bytes) => options.onOperation?.({ operation: "source-read", path, bytes }),
@@ -71,7 +79,7 @@ export async function materializeRelease(packageRoot, dataDir, options = {}) {
71
79
  const releaseRoot = resolveWithin(storeRoot, identity.releaseId);
72
80
  if (await lstat(releaseRoot).catch(() => null)) {
73
81
  await rm(candidate, { recursive: true, force: true });
74
- return certificationReadyRelease(await verifyMaterializedRelease(releaseRoot, identity));
82
+ return certificationReadyRelease(await verifyOrRepairMaterializedRelease(payload.packageRoot, releaseRoot, identity));
75
83
  }
76
84
  await writeFile(resolve(candidate, RELEASE_MANIFEST_FILENAME), JSON.stringify(identity, null, 2), { mode: 0o400, flag: "wx" });
77
85
  try {
@@ -81,7 +89,7 @@ export async function materializeRelease(packageRoot, dataDir, options = {}) {
81
89
  if (!await lstat(releaseRoot).catch(() => null))
82
90
  throw error;
83
91
  await rm(candidate, { recursive: true, force: true });
84
- return certificationReadyRelease(await verifyMaterializedRelease(releaseRoot, identity));
92
+ return certificationReadyRelease(await verifyOrRepairMaterializedRelease(payload.packageRoot, releaseRoot, identity));
85
93
  }
86
94
  return certificationReadyRelease({ ...identity, releaseRoot: await realpath(releaseRoot) });
87
95
  }
@@ -102,6 +110,61 @@ export async function readMaterializedRelease(releaseRoot, selectedStoreRoot) {
102
110
  const manifest = JSON.parse(await readFile(resolve(canonical, RELEASE_MANIFEST_FILENAME), "utf8"));
103
111
  return certificationReadyRelease(await verifyMaterializedRelease(canonical, manifest, selectedStoreRoot));
104
112
  }
113
+ /** Fast presence check before trusted certification permits a release to skip full verification. */
114
+ export async function assertCertifiedFilesPresent(releaseRoot) {
115
+ const canonical = await realpath(releaseRoot);
116
+ const manifest = await readReleaseManifest(canonical);
117
+ await mapWithConcurrency(manifest.files, RELEASE_FILE_IO_CONCURRENCY, async (file) => {
118
+ const metadata = await lstat(resolveWithin(canonical, file.path));
119
+ if (!metadata.isFile() || metadata.isSymbolicLink())
120
+ throw new Error(`certified release file is missing: ${file.path}`);
121
+ });
122
+ }
123
+ /**
124
+ * Restore changed or missing product payload files from the installed package without replacing the
125
+ * immutable release directory. Dependency layers retain their own verification and repair policy.
126
+ */
127
+ export async function repairMaterializedRelease(packageRoot, releaseRoot) {
128
+ const canonical = await realpath(releaseRoot);
129
+ let manifest;
130
+ let payload;
131
+ try {
132
+ manifest = await readReleaseManifest(canonical);
133
+ payload = await discoverReleasePayload(packageRoot);
134
+ assertCurrentLaunchContract(payload);
135
+ }
136
+ catch (error) {
137
+ throw repairUnavailable("release repair metadata is unavailable", error);
138
+ }
139
+ if (payload.packageVersion !== manifest.packageVersion) {
140
+ throw repairUnavailable(`installed package version ${payload.packageVersion} does not match release ${manifest.packageVersion}`);
141
+ }
142
+ const payloadPaths = new Set(payload.paths.filter(path => !path.startsWith("node_modules/")));
143
+ try {
144
+ await mapWithConcurrency(manifest.files, RELEASE_FILE_IO_CONCURRENCY, async (file) => {
145
+ const destination = resolveWithin(canonical, file.path);
146
+ if (!await certifiedFileDiffers(destination, file))
147
+ return;
148
+ if (!payloadPaths.has(file.path))
149
+ throw repairUnavailable(`installed package does not carry ${file.path}`);
150
+ const source = resolveWithin(payload.packageRoot, file.path);
151
+ const sourceMetadata = await lstat(source);
152
+ if (!sourceMetadata.isFile() || sourceMetadata.isSymbolicLink())
153
+ throw repairUnavailable(`installed package file is not regular: ${file.path}`);
154
+ const bytes = payload.cachedFiles.get(file.path) ?? await readFile(source);
155
+ if (bytes.length !== file.bytes || createHash("sha256").update(bytes).digest("hex") !== file.sha256) {
156
+ throw repairUnavailable(`installed package bytes do not match release file ${file.path}`);
157
+ }
158
+ await replaceReleaseFile(destination, bytes, file);
159
+ });
160
+ return await verifyMaterializedRelease(canonical, manifest);
161
+ }
162
+ catch (error) {
163
+ if (error instanceof ReleaseRepairUnavailableError)
164
+ throw error;
165
+ throw repairUnavailable("release repair could not restore the certified payload", error);
166
+ }
167
+ }
105
168
  /**
106
169
  * Load metadata for a release whose bytes were already certified by the
107
170
  * current parent process or an authenticated live supervisor. Callers must
@@ -197,6 +260,65 @@ async function verifyFile(root, file, options) {
197
260
  if (digest !== file.sha256)
198
261
  throw new Error(`release file digest mismatch: ${file.path}`);
199
262
  }
263
+ async function verifyOrRepairMaterializedRelease(packageRoot, releaseRoot, expected) {
264
+ try {
265
+ return await verifyMaterializedRelease(releaseRoot, expected);
266
+ }
267
+ catch {
268
+ await repairMaterializedRelease(packageRoot, releaseRoot);
269
+ return await verifyMaterializedRelease(releaseRoot, expected);
270
+ }
271
+ }
272
+ async function readReleaseManifest(releaseRoot) {
273
+ const manifest = JSON.parse(await readFile(resolveWithin(releaseRoot, RELEASE_MANIFEST_FILENAME), "utf8"));
274
+ validateManifest(manifest);
275
+ if (releaseRoot.split(sep).at(-1) !== manifest.releaseId)
276
+ throw new Error(`release directory does not match identity ${manifest.releaseId}`);
277
+ return manifest;
278
+ }
279
+ async function certifiedFileDiffers(path, file) {
280
+ const metadata = await lstat(path).catch((error) => {
281
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
282
+ return null;
283
+ throw error;
284
+ });
285
+ if (metadata === null || !metadata.isFile() || metadata.isSymbolicLink() || metadata.size !== file.bytes)
286
+ return true;
287
+ if ((metadata.mode & 0o222) !== 0 || ((metadata.mode & 0o111) !== 0) !== file.executable)
288
+ return true;
289
+ return createHash("sha256").update(await readFile(path)).digest("hex") !== file.sha256;
290
+ }
291
+ async function replaceReleaseFile(path, bytes, file) {
292
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
293
+ const temporary = resolve(dirname(path), `.a1-repair-${randomUUID()}`);
294
+ const mode = file.executable ? 0o500 : 0o400;
295
+ let existing = null;
296
+ try {
297
+ await writeFile(temporary, bytes, { flag: "wx", mode: 0o600 });
298
+ existing = await lstat(path).catch((error) => {
299
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
300
+ return null;
301
+ throw error;
302
+ });
303
+ if (existing !== null) {
304
+ if (!existing.isFile() || existing.isSymbolicLink())
305
+ throw repairUnavailable(`release file cannot be safely replaced: ${file.path}`);
306
+ // Platform: Windows refuses rename-over for a read-only destination.
307
+ await chmod(path, 0o600);
308
+ }
309
+ await rename(temporary, path);
310
+ await chmod(path, mode);
311
+ }
312
+ catch (error) {
313
+ await rm(temporary, { force: true }).catch(() => { });
314
+ if (existing !== null)
315
+ await chmod(path, mode).catch(() => { });
316
+ throw error;
317
+ }
318
+ }
319
+ function repairUnavailable(message, cause) {
320
+ return new ReleaseRepairUnavailableError(message, cause === undefined ? {} : { cause });
321
+ }
200
322
  function certificationReadyRelease(release) {
201
323
  certificationReady.add(release);
202
324
  return release;
@@ -1,3 +1,4 @@
1
+ import type { TerminalHostCommand } from "./ensure-terminal-host.js";
1
2
  export interface BoundedHostCommandResult {
2
3
  readonly code: number;
3
4
  readonly stdout: string;
@@ -8,9 +9,10 @@ export interface BoundedHostCommandLimits {
8
9
  readonly timeoutMs: number;
9
10
  /** Names the exchange in the timeout error. */
10
11
  readonly label: string;
12
+ readonly environment?: NodeJS.ProcessEnv;
11
13
  }
12
14
  /**
13
15
  * Runs one terminal-host command whose report is small. An oversized report kills the command and
14
16
  * reports a failed exchange; a command that outlives its deadline is killed and rejected.
15
17
  */
16
- export declare function runBoundedHostCommand(executable: string, arguments_: readonly string[], limits: BoundedHostCommandLimits): Promise<BoundedHostCommandResult>;
18
+ export declare function runBoundedHostCommand(command: TerminalHostCommand, arguments_: readonly string[], limits: BoundedHostCommandLimits): Promise<BoundedHostCommandResult>;
@@ -3,9 +3,13 @@ import { spawn } from "node:child_process";
3
3
  * Runs one terminal-host command whose report is small. An oversized report kills the command and
4
4
  * reports a failed exchange; a command that outlives its deadline is killed and rejected.
5
5
  */
6
- export async function runBoundedHostCommand(executable, arguments_, limits) {
6
+ export async function runBoundedHostCommand(command, arguments_, limits) {
7
7
  return await new Promise((resolvePromise, rejectPromise) => {
8
- const child = spawn(executable, [...arguments_], { windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
8
+ const child = spawn(command.executable, [command.entry, ...arguments_], {
9
+ env: limits.environment,
10
+ windowsHide: true,
11
+ stdio: ["ignore", "pipe", "ignore"],
12
+ });
9
13
  const stdout = [];
10
14
  let size = 0;
11
15
  let overflowed = false;
@@ -15,6 +15,9 @@ export type TabBridgeCommand = {
15
15
  readonly type: "rename";
16
16
  readonly name: string;
17
17
  readonly nameSource: "default" | "user" | "auto";
18
+ } | {
19
+ readonly type: "route";
20
+ readonly route: "settings";
18
21
  } | {
19
22
  readonly type: "stop";
20
23
  readonly deadlineMs: number;
@@ -39,6 +42,7 @@ export interface TabBridgeOptions {
39
42
  export declare class TabBridgeClient {
40
43
  #private;
41
44
  constructor(options: TabBridgeOptions);
45
+ get tabId(): string;
42
46
  get available(): boolean;
43
47
  captureAttachment(): TabAttachment | null;
44
48
  onCommand(listener: (command: TabBridgeCommand) => void): () => void;
@@ -48,8 +52,11 @@ export declare class TabBridgeClient {
48
52
  accent(accent: string): void;
49
53
  session(file: string | null, name: string | null, nameSource?: "default" | "user" | "auto"): void;
50
54
  prompt(interrupted: boolean): void;
55
+ /** Publishes the effective tab shortcut assignments so the host dispatches and labels the same keys. */
56
+ shortcuts(assignments: Readonly<Record<string, string | null>>): void;
51
57
  requestDetach(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
52
58
  requestRestart(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
59
+ requestClose(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
53
60
  consumeControllerBoundaryInput(data: string): string;
54
61
  close(): void;
55
62
  }