@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.
- package/CHANGELOG.md +28 -1
- package/DESIGN.md +7 -2
- package/README.md +7 -7
- package/README.zh-CN.md +5 -3
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/assets/hooks/pre-edit-guard.sh +8 -39
- package/assets/hooks/prompt-guard.sh +2 -2
- package/assets/plugin-registry.v1.json +54 -3
- package/assets/reference-configs/hook-operations.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +2 -2
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/ensure-task-workflow.sh +2 -2
- package/assets/templates/helpers/sprint-backlog.sh +6 -6
- package/docs/architecture/current/automation-and-schedule-engine.md +9 -3
- package/docs/architecture/current/codegraph-context-provider.md +15 -3
- package/docs/architecture/current/dispatch-and-agent-strategy.md +32 -13
- package/docs/architecture/current/entity-model.md +20 -0
- package/docs/architecture/current/external-plugin-governance.md +3 -3
- package/docs/architecture/current/system-overview.md +26 -1
- package/docs/architecture/current/thin-harness-v1.md +10 -0
- package/docs/forge-chatgpt-mcp-setup.md +27 -3
- package/docs/operations/chatgpt-personal-assistant-runtime.md +12 -2
- package/docs/operations/controller-browser-plugin.md +12 -16
- package/docs/operations/features.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/public-usage-guide.md +1 -1
- package/docs/public-usage-guide.zh-CN.md +1 -1
- package/docs/tutorials/01-install-and-start.md +6 -4
- package/docs/tutorials/01-install-and-start.zh-CN.md +3 -1
- package/docs/tutorials/02-connect-chatgpt.md +8 -2
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +8 -2
- package/docs/wiki/Installation.md +1 -1
- package/docs/wiki/Quick-Start.md +1 -1
- package/package.json +1 -1
- package/scripts/check-public-docs.sh +14 -1
- package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
- package/scripts/sprint-backlog.sh +1 -1
- package/src/cli/commands/chatgpt.ts +31 -2
- package/src/cli/commands/init-hook.ts +3 -1
- package/src/cli/commands/mcp.ts +2 -0
- package/src/cli/commands/plugin.ts +36 -9
- package/src/cli/commands/runtime.ts +1 -0
- package/src/cli/commands/setup-profile.ts +49 -18
- package/src/cli/controller/context-pack.ts +158 -6
- package/src/cli/hook/prompt-guard-decision.ts +17 -18
- package/src/cli/hook/prompt-intents.ts +1 -1
- package/src/cli/local-bridge/console-assets.ts +17 -0
- package/src/cli/local-bridge/console-automations.ts +205 -23
- package/src/cli/local-bridge/console-view-models.ts +2 -0
- package/src/cli/local-bridge/facade-api.ts +2 -0
- package/src/cli/local-bridge/frontend/src/App.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +143 -3
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +181 -3
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
- package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -6
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -1
- package/src/cli/local-bridge/frontend/src/styles.css +27 -11
- package/src/cli/local-bridge/frontend/src/types.ts +6 -2
- package/src/cli/local-bridge/server.ts +31 -20
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +5003 -4815
- package/src/cli/mcp/auth.ts +2 -0
- package/src/cli/mcp/paths.ts +8 -2
- package/src/cli/mcp/repository-tools.ts +124 -9
- package/src/cli/mcp/server.ts +164 -19
- package/src/cli/mcp/setup.ts +9 -1
- package/src/cli/mcp/transports/http.ts +52 -2
- package/src/cli/mcp/transports/session-registry.ts +24 -0
- package/src/cli/repositories/command-classifier.ts +113 -0
- package/src/cli/repositories/safe-patch.ts +6 -4
- package/src/cli/repositories/structured-git.ts +49 -7
- package/src/cli/repository/inspector.ts +16 -3
- package/src/runtime/assistant/intent.ts +3 -3
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +157 -2
- package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
- package/src/runtime/control-plane/execution/validation.ts +15 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +32 -0
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +6 -7
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +2 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +1 -1
- package/src/runtime/control-plane/facade/controller-session-store.ts +12 -13
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +7 -2
- package/src/runtime/control-plane/facade/goal-workloop.ts +231 -8
- package/src/runtime/control-plane/facade/plan-contract-store.ts +32 -12
- package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
- package/src/runtime/control-plane/facade/types.ts +33 -6
- package/src/runtime/control-plane/facade/work-contract-store.ts +31 -2
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +375 -23
- package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
- package/src/runtime/diagnostics/mcp-timing.ts +6 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +1 -10
- package/src/runtime/execution/process-runtime/check-result.ts +10 -2
- package/src/runtime/execution/process-runtime/command-facade.ts +1 -0
- package/src/runtime/execution/process-runtime/gc.ts +64 -10
- package/src/runtime/execution/process-runtime/persisted-check.ts +10 -2
- package/src/runtime/execution/process-runtime/resource-claims.ts +87 -2
- package/src/runtime/execution/process-runtime/runtime.ts +16 -1
- package/src/runtime/execution/process-runtime/store.ts +6 -0
- package/src/runtime/gateway/mcp/execution-tools.ts +27 -12
- package/src/runtime/gateway/mcp/router.ts +1 -1
- package/src/runtime/gateway/mcp/runtime-tools.ts +482 -178
- package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
- package/src/runtime/plugins/browser-adapter.ts +19 -72
- package/src/runtime/plugins/browser-handoff-host.ts +6 -31
- package/src/runtime/plugins/browser-handoff.ts +0 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +8 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +8 -11
- package/src/runtime/plugins/external-registration.ts +84 -5
- package/src/runtime/plugins/first-party-registry.ts +2 -2
- package/src/runtime/plugins/gmail-adapter.ts +28 -2
- package/src/runtime/plugins/local-system-adapter.ts +2 -2
- package/src/runtime/plugins/plugin-management-adapter.ts +140 -0
- package/src/runtime/plugins/store.ts +107 -5
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/recovery/actions.ts +0 -10
- package/src/runtime/recovery/classifier.ts +0 -9
- package/src/runtime/recovery/maintenance-executor.ts +43 -11
- package/src/runtime/recovery/probe.ts +0 -1
- package/src/runtime/recovery/types.ts +0 -1
- package/src/runtime/root/package-connector-service.ts +136 -0
- package/src/runtime/root/package-runtime-service.ts +25 -12
- package/src/runtime/root/release-materialize.ts +161 -18
- package/src/runtime/root/service.ts +6 -4
- package/src/runtime/safe-tooling/index.ts +0 -1
- package/src/runtime/safe-tooling/types.ts +0 -30
- package/src/runtime/standalone-recovery/core.ts +2 -2
- package/src/runtime/workflow/schedules/browser-probe.ts +180 -0
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
- package/src/runtime/workflow/schedules/engine.ts +357 -64
- package/src/runtime/workflow/schedules/types.ts +5 -0
- package/src/runtime/workflow/schedules/work-continuation.ts +100 -27
- package/bin/forge-desktop-helper.mjs +0 -204
- package/docs/operations/controller-desktop-plugin.md +0 -112
- package/src/runtime/plugins/desktop-adapter.ts +0 -374
- 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('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
477
|
-
|
|
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 =
|
|
254
|
-
|
|
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
|
|
|
@@ -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 (
|