@moretea-labs/forge 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/DESIGN.md +7 -2
  3. package/README.md +7 -7
  4. package/README.zh-CN.md +5 -3
  5. package/THIRD_PARTY_NOTICES.md +1 -1
  6. package/assets/hooks/pre-edit-guard.sh +8 -39
  7. package/assets/hooks/prompt-guard.sh +2 -2
  8. package/assets/plugin-registry.v1.json +54 -3
  9. package/assets/reference-configs/hook-operations.md +1 -1
  10. package/assets/reference-configs/sprint-contracts.md +2 -2
  11. package/assets/skill-version.json +2 -2
  12. package/assets/templates/helpers/ensure-task-workflow.sh +2 -2
  13. package/assets/templates/helpers/sprint-backlog.sh +6 -6
  14. package/docs/architecture/current/automation-and-schedule-engine.md +9 -3
  15. package/docs/architecture/current/codegraph-context-provider.md +15 -3
  16. package/docs/architecture/current/dispatch-and-agent-strategy.md +32 -13
  17. package/docs/architecture/current/entity-model.md +20 -0
  18. package/docs/architecture/current/external-plugin-governance.md +3 -3
  19. package/docs/architecture/current/system-overview.md +26 -1
  20. package/docs/architecture/current/thin-harness-v1.md +10 -0
  21. package/docs/forge-chatgpt-mcp-setup.md +27 -3
  22. package/docs/operations/chatgpt-personal-assistant-runtime.md +12 -2
  23. package/docs/operations/controller-browser-plugin.md +12 -16
  24. package/docs/operations/features.md +1 -1
  25. package/docs/operations/homebrew.md +2 -2
  26. package/docs/public-usage-guide.md +1 -1
  27. package/docs/public-usage-guide.zh-CN.md +1 -1
  28. package/docs/tutorials/01-install-and-start.md +6 -4
  29. package/docs/tutorials/01-install-and-start.zh-CN.md +3 -1
  30. package/docs/tutorials/02-connect-chatgpt.md +8 -2
  31. package/docs/tutorials/02-connect-chatgpt.zh-CN.md +8 -2
  32. package/docs/wiki/Installation.md +1 -1
  33. package/docs/wiki/Quick-Start.md +1 -1
  34. package/package.json +1 -1
  35. package/scripts/check-public-docs.sh +14 -1
  36. package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
  37. package/scripts/sprint-backlog.sh +1 -1
  38. package/src/cli/commands/chatgpt.ts +31 -2
  39. package/src/cli/commands/init-hook.ts +3 -1
  40. package/src/cli/commands/mcp.ts +2 -0
  41. package/src/cli/commands/plugin.ts +36 -9
  42. package/src/cli/commands/runtime.ts +1 -0
  43. package/src/cli/commands/setup-profile.ts +49 -18
  44. package/src/cli/controller/context-pack.ts +158 -6
  45. package/src/cli/hook/prompt-guard-decision.ts +17 -18
  46. package/src/cli/hook/prompt-intents.ts +1 -1
  47. package/src/cli/local-bridge/console-assets.ts +17 -0
  48. package/src/cli/local-bridge/console-automations.ts +205 -23
  49. package/src/cli/local-bridge/console-view-models.ts +2 -0
  50. package/src/cli/local-bridge/facade-api.ts +2 -0
  51. package/src/cli/local-bridge/frontend/src/App.tsx +1 -1
  52. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +143 -3
  53. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +1 -1
  54. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +181 -3
  55. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
  56. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
  57. package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -6
  58. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
  59. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -1
  60. package/src/cli/local-bridge/frontend/src/styles.css +27 -11
  61. package/src/cli/local-bridge/frontend/src/types.ts +6 -2
  62. package/src/cli/local-bridge/server.ts +31 -20
  63. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  64. package/src/cli/local-bridge/ui-dist/app.js +5003 -4815
  65. package/src/cli/mcp/auth.ts +2 -0
  66. package/src/cli/mcp/paths.ts +8 -2
  67. package/src/cli/mcp/repository-tools.ts +124 -9
  68. package/src/cli/mcp/server.ts +164 -19
  69. package/src/cli/mcp/setup.ts +9 -1
  70. package/src/cli/mcp/transports/http.ts +52 -2
  71. package/src/cli/mcp/transports/session-registry.ts +24 -0
  72. package/src/cli/repositories/command-classifier.ts +113 -0
  73. package/src/cli/repositories/safe-patch.ts +6 -4
  74. package/src/cli/repositories/structured-git.ts +49 -7
  75. package/src/cli/repository/inspector.ts +16 -3
  76. package/src/runtime/assistant/intent.ts +3 -3
  77. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +157 -2
  78. package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
  79. package/src/runtime/control-plane/execution/validation.ts +15 -2
  80. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +32 -0
  81. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +6 -7
  82. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +2 -1
  83. package/src/runtime/control-plane/facade/capability-registry.ts +1 -1
  84. package/src/runtime/control-plane/facade/controller-session-store.ts +12 -13
  85. package/src/runtime/control-plane/facade/goal-workloop-access.ts +7 -2
  86. package/src/runtime/control-plane/facade/goal-workloop.ts +231 -8
  87. package/src/runtime/control-plane/facade/plan-contract-store.ts +32 -12
  88. package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
  89. package/src/runtime/control-plane/facade/types.ts +33 -6
  90. package/src/runtime/control-plane/facade/work-contract-store.ts +31 -2
  91. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +375 -23
  92. package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
  93. package/src/runtime/diagnostics/mcp-timing.ts +6 -0
  94. package/src/runtime/execution/process-runtime/check-facade.ts +1 -10
  95. package/src/runtime/execution/process-runtime/check-result.ts +10 -2
  96. package/src/runtime/execution/process-runtime/command-facade.ts +1 -0
  97. package/src/runtime/execution/process-runtime/gc.ts +64 -10
  98. package/src/runtime/execution/process-runtime/persisted-check.ts +10 -2
  99. package/src/runtime/execution/process-runtime/resource-claims.ts +87 -2
  100. package/src/runtime/execution/process-runtime/runtime.ts +16 -1
  101. package/src/runtime/execution/process-runtime/store.ts +6 -0
  102. package/src/runtime/gateway/mcp/execution-tools.ts +27 -12
  103. package/src/runtime/gateway/mcp/router.ts +1 -1
  104. package/src/runtime/gateway/mcp/runtime-tools.ts +482 -178
  105. package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
  106. package/src/runtime/plugins/browser-adapter.ts +19 -72
  107. package/src/runtime/plugins/browser-handoff-host.ts +6 -31
  108. package/src/runtime/plugins/browser-handoff.ts +0 -3
  109. package/src/runtime/plugins/browser-macos-bridge.ts +8 -0
  110. package/src/runtime/plugins/desktop-operator-registration.ts +8 -11
  111. package/src/runtime/plugins/external-registration.ts +84 -5
  112. package/src/runtime/plugins/first-party-registry.ts +2 -2
  113. package/src/runtime/plugins/gmail-adapter.ts +28 -2
  114. package/src/runtime/plugins/local-system-adapter.ts +2 -2
  115. package/src/runtime/plugins/plugin-management-adapter.ts +140 -0
  116. package/src/runtime/plugins/store.ts +107 -5
  117. package/src/runtime/plugins/types.ts +4 -0
  118. package/src/runtime/recovery/actions.ts +0 -10
  119. package/src/runtime/recovery/classifier.ts +0 -9
  120. package/src/runtime/recovery/maintenance-executor.ts +43 -11
  121. package/src/runtime/recovery/probe.ts +0 -1
  122. package/src/runtime/recovery/types.ts +0 -1
  123. package/src/runtime/root/package-connector-service.ts +136 -0
  124. package/src/runtime/root/package-runtime-service.ts +25 -12
  125. package/src/runtime/root/release-materialize.ts +161 -18
  126. package/src/runtime/root/service.ts +6 -4
  127. package/src/runtime/safe-tooling/index.ts +0 -1
  128. package/src/runtime/safe-tooling/types.ts +0 -30
  129. package/src/runtime/standalone-recovery/core.ts +2 -2
  130. package/src/runtime/workflow/schedules/browser-probe.ts +180 -0
  131. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
  132. package/src/runtime/workflow/schedules/engine.ts +357 -64
  133. package/src/runtime/workflow/schedules/types.ts +5 -0
  134. package/src/runtime/workflow/schedules/work-continuation.ts +100 -27
  135. package/bin/forge-desktop-helper.mjs +0 -204
  136. package/docs/operations/controller-desktop-plugin.md +0 -112
  137. package/src/runtime/plugins/desktop-adapter.ts +0 -374
  138. package/src/runtime/safe-tooling/browser-safe.ts +0 -133
@@ -0,0 +1,136 @@
1
+ import { createHash } from 'crypto';
2
+ import { closeSync, mkdirSync, openSync, renameSync, writeFileSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { basename, dirname, join, resolve } from 'path';
5
+ import { spawn, spawnSync } from 'child_process';
6
+ import { bootstrapLaunchAgentWithRetryV2, installLaunchAgent, launchAgentPath } from '../../cli/controller/launch-agents';
7
+ import type { PackageRuntimeRelease } from './package-runtime-release';
8
+
9
+ export interface PackageConnectorServicePaths {
10
+ label: string;
11
+ serviceRoot: string;
12
+ sourcePlistPath: string;
13
+ installedPlistPath: string;
14
+ stdoutPath: string;
15
+ stderrPath: string;
16
+ }
17
+
18
+ export interface PackageConnectorServiceResult {
19
+ endpoint: string;
20
+ mode: 'launchd' | 'systemd-user' | 'portable';
21
+ persistent: boolean;
22
+ servicePath?: string;
23
+ pid?: number;
24
+ }
25
+
26
+ function xml(value: string): string {
27
+ return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
28
+ }
29
+
30
+ function atomicWrite(path: string, content: string, mode = 0o600): void {
31
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
32
+ const temporary = `${path}.${process.pid}.tmp`;
33
+ writeFileSync(temporary, content, { encoding: 'utf8', mode });
34
+ renameSync(temporary, path);
35
+ }
36
+
37
+ export function packageConnectorServicePaths(controllerHome: string, accountHome = process.env.HOME ?? homedir()): PackageConnectorServicePaths {
38
+ const home = resolve(controllerHome);
39
+ const suffix = createHash('sha256').update(home).digest('hex').slice(0, 12);
40
+ const label = `com.moretea.forge.mcp-gateway.${suffix}`;
41
+ const serviceRoot = join(home, 'runtime', 'connector-service');
42
+ return {
43
+ label,
44
+ serviceRoot,
45
+ sourcePlistPath: join(serviceRoot, `${label}.plist`),
46
+ installedPlistPath: launchAgentPath(label, accountHome),
47
+ stdoutPath: join(serviceRoot, 'logs', 'stdout.log'),
48
+ stderrPath: join(serviceRoot, 'logs', 'stderr.log'),
49
+ };
50
+ }
51
+
52
+ export function packageConnectorLaunchSpec(input: { release: PackageRuntimeRelease; controllerHome: string; endpoint: string; executable?: string }): { executable: string; args: string[]; environment: Record<string, string>; port: number } {
53
+ const parsed = new URL(input.endpoint);
54
+ const port = Number(parsed.port);
55
+ if (parsed.protocol !== 'http:' || parsed.hostname !== '127.0.0.1' || parsed.pathname !== '/mcp' || !Number.isInteger(port) || port < 1 || port > 65_535) {
56
+ throw new Error('FORGE_PACKAGE_CONNECTOR_ENDPOINT_INVALID');
57
+ }
58
+ const executable = resolve(input.executable ?? process.execPath);
59
+ const packageRoot = resolve(input.release.packageRoot);
60
+ const cliEntry = join(packageRoot, 'src', 'cli', 'index.ts');
61
+ const nodeLoader = join(packageRoot, 'src', 'runtime', 'shared', 'node-ts-loader.mjs');
62
+ const isBun = Boolean(process.versions.bun) || /(?:^|[/\\-])bun(?:$|[/\\]|\.exe$)/i.test(basename(executable));
63
+ const cliArgs = [
64
+ 'mcp', 'serve', '--repo', packageRoot, '--controller-home', resolve(input.controllerHome),
65
+ '--transport', 'http', '--host', '127.0.0.1', '--port', String(port), '--profile', 'controller', '--auth', 'oauth',
66
+ ];
67
+ return {
68
+ executable,
69
+ args: isBun ? [cliEntry, ...cliArgs] : ['--loader', nodeLoader, cliEntry, ...cliArgs],
70
+ environment: { FORGE_CONTROLLER_HOME: resolve(input.controllerHome), FORGE_CONTROLLER_LIFECYCLE_OWNER: '1' },
71
+ port,
72
+ };
73
+ }
74
+
75
+ export function renderPackageConnectorLaunchAgent(input: { paths: PackageConnectorServicePaths; launch: ReturnType<typeof packageConnectorLaunchSpec> }): string {
76
+ const environmentXml = Object.entries(input.launch.environment).sort(([a], [b]) => a.localeCompare(b))
77
+ .map(([key, value]) => ` <key>${xml(key)}</key>\n <string>${xml(value)}</string>`).join('\n');
78
+ const args = [input.launch.executable, ...input.launch.args];
79
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key>\n <string>${xml(input.paths.label)}</string>\n <key>ProgramArguments</key>\n <array>\n${args.map((arg) => ` <string>${xml(arg)}</string>`).join('\n')}\n </array>\n <key>EnvironmentVariables</key>\n <dict>\n${environmentXml}\n </dict>\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <dict><key>SuccessfulExit</key><false/></dict>\n <key>ThrottleInterval</key><integer>5</integer>\n <key>StandardOutPath</key><string>${xml(input.paths.stdoutPath)}</string>\n <key>StandardErrorPath</key><string>${xml(input.paths.stderrPath)}</string>\n</dict>\n</plist>\n`;
80
+ }
81
+
82
+ export function renderPackageConnectorSystemdUserUnit(input: { launch: ReturnType<typeof packageConnectorLaunchSpec> }): string {
83
+ const quote = (value: string) => `"${value.replaceAll('%', '%%').replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
84
+ const env = Object.entries(input.launch.environment).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `Environment=${quote(`${k}=${v}`)}`);
85
+ return ['[Unit]', 'Description=Forge ChatGPT OAuth Gateway', 'After=network-online.target', '', '[Service]', 'Type=simple', `ExecStart=${[input.launch.executable, ...input.launch.args].map(quote).join(' ')}`, ...env, 'Restart=on-failure', 'RestartSec=5', '', '[Install]', 'WantedBy=default.target', ''].join('\n');
86
+ }
87
+
88
+ function installSystemd(paths: PackageConnectorServicePaths, launch: ReturnType<typeof packageConnectorLaunchSpec>, env: NodeJS.ProcessEnv): string {
89
+ const unitName = `${paths.label}.service`;
90
+ const unitPath = join(env.HOME ?? homedir(), '.config', 'systemd', 'user', unitName);
91
+ atomicWrite(unitPath, renderPackageConnectorSystemdUserUnit({ launch }), 0o644);
92
+ for (const args of [['--user', 'daemon-reload'], ['--user', 'enable', '--now', unitName]]) {
93
+ const result = spawnSync('systemctl', args, { encoding: 'utf8', env, timeout: 30_000 });
94
+ if (result.status !== 0) throw new Error(`FORGE_PACKAGE_CONNECTOR_SYSTEMD_INSTALL_FAILED: ${(result.stderr || result.stdout || '').trim()}`);
95
+ }
96
+ return unitPath;
97
+ }
98
+
99
+ function startPortable(paths: PackageConnectorServicePaths, launch: ReturnType<typeof packageConnectorLaunchSpec>, env: NodeJS.ProcessEnv): number {
100
+ mkdirSync(join(paths.serviceRoot, 'logs'), { recursive: true, mode: 0o700 });
101
+ const stdout = openSync(paths.stdoutPath, 'a', 0o600), stderr = openSync(paths.stderrPath, 'a', 0o600);
102
+ // A connector is not the Canonical Runtime writer. Never let a transient
103
+ // installer/worker write claim escape into this long-lived process; the claim
104
+ // would become stale as soon as its parent exits. Keep only the explicit
105
+ // Controller Home and lifecycle-role marker from launch.environment.
106
+ const childEnv = { ...env };
107
+ for (const key of [
108
+ 'FORGE_RUNTIME_INSTANCE_ID', 'FORGE_RUNTIME_OWNER_PID', 'FORGE_RELEASE_AUTHORITY_REVISION',
109
+ 'FORGE_RELEASE_FENCING_TOKEN', 'FORGE_RELEASE_ID', 'FORGE_ARTIFACT_IDENTITY', 'FORGE_WORKER_PROTOCOL_VERSION',
110
+ ]) delete childEnv[key];
111
+ try {
112
+ const child = spawn(launch.executable, launch.args, { detached: true, stdio: ['ignore', stdout, stderr], env: { ...childEnv, ...launch.environment } });
113
+ if (!child.pid) throw new Error('FORGE_PACKAGE_CONNECTOR_START_FAILED');
114
+ child.unref();
115
+ return child.pid;
116
+ } finally { closeSync(stdout); closeSync(stderr); }
117
+ }
118
+
119
+ export async function installPackageConnectorService(input: { release: PackageRuntimeRelease; controllerHome: string; endpoint: string; platform?: NodeJS.Platform; env?: NodeJS.ProcessEnv; forcePortable?: boolean }): Promise<PackageConnectorServiceResult> {
120
+ const paths = packageConnectorServicePaths(input.controllerHome, input.env?.HOME);
121
+ mkdirSync(join(paths.serviceRoot, 'logs'), { recursive: true, mode: 0o700 });
122
+ const launch = packageConnectorLaunchSpec({ release: input.release, controllerHome: input.controllerHome, endpoint: input.endpoint });
123
+ const platform = input.platform ?? process.platform, env = input.env ?? process.env;
124
+ if (!input.forcePortable && platform === 'darwin') {
125
+ atomicWrite(paths.sourcePlistPath, renderPackageConnectorLaunchAgent({ paths, launch }), 0o600);
126
+ installLaunchAgent(paths.sourcePlistPath, paths.label);
127
+ const result = await bootstrapLaunchAgentWithRetryV2({ label: paths.label, plistPath: paths.installedPlistPath });
128
+ if (!result.ok) throw new Error(`FORGE_PACKAGE_CONNECTOR_LAUNCHD_INSTALL_FAILED: ${result.diagnostics.join('; ')}`);
129
+ return { endpoint: input.endpoint, mode: 'launchd', persistent: true, servicePath: paths.installedPlistPath };
130
+ }
131
+ if (!input.forcePortable && platform === 'linux') {
132
+ const probe = spawnSync('systemctl', ['--user', 'show-environment'], { encoding: 'utf8', env, timeout: 30_000 });
133
+ if (probe.status === 0) return { endpoint: input.endpoint, mode: 'systemd-user', persistent: true, servicePath: installSystemd(paths, launch, env) };
134
+ }
135
+ return { endpoint: input.endpoint, mode: 'portable', persistent: false, pid: startPortable(paths, launch, env) };
136
+ }
@@ -14,6 +14,8 @@ import {
14
14
  type ForgeRuntimeServiceConfig,
15
15
  } from './service';
16
16
  import { materializePackageRuntimeRelease, type PackageRuntimeRelease } from './package-runtime-release';
17
+ import { loadMcpServiceLocalConfig } from '../../cli/mcp/auth';
18
+ import { installPackageConnectorService, type PackageConnectorServiceResult } from './package-connector-service';
17
19
 
18
20
  export type PackageRuntimeServiceMode = 'launchd' | 'systemd-user' | 'portable';
19
21
 
@@ -26,6 +28,7 @@ export interface PackageRuntimeServiceInstallResult {
26
28
  servicePath?: string;
27
29
  pid?: number;
28
30
  warnings: string[];
31
+ connector?: PackageConnectorServiceResult;
29
32
  }
30
33
 
31
34
  export interface PackageRuntimeServiceOptions {
@@ -147,26 +150,36 @@ export async function installPackageRuntimeService(options: PackageRuntimeServic
147
150
  repositoryRoot: release.packageRoot,
148
151
  host: options.host ?? '127.0.0.1',
149
152
  port: options.port ?? 8765,
150
- authTokenFile: resolve(options.authTokenFile),
153
+ authTokenFile: options.authTokenFile,
151
154
  ...(options.exclusiveWorkId?.trim() ? { exclusiveWorkId: options.exclusiveWorkId.trim() } : {}),
152
155
  };
153
156
  writeForgeRuntimeServiceConfig(config);
154
157
  syncForgeRuntimeActiveEntrypoint(controllerHome);
155
158
  const platform = options.platform ?? process.platform;
156
159
  const env = options.env ?? process.env;
160
+
161
+ let base: PackageRuntimeServiceInstallResult;
157
162
  if (!options.forcePortable && platform === 'darwin') {
158
163
  const paths = await installForgeRuntimeService({ config, runnerPath: join(release.packageRoot, 'bin', 'forge-runtime-service.mjs'), nodeExecutable: process.execPath });
159
- return { status: 'installed', mode: 'launchd', persistent: true, controllerHome, release, servicePath: paths.installedPlistPath, warnings: [] };
160
- }
161
- if (!options.forcePortable && platform === 'linux' && systemdUserAvailable(env)) {
164
+ base = { status: 'installed', mode: 'launchd', persistent: true, controllerHome, release, servicePath: paths.installedPlistPath, warnings: [] };
165
+ } else if (!options.forcePortable && platform === 'linux' && systemdUserAvailable(env)) {
162
166
  const unitPath = installSystemdUserService(controllerHome, env);
163
- return { status: 'installed', mode: 'systemd-user', persistent: true, controllerHome, release, servicePath: unitPath, warnings: [] };
167
+ base = { status: 'installed', mode: 'systemd-user', persistent: true, controllerHome, release, servicePath: unitPath, warnings: [] };
168
+ } else {
169
+ const pid = startPortableRuntime(controllerHome, env);
170
+ base = {
171
+ status: 'installed', mode: 'portable', persistent: false, controllerHome, release, pid,
172
+ warnings: [platform === 'win32'
173
+ ? 'Native Windows is preview: Forge started detached Runtime and OAuth Gateway processes for this session; automatic login/reboot persistence is not yet claimed.'
174
+ : 'systemd --user is unavailable: Forge started detached Runtime and OAuth Gateway processes; enable a user service manager for reboot persistence.'],
175
+ };
164
176
  }
165
- const pid = startPortableRuntime(controllerHome, env);
166
- return {
167
- status: 'installed', mode: 'portable', persistent: false, controllerHome, release, pid,
168
- warnings: [platform === 'win32'
169
- ? 'Native Windows is preview: Forge started a detached user process for this session; automatic login/reboot persistence is not yet claimed.'
170
- : 'systemd --user is unavailable: Forge started a detached user process; enable a user service manager for reboot persistence.'],
171
- };
177
+
178
+ const localConfig = loadMcpServiceLocalConfig(controllerHome);
179
+ const connectorEndpoint = localConfig?.chatgpt?.localEndpoint;
180
+ if (!connectorEndpoint) return base;
181
+ const connector = await installPackageConnectorService({
182
+ release, controllerHome, endpoint: connectorEndpoint, platform, env, forcePortable: options.forcePortable === true,
183
+ });
184
+ return { ...base, connector };
172
185
  }
@@ -14,6 +14,15 @@ import { CONTROL_PLANE_SCHEMA_VERSION } from '../control-plane/persistence/sqlit
14
14
  * `forge runtime service install` operation; staging alone never starts or
15
15
  * publishes anything.
16
16
  */
17
+ export const FORGE_MACOS_RUNTIME_SIGNING_IDENTIFIER = 'com.moretea.forge.runtime';
18
+
19
+ export interface MacOSRuntimeCodeSigning {
20
+ identifier: string;
21
+ teamIdentifier: string;
22
+ designatedRequirement: string;
23
+ authority: string;
24
+ }
25
+
17
26
  export interface StagedRuntimeRelease {
18
27
  releasePath: string;
19
28
  manifestPath: string;
@@ -22,19 +31,22 @@ export interface StagedRuntimeRelease {
22
31
  diagnosticArtifactIdentity?: string;
23
32
  browserNodeBridgeArtifactIdentity?: string;
24
33
  browserHandoffArtifactIdentity?: string;
25
- desktopHelperArtifactIdentity?: string;
26
34
  processRunnerArtifactIdentity?: string;
27
35
  checkRunnerArtifactIdentity?: string;
28
36
  externalPluginProbeArtifactIdentity?: string;
29
37
  codeGraphNodeArtifactIdentity?: string;
30
38
  codeGraphSidecarArtifactIdentity?: string;
31
39
  codeGraphLibraryArtifactIdentity?: string;
40
+ controllerUiArtifactIdentity?: string;
41
+ macosCodeSigning?: MacOSRuntimeCodeSigning;
32
42
  manifestSha256: string;
33
43
  sourceCommit: string;
34
44
  }
35
45
 
36
46
  export interface RuntimeReleaseMaterializerDependencies {
37
47
  now?: () => number;
48
+ platform?: NodeJS.Platform;
49
+ signMacOSRuntime?: (input: { executable: string; controllerHome: string }) => MacOSRuntimeCodeSigning;
38
50
  uuid?: () => string;
39
51
  compileBinary?: (input: { sourceRoot: string; outputPath: string; entryPath?: string }) => { ok: boolean; stderr?: string; stdout?: string; error?: string };
40
52
  bundleNodeHost?: (input: { sourceRoot: string; outputPath: string; entryPath: string }) => { ok: boolean; stderr?: string; stdout?: string; error?: string };
@@ -58,6 +70,8 @@ export interface CandidateRuntimeStageReceiptV1 {
58
70
  }
59
71
 
60
72
  export interface CandidateRuntimeReleaseStagerDependencies {
73
+ platform?: NodeJS.Platform;
74
+ inspectMacOSRuntime?: (executable: string) => MacOSRuntimeCodeSigning;
61
75
  runCandidateStager?: (input: {
62
76
  bunExecutable: string;
63
77
  scriptPath: string;
@@ -77,6 +91,95 @@ function sha256(path: string): string {
77
91
  return createHash('sha256').update(readFileSync(path)).digest('hex');
78
92
  }
79
93
 
94
+ function parseMacOSRuntimeCodeSigning(value: unknown, context: string): MacOSRuntimeCodeSigning | undefined {
95
+ if (value === undefined) return undefined;
96
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${context}: macosCodeSigning must be an object`);
97
+ const record = value as Record<string, unknown>;
98
+ const field = (name: keyof MacOSRuntimeCodeSigning): string => {
99
+ const entry = record[name];
100
+ if (typeof entry !== 'string' || !entry.trim()) throw new Error(`${context}: macosCodeSigning.${name} is required`);
101
+ return entry.trim();
102
+ };
103
+ const signing = {
104
+ identifier: field('identifier'),
105
+ teamIdentifier: field('teamIdentifier'),
106
+ designatedRequirement: field('designatedRequirement'),
107
+ authority: field('authority'),
108
+ };
109
+ if (signing.identifier !== FORGE_MACOS_RUNTIME_SIGNING_IDENTIFIER) {
110
+ throw new Error(`${context}: macosCodeSigning.identifier must be ${FORGE_MACOS_RUNTIME_SIGNING_IDENTIFIER}`);
111
+ }
112
+ if (!/^[A-Z0-9]{10}$/.test(signing.teamIdentifier)) throw new Error(`${context}: macosCodeSigning.teamIdentifier is invalid`);
113
+ if (!signing.authority.startsWith('Developer ID Application: ')) throw new Error(`${context}: macosCodeSigning.authority must be Developer ID Application`);
114
+ if (!signing.designatedRequirement.includes(`identifier \"${FORGE_MACOS_RUNTIME_SIGNING_IDENTIFIER}\"`)
115
+ || !signing.designatedRequirement.includes('anchor apple generic')
116
+ || !signing.designatedRequirement.includes(`certificate leaf[subject.OU] = ${signing.teamIdentifier}`)) {
117
+ throw new Error(`${context}: macosCodeSigning.designatedRequirement is not the stable Developer ID contract`);
118
+ }
119
+ return signing;
120
+ }
121
+
122
+ function readActiveMacOSRuntimeTeam(controllerHome: string): string | undefined {
123
+ const authorityPath = join(resolve(controllerHome), 'runtime', 'releases', 'authority.json');
124
+ if (!existsSync(authorityPath)) return undefined;
125
+ try {
126
+ const authority = JSON.parse(readFileSync(authorityPath, 'utf8')) as { active?: { manifestPath?: string } };
127
+ if (!authority.active?.manifestPath || !existsSync(authority.active.manifestPath)) return undefined;
128
+ const manifest = JSON.parse(readFileSync(authority.active.manifestPath, 'utf8')) as Record<string, unknown>;
129
+ return parseMacOSRuntimeCodeSigning(manifest.macosCodeSigning, 'RUNTIME_RELEASE_ACTIVE_SIGNING_INVALID')?.teamIdentifier;
130
+ } catch (error) {
131
+ if (error instanceof Error && error.message.startsWith('RUNTIME_RELEASE_ACTIVE_SIGNING_INVALID')) throw error;
132
+ return undefined;
133
+ }
134
+ }
135
+
136
+ function inspectMacOSRuntimeCodeSigning(executable: string): MacOSRuntimeCodeSigning {
137
+ const verify = runProcess('codesign', ['--verify', '--strict', executable], { timeoutMs: 30_000, maxOutputBytes: 64 * 1024 });
138
+ if (!verify.ok) throw new Error(`RUNTIME_RELEASE_MACOS_SIGNATURE_INVALID: ${verify.stderr || verify.stdout || verify.error}`.slice(0, 2_000));
139
+ const detail = runProcess('codesign', ['-dv', '--verbose=4', executable], { timeoutMs: 30_000, maxOutputBytes: 64 * 1024 });
140
+ if (!detail.ok) throw new Error(`RUNTIME_RELEASE_MACOS_SIGNATURE_INSPECTION_FAILED: ${detail.stderr || detail.stdout || detail.error}`.slice(0, 2_000));
141
+ const detailText = `${detail.stdout}\n${detail.stderr}`;
142
+ const identifier = detailText.match(/^Identifier=(.+)$/m)?.[1]?.trim() ?? '';
143
+ const teamIdentifier = detailText.match(/^TeamIdentifier=(.+)$/m)?.[1]?.trim() ?? '';
144
+ const authority = detailText.match(/^Authority=(Developer ID Application:.+)$/m)?.[1]?.trim() ?? '';
145
+ const requirementResult = runProcess('codesign', ['-d', '-r-', executable], { timeoutMs: 30_000, maxOutputBytes: 64 * 1024 });
146
+ if (!requirementResult.ok) throw new Error(`RUNTIME_RELEASE_MACOS_REQUIREMENT_INSPECTION_FAILED: ${requirementResult.stderr || requirementResult.stdout || requirementResult.error}`.slice(0, 2_000));
147
+ const requirementText = `${requirementResult.stdout}\n${requirementResult.stderr}`;
148
+ const designatedRequirement = requirementText.match(/^designated => (.+)$/m)?.[1]?.trim() ?? '';
149
+ return parseMacOSRuntimeCodeSigning({ identifier, teamIdentifier, authority, designatedRequirement }, 'RUNTIME_RELEASE_MACOS_SIGNATURE_INVALID')!;
150
+ }
151
+
152
+ function defaultSignMacOSRuntime(input: { executable: string; controllerHome: string }): MacOSRuntimeCodeSigning {
153
+ const identities = runProcess('security', ['find-identity', '-v', '-p', 'codesigning'], { timeoutMs: 30_000, maxOutputBytes: 128 * 1024 });
154
+ if (!identities.ok) throw new Error(`RUNTIME_RELEASE_MACOS_SIGNING_IDENTITY_LOOKUP_FAILED: ${identities.stderr || identities.stdout || identities.error}`.slice(0, 2_000));
155
+ const candidates = identities.stdout.split(/\r?\n/).flatMap((line) => {
156
+ const match = line.match(/^\s*\d+\)\s+([A-Fa-f0-9]{40})\s+\"(Developer ID Application: .+ \(([A-Z0-9]{10})\))\"/);
157
+ return match ? [{ hash: match[1]!, label: match[2]!, teamIdentifier: match[3]! }] : [];
158
+ });
159
+ const configured = process.env.FORGE_MACOS_RUNTIME_SIGNING_IDENTITY?.trim();
160
+ const expectedTeam = readActiveMacOSRuntimeTeam(input.controllerHome);
161
+ let selected = configured
162
+ ? candidates.find((candidate) => candidate.hash.toLowerCase() === configured.toLowerCase() || candidate.label === configured)
163
+ : undefined;
164
+ if (configured && !selected) throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_IDENTITY_NOT_FOUND');
165
+ if (!selected) {
166
+ const eligible = expectedTeam ? candidates.filter((candidate) => candidate.teamIdentifier === expectedTeam) : candidates;
167
+ const teams = Array.from(new Set(eligible.map((candidate) => candidate.teamIdentifier)));
168
+ if (teams.length !== 1 || eligible.length === 0) {
169
+ throw new Error(expectedTeam ? 'RUNTIME_RELEASE_MACOS_SIGNING_IDENTITY_FOR_TEAM_REQUIRED' : 'RUNTIME_RELEASE_MACOS_SIGNING_IDENTITY_REQUIRED');
170
+ }
171
+ selected = eligible[0];
172
+ }
173
+ if (expectedTeam && selected.teamIdentifier !== expectedTeam) throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_TEAM_CHANGED');
174
+ const signed = runProcess('codesign', [
175
+ '--force', '--sign', selected.hash, '--identifier', FORGE_MACOS_RUNTIME_SIGNING_IDENTIFIER, '--options', 'runtime', input.executable,
176
+ ], { timeoutMs: 120_000, maxOutputBytes: 128 * 1024 });
177
+ if (!signed.ok) throw new Error(`RUNTIME_RELEASE_MACOS_SIGNING_FAILED: ${signed.stderr || signed.stdout || signed.error}`.slice(0, 2_000));
178
+ const inspected = inspectMacOSRuntimeCodeSigning(input.executable);
179
+ if (inspected.teamIdentifier !== selected.teamIdentifier) throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_TEAM_MISMATCH');
180
+ return inspected;
181
+ }
182
+
80
183
  function sha256Directory(root: string): string {
81
184
  const hash = createHash('sha256');
82
185
  const visit = (directory: string): void => {
@@ -202,12 +305,28 @@ export function stageRuntimeReleaseFromCandidateSource(input: {
202
305
  if (sha256(manifestPath) !== receipt.manifestSha256) {
203
306
  throw new Error('RUNTIME_RELEASE_CANDIDATE_MANIFEST_IDENTITY_MISMATCH');
204
307
  }
308
+ let rawManifest: Record<string, unknown>;
309
+ try { rawManifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Record<string, unknown>; }
310
+ catch (error) { throw new Error(`RUNTIME_RELEASE_CANDIDATE_MANIFEST_INVALID: ${error instanceof Error ? error.message : String(error)}`); }
311
+ const platform = dependencies.platform ?? process.platform;
312
+ const macosCodeSigning = parseMacOSRuntimeCodeSigning(rawManifest.macosCodeSigning, 'RUNTIME_RELEASE_CANDIDATE_SIGNING_INVALID');
313
+ if (platform === 'darwin') {
314
+ if (!macosCodeSigning) throw new Error('RUNTIME_RELEASE_CANDIDATE_MACOS_SIGNING_REQUIRED');
315
+ const actual = (dependencies.inspectMacOSRuntime ?? inspectMacOSRuntimeCodeSigning)(runtimePath);
316
+ if (actual.identifier !== macosCodeSigning.identifier
317
+ || actual.teamIdentifier !== macosCodeSigning.teamIdentifier
318
+ || actual.authority !== macosCodeSigning.authority
319
+ || actual.designatedRequirement !== macosCodeSigning.designatedRequirement) {
320
+ throw new Error('RUNTIME_RELEASE_CANDIDATE_MACOS_SIGNING_MISMATCH');
321
+ }
322
+ }
205
323
 
206
324
  return {
207
325
  releasePath,
208
326
  manifestPath,
209
327
  releaseId: receipt.releaseId,
210
328
  artifactIdentity: receipt.artifactIdentity,
329
+ ...(macosCodeSigning ? { macosCodeSigning } : {}),
211
330
  manifestSha256: receipt.manifestSha256,
212
331
  sourceCommit: receipt.sourceCommit,
213
332
  };
@@ -296,6 +415,14 @@ export function stageRuntimeRelease(input: {
296
415
  throw new Error(`RUNTIME_RELEASE_BUILD_FAILED: ${compile.stderr || compile.stdout || compile.error}`.slice(0, 2_000));
297
416
  }
298
417
  chmodSync(executable, 0o700);
418
+ const platform = dependencies.platform ?? process.platform;
419
+ const macosCodeSigning = platform === 'darwin'
420
+ ? (dependencies.signMacOSRuntime ?? defaultSignMacOSRuntime)({ executable, controllerHome: resolve(input.controllerHome) })
421
+ : undefined;
422
+ if (platform === 'darwin' && !macosCodeSigning) throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_REQUIRED');
423
+ const normalizedMacOSCodeSigning = macosCodeSigning
424
+ ? parseMacOSRuntimeCodeSigning(macosCodeSigning, 'RUNTIME_RELEASE_MACOS_SIGNING_INVALID')
425
+ : undefined;
299
426
  const artifactIdentity = `sha256:${sha256(executable)}`;
300
427
 
301
428
  const diagnosticExecutable = join(staging, 'forge-cli');
@@ -395,28 +522,29 @@ export function stageRuntimeRelease(input: {
395
522
  const codeGraphSidecarArtifactIdentity = `sha256:${sha256(codeGraphSidecarPath)}`;
396
523
  const codeGraphLibraryArtifactIdentity = `sha256:${sha256Directory(codeGraphLibraryPath)}`;
397
524
 
398
- const desktopHelperEntrypoint = 'forge-desktop-helper.mjs' as const;
399
- const sourceDesktopHelperPath = join(sourceRoot, 'bin', desktopHelperEntrypoint);
400
- if (!existsSync(sourceDesktopHelperPath)) {
401
- throw new Error(`RUNTIME_RELEASE_DESKTOP_HELPER_SOURCE_MISSING: ${sourceDesktopHelperPath}`);
525
+ const controllerUiRoot = 'ui-dist' as const;
526
+ const sourceControllerUiPath = join(sourceRoot, 'src', 'cli', 'local-bridge', controllerUiRoot);
527
+ const sourceControllerUiJs = join(sourceControllerUiPath, 'app.js');
528
+ const sourceControllerUiCss = join(sourceControllerUiPath, 'app.css');
529
+ if (!existsSync(sourceControllerUiJs) || !existsSync(sourceControllerUiCss)) {
530
+ throw new Error(`RUNTIME_RELEASE_CONTROLLER_UI_SOURCE_MISSING: ${sourceControllerUiPath}`);
402
531
  }
403
- const desktopHelperPath = join(staging, desktopHelperEntrypoint);
404
- copyFileSync(sourceDesktopHelperPath, desktopHelperPath);
405
- chmodSync(desktopHelperPath, 0o700);
406
- const desktopHelperArtifactIdentity = `sha256:${sha256(desktopHelperPath)}`;
532
+ const controllerUiPath = join(staging, controllerUiRoot);
533
+ cpSync(sourceControllerUiPath, controllerUiPath, { recursive: true, force: false });
534
+ const controllerUiArtifactIdentity = `sha256:${sha256Directory(controllerUiPath)}`;
535
+
407
536
  const manifest = {
408
537
  schemaVersion: 1,
409
538
  releaseId,
410
539
  artifactIdentity,
411
540
  entrypoint: 'forge-runtime',
541
+ ...(normalizedMacOSCodeSigning ? { macosCodeSigning: normalizedMacOSCodeSigning } : {}),
412
542
  diagnosticEntrypoint: 'forge-cli',
413
543
  diagnosticArtifactIdentity,
414
544
  browserNodeBridgeEntrypoint,
415
545
  browserNodeBridgeArtifactIdentity,
416
546
  browserHandoffEntrypoint,
417
547
  browserHandoffArtifactIdentity,
418
- desktopHelperEntrypoint,
419
- desktopHelperArtifactIdentity,
420
548
  processRunnerEntrypoint,
421
549
  processRunnerArtifactIdentity,
422
550
  checkRunnerEntrypoint,
@@ -429,6 +557,8 @@ export function stageRuntimeRelease(input: {
429
557
  codeGraphSidecarArtifactIdentity,
430
558
  codeGraphLibraryRoot,
431
559
  codeGraphLibraryArtifactIdentity,
560
+ controllerUiRoot,
561
+ controllerUiArtifactIdentity,
432
562
  arguments: [],
433
563
  configurationSchemaVersion: 1,
434
564
  controllerHome: resolve(input.controllerHome),
@@ -450,16 +580,17 @@ export function stageRuntimeRelease(input: {
450
580
  manifestPath: join(releasePath, 'manifest.json'),
451
581
  releaseId,
452
582
  artifactIdentity,
583
+ ...(normalizedMacOSCodeSigning ? { macosCodeSigning: normalizedMacOSCodeSigning } : {}),
453
584
  diagnosticArtifactIdentity,
454
585
  browserNodeBridgeArtifactIdentity,
455
586
  browserHandoffArtifactIdentity,
456
- desktopHelperArtifactIdentity,
457
587
  processRunnerArtifactIdentity,
458
588
  checkRunnerArtifactIdentity,
459
589
  externalPluginProbeArtifactIdentity,
460
590
  codeGraphNodeArtifactIdentity,
461
591
  codeGraphSidecarArtifactIdentity,
462
592
  codeGraphLibraryArtifactIdentity,
593
+ controllerUiArtifactIdentity,
463
594
  manifestSha256: createHash('sha256').update(`${JSON.stringify(manifest, null, 2)}\n`).digest('hex'),
464
595
  sourceCommit,
465
596
  };
@@ -469,12 +600,23 @@ export function stageRuntimeRelease(input: {
469
600
  }
470
601
  }
471
602
 
472
- export function assertRuntimeReleaseFiles(release: StagedRuntimeRelease): void {
603
+ export function assertRuntimeReleaseFiles(release: StagedRuntimeRelease, dependencies: { platform?: NodeJS.Platform; inspectMacOSRuntime?: (executable: string) => MacOSRuntimeCodeSigning } = {}): void {
473
604
  if (!existsSync(release.releasePath) || !existsSync(release.manifestPath)) {
474
605
  throw new Error(`RUNTIME_RELEASE_FILES_MISSING: ${release.releasePath}`);
475
606
  }
476
- if (!existsSync(join(release.releasePath, 'forge-runtime'))) {
477
- throw new Error(`RUNTIME_RELEASE_ENTRYPOINT_MISSING: ${join(release.releasePath, 'forge-runtime')}`);
607
+ const runtimePath = join(release.releasePath, 'forge-runtime');
608
+ if (!existsSync(runtimePath)) {
609
+ throw new Error(`RUNTIME_RELEASE_ENTRYPOINT_MISSING: ${runtimePath}`);
610
+ }
611
+ const platform = dependencies.platform ?? process.platform;
612
+ if (platform === 'darwin' && release.macosCodeSigning) {
613
+ const actual = (dependencies.inspectMacOSRuntime ?? inspectMacOSRuntimeCodeSigning)(runtimePath);
614
+ if (actual.identifier !== release.macosCodeSigning.identifier
615
+ || actual.teamIdentifier !== release.macosCodeSigning.teamIdentifier
616
+ || actual.authority !== release.macosCodeSigning.authority
617
+ || actual.designatedRequirement !== release.macosCodeSigning.designatedRequirement) {
618
+ throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_MISMATCH');
619
+ }
478
620
  }
479
621
  if (release.diagnosticArtifactIdentity && !existsSync(join(release.releasePath, 'forge-cli'))) {
480
622
  throw new Error(`RUNTIME_RELEASE_DIAGNOSTIC_ENTRYPOINT_MISSING: ${join(release.releasePath, 'forge-cli')}`);
@@ -485,9 +627,6 @@ export function assertRuntimeReleaseFiles(release: StagedRuntimeRelease): void {
485
627
  if (release.browserHandoffArtifactIdentity && !existsSync(join(release.releasePath, 'browser-handoff-host.js'))) {
486
628
  throw new Error(`RUNTIME_RELEASE_BROWSER_HANDOFF_HOST_MISSING: ${join(release.releasePath, 'browser-handoff-host.js')}`);
487
629
  }
488
- if (release.desktopHelperArtifactIdentity && !existsSync(join(release.releasePath, 'forge-desktop-helper.mjs'))) {
489
- throw new Error(`RUNTIME_RELEASE_DESKTOP_HELPER_MISSING: ${join(release.releasePath, 'forge-desktop-helper.mjs')}`);
490
- }
491
630
  if (release.processRunnerArtifactIdentity && !existsSync(join(release.releasePath, 'process-runner.js'))) {
492
631
  throw new Error(`RUNTIME_RELEASE_PROCESS_RUNNER_MISSING: ${join(release.releasePath, 'process-runner.js')}`);
493
632
  }
@@ -506,4 +645,8 @@ export function assertRuntimeReleaseFiles(release: StagedRuntimeRelease): void {
506
645
  if (release.codeGraphLibraryArtifactIdentity && !existsSync(join(release.releasePath, 'codegraph-lib', 'dist', 'index.js'))) {
507
646
  throw new Error(`RUNTIME_RELEASE_CODEGRAPH_LIBRARY_MISSING: ${join(release.releasePath, 'codegraph-lib', 'dist', 'index.js')}`);
508
647
  }
648
+ if (release.controllerUiArtifactIdentity
649
+ && (!existsSync(join(release.releasePath, 'ui-dist', 'app.js')) || !existsSync(join(release.releasePath, 'ui-dist', 'app.css')))) {
650
+ throw new Error(`RUNTIME_RELEASE_CONTROLLER_UI_MISSING: ${join(release.releasePath, 'ui-dist')}`);
651
+ }
509
652
  }
@@ -232,7 +232,7 @@ export function syncForgeRuntimeActiveEntrypoint(controllerHome: string): { path
232
232
  }
233
233
 
234
234
  export function ensureForgeRuntimeLaunchAgentContract(
235
- input: { controllerHome: string; bootstrapNodeExecutable?: string; bootstrapRunnerPath?: string },
235
+ input: { controllerHome: string; bootstrapNodeExecutable?: string; bootstrapRunnerPath?: string; installUserLaunchAgent?: boolean },
236
236
  ): { paths: ForgeRuntimeServicePaths; mode: 'release' | 'bootstrap'; changed: boolean } {
237
237
  const paths = forgeRuntimeServicePaths(input.controllerHome);
238
238
  const active = activeRuntimeEntrypoint(input.controllerHome);
@@ -250,10 +250,12 @@ export function ensureForgeRuntimeLaunchAgentContract(
250
250
  : { nodeExecutable: input.bootstrapNodeExecutable!, runnerPath: input.bootstrapRunnerPath! }),
251
251
  });
252
252
  const existingSource = existsSync(paths.sourcePlistPath) ? readFileSync(paths.sourcePlistPath, 'utf8') : undefined;
253
- const existingInstalled = existsSync(paths.installedPlistPath) ? readFileSync(paths.installedPlistPath, 'utf8') : undefined;
254
- const changed = existingSource !== plist || existingInstalled !== plist;
253
+ const existingInstalled = input.installUserLaunchAgent && existsSync(paths.installedPlistPath)
254
+ ? readFileSync(paths.installedPlistPath, 'utf8')
255
+ : undefined;
256
+ const changed = existingSource !== plist || (input.installUserLaunchAgent === true && existingInstalled !== plist);
255
257
  if (existingSource !== plist) atomicWrite(paths.sourcePlistPath, plist);
256
- if (existingInstalled !== plist) atomicWrite(paths.installedPlistPath, plist, 0o644);
258
+ if (input.installUserLaunchAgent === true && existingInstalled !== plist) atomicWrite(paths.installedPlistPath, plist, 0o644);
257
259
  return { paths, mode: useRelease ? 'release' : 'bootstrap', changed };
258
260
  }
259
261
 
@@ -1,6 +1,5 @@
1
1
  export * from './types';
2
2
  export * from './plugin-summary';
3
- export * from './browser-safe';
4
3
  export * from './job-summary';
5
4
  export * from './external-filesystem';
6
5
  export * from './workspace-auth';
@@ -44,36 +44,6 @@ export interface SafePluginSummary {
44
44
  };
45
45
  }
46
46
 
47
- export interface WebTarget {
48
- targetKey: string;
49
- domain: string;
50
- origin: string;
51
- allowed: true;
52
- defaultPath: '/';
53
- }
54
-
55
- export interface WebDomainAccessPreview {
56
- ticketId: string;
57
- normalizedDomain: string;
58
- domainKey: string;
59
- reason: string;
60
- expiresAt: string;
61
- alreadyAllowed: boolean;
62
- currentAllowedDomainCount: number;
63
- risk: 'workspace_write';
64
- confirmation: 'authorization';
65
- localOnly: true;
66
- willChange: {
67
- pluginId: 'browser';
68
- configField: 'allowedDomains';
69
- addDomain: string;
70
- };
71
- safety: {
72
- arbitraryUrlAccepted: false;
73
- domainOnly: true;
74
- sensitiveConfigReturned: false;
75
- };
76
- }
77
47
 
78
48
  export interface SafeJobResultSummary {
79
49
  jobId: string;
@@ -1740,7 +1740,7 @@ export async function activateRuntimeRelease(
1740
1740
  return { ok: false, attempted: true, detail, serviceTarget: service.target, verify: await verifyLocal(config) } satisfies RuntimeReleaseActivationResult;
1741
1741
  }
1742
1742
  try {
1743
- ensureForgeRuntimeLaunchAgentContract({ controllerHome: config.controllerHome });
1743
+ ensureForgeRuntimeLaunchAgentContract({ controllerHome: config.controllerHome, installUserLaunchAgent: true });
1744
1744
  } catch (error) {
1745
1745
  const detail = `runtime launchd stable-release contract update failed: ${error instanceof Error ? error.message : String(error)}`;
1746
1746
  audit(config, 'runtime_release_activation_launchd_contract_failed', { serviceTarget: service.target, operationId, detail });
@@ -1821,7 +1821,7 @@ export async function activateRuntimeRelease(
1821
1821
  physicalHome: storageMigration.physicalHome,
1822
1822
  });
1823
1823
  }
1824
- ensureForgeRuntimeLaunchAgentContract({ controllerHome: config.controllerHome });
1824
+ ensureForgeRuntimeLaunchAgentContract({ controllerHome: config.controllerHome, installUserLaunchAgent: true });
1825
1825
  const previousRevision = previousActive?.releaseId ?? '';
1826
1826
  const previousIdentity = previousActive?.artifactIdentity;
1827
1827
  if (