@moretea-labs/forge 1.5.0-rc.1 → 1.5.1
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/AGENTS.md +9 -11
- package/CHANGELOG.md +27 -1
- package/CLAUDE.md +8 -10
- package/DESIGN.md +278 -0
- package/README.md +70 -90
- package/README.zh-CN.md +52 -55
- package/SKILL.md +8 -11
- package/THIRD_PARTY_NOTICES.md +9 -3
- package/assets/branding/README.md +4 -2
- package/assets/hooks/lib/workflow-state.sh +19 -18
- package/assets/hooks/post-edit-guard.sh +6 -75
- package/assets/hooks/prompt-guard.sh +24 -12
- package/assets/hooks/session-start-context.sh +17 -17
- package/assets/hooks/stop-orchestrator.sh +2 -2
- package/assets/partials/04-project-structure.partial.md +6 -6
- package/assets/partials/05-workflow.partial.md +6 -6
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials/08-orchestration.partial.md +2 -2
- package/assets/partials-agents/02-operating-mode.partial.md +5 -5
- package/assets/partials-agents/03-orchestration.partial.md +3 -3
- package/assets/partials-agents/04-task-protocol.partial.md +3 -3
- package/assets/partials-agents/08-deep-docs.partial.md +1 -1
- package/assets/reference-configs/agentic-development-flow.md +49 -86
- package/assets/reference-configs/external-tooling.md +21 -61
- package/assets/reference-configs/handoff-protocol.md +25 -20
- package/assets/reference-configs/harness-overview.md +8 -8
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/archive-workflow.sh +0 -1
- package/assets/templates/helpers/capture-plan.sh +3 -3
- package/assets/templates/helpers/check-task-workflow.sh +2 -2
- package/assets/templates/helpers/codex-handoff-resume.sh +5 -5
- package/assets/templates/helpers/ensure-task-workflow.sh +37 -35
- package/assets/templates/helpers/new-plan.sh +1 -1
- package/assets/templates/helpers/plan-to-todo.sh +2 -2
- package/assets/templates/helpers/prepare-codex-handoff.sh +2 -2
- package/assets/templates/helpers/prepare-handoff.sh +9 -9
- package/assets/templates/helpers/refresh-current-status.sh +2 -2
- package/assets/templates/helpers/ship-worktrees.sh +1 -1
- package/assets/templates/helpers/sprint-backlog.sh +1 -1
- package/assets/templates/helpers/verify-sprint.sh +7 -7
- package/assets/templates/helpers/workstream-sync.sh +1 -1
- package/assets/templates/plan.template.md +1 -1
- package/assets/workflow-contract.v1.json +6 -3
- package/docs/architecture/current/README.md +10 -9
- package/docs/architecture/current/approved-target-architecture.zh-CN.md +2 -2
- package/docs/architecture/current/architecture-invariants.md +2 -2
- package/docs/architecture/current/automation-and-schedule-engine.md +15 -15
- package/docs/architecture/current/codegraph-context-provider.md +2 -2
- package/docs/architecture/current/control-plane-authority-inventory.md +12 -0
- package/docs/architecture/current/dispatch-and-agent-strategy.md +11 -11
- package/docs/architecture/current/entity-model.md +0 -27
- package/docs/architecture/current/failure-recovery.md +0 -24
- package/docs/architecture/current/governance.md +1 -1
- package/docs/architecture/current/implementation-status.md +1 -1
- package/docs/architecture/current/legacy-governance.md +58 -0
- package/docs/architecture/current/multi-repository-execution.md +1 -1
- package/docs/architecture/current/peer-supercontroller-runtime.md +1 -1
- package/docs/architecture/current/personal-assistant-plugin-baseline.md +4 -5
- package/docs/architecture/current/scheduler-and-resource-claims.md +0 -10
- package/docs/architecture/current/target-requirements-traceability.md +2 -2
- package/docs/architecture/current/thin-harness-governance-policy.md +1 -1
- package/docs/architecture/current/thin-harness-v1.md +9 -11
- package/docs/architecture/domains/runtime-harness.md +1 -1
- package/docs/forge-autonomous-goal-loop.md +3 -1
- package/docs/forge-chatgpt-mcp-setup.md +67 -176
- package/docs/images/forge-banner-cn.svg +26 -1
- package/docs/images/forge-banner.svg +26 -1
- package/docs/operations/features.md +1 -1
- package/docs/operations/homebrew.md +3 -3
- package/docs/operations/mcp-tool-exposure.md +5 -5
- package/docs/operations/platform-support.md +45 -37
- package/docs/operations/platform-support.zh-CN.md +40 -38
- package/docs/operations/releasing.md +3 -3
- package/docs/operations/releasing.zh-CN.md +3 -3
- package/docs/operations/troubleshooting.md +11 -6
- package/docs/operations/troubleshooting.zh-CN.md +11 -6
- package/docs/public-usage-guide.md +28 -21
- package/docs/public-usage-guide.zh-CN.md +28 -21
- package/docs/tutorials/01-install-and-start.md +58 -35
- package/docs/tutorials/01-install-and-start.zh-CN.md +58 -35
- package/docs/tutorials/02-connect-chatgpt.md +118 -46
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +119 -45
- package/docs/wiki/Architecture.md +1 -1
- package/docs/wiki/Installation.md +2 -2
- package/docs/wiki/Quick-Start.md +1 -1
- package/docs/wiki/Work-Lifecycle.md +1 -1
- package/install.ps1 +4 -5
- package/install.sh +4 -6
- package/package.json +15 -5
- package/scripts/archive-workflow.sh +0 -1
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +27 -14
- package/scripts/build-controller-ui.ts +28 -0
- package/scripts/capture-plan.sh +4 -4
- package/scripts/check-agent-tooling.sh +12 -1
- package/scripts/check-platform-support.mjs +2 -2
- package/scripts/check-public-docs.sh +16 -5
- package/scripts/check-release-readiness.sh +5 -11
- package/scripts/check-runtime-architecture.mjs +14 -22
- package/scripts/check-tarball-install-smoke.sh +3 -2
- package/scripts/check-task-workflow.sh +2 -2
- package/scripts/codex-handoff-resume.sh +5 -5
- package/scripts/create-project-dirs.sh +3 -3
- package/scripts/ensure-task-workflow.sh +45 -35
- package/scripts/init-project.sh +4 -4
- package/scripts/install-desktop-operator-registration.ts +2 -2
- package/scripts/lib/project-init-lib.sh +46 -44
- package/scripts/migrate-project-template.sh +7 -4
- package/scripts/new-plan.sh +1 -1
- package/scripts/plan-to-todo.sh +2 -2
- package/scripts/prepare-codex-handoff.sh +2 -2
- package/scripts/prepare-handoff.sh +9 -9
- package/scripts/refresh-current-status.sh +2 -2
- package/scripts/setup-plugins.sh +3 -3
- package/scripts/ship-worktrees.sh +1 -1
- package/scripts/sprint-backlog.sh +5 -5
- package/scripts/sync-codex-installed-copies.sh +2 -3
- package/scripts/verify-sprint.sh +7 -7
- package/scripts/workstream-sync.sh +1 -1
- package/src/cli/chatgpt-browser/bridge-extension.ts +1 -1
- package/src/cli/chatgpt-browser/bridge-provider.ts +29 -0
- package/src/cli/chatgpt-browser/file-policy.ts +3 -1
- package/src/cli/commands/chatgpt.ts +51 -0
- package/src/cli/commands/doctor.ts +1 -1
- package/src/cli/commands/init-hook.ts +213 -70
- package/src/cli/commands/init.ts +5 -6
- package/src/cli/commands/mcp.ts +31 -5
- package/src/cli/commands/runtime.ts +61 -8
- package/src/cli/commands/setup-profile.ts +472 -0
- package/src/cli/controller/check-runner.ts +20 -3
- package/src/cli/controller/codex-continuation.ts +4 -7
- package/src/cli/controller/context-pack.ts +118 -14
- package/src/cli/controller/operational-plan.ts +4 -4
- package/src/cli/controller/task-ledger.ts +10 -10
- package/src/cli/controller/work-mode.ts +6 -13
- package/src/cli/index.ts +14 -35
- package/src/cli/local-bridge/connector-freshness.ts +1 -1
- package/src/cli/local-bridge/console-assets.ts +17 -0
- package/src/cli/local-bridge/console-automations.ts +160 -0
- package/src/cli/local-bridge/dashboard.ts +8 -1303
- package/src/cli/local-bridge/facade-api.ts +16 -7
- package/src/cli/local-bridge/frontend/src/App.tsx +10 -0
- package/src/cli/local-bridge/frontend/src/components/Button.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/design/icons.tsx +11 -0
- package/src/cli/local-bridge/frontend/src/design/tokens.css +8 -0
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +4 -0
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +2 -0
- package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +2 -0
- package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +2 -0
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/lib/api.ts +23 -0
- package/src/cli/local-bridge/frontend/src/lib/format.ts +4 -0
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +7 -0
- package/src/cli/local-bridge/frontend/src/main.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +2 -0
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/styles.css +12 -0
- package/src/cli/local-bridge/frontend/src/types.ts +14 -0
- package/src/cli/local-bridge/frontend/src/vite-env.d.ts +1 -0
- package/src/cli/local-bridge/frontend/vite.config.ts +27 -0
- package/src/cli/local-bridge/server.ts +45 -0
- package/src/cli/local-bridge/ui-dist/app.css +1 -0
- package/src/cli/local-bridge/ui-dist/app.js +11094 -0
- package/src/cli/mcp/auth.ts +2 -0
- package/src/cli/mcp/instructions.ts +3 -3
- package/src/cli/mcp/legacy-tool-service.ts +27 -23
- package/src/cli/mcp/multi-repository.ts +2 -0
- package/src/cli/mcp/policy.ts +7 -5
- package/src/cli/mcp/repository-tools.ts +53 -14
- package/src/cli/mcp/server.ts +5 -10
- package/src/cli/mcp/setup.ts +220 -276
- package/src/cli/mcp/tool-capability-cache.ts +1 -1
- package/src/cli/mcp/toolset-names.ts +1 -1
- package/src/cli/mcp/toolset.ts +0 -1
- package/src/cli/mcp/transports/http.ts +51 -4
- package/src/cli/repositories/branch-name-policy.ts +0 -7
- package/src/cli/repositories/controller-home.ts +110 -3
- package/src/cli/repositories/safe-patch.ts +1 -1
- package/src/cli/repositories/selected-path-actions.ts +26 -24
- package/src/core/adoption/gitignore-plan.ts +2 -3
- package/src/core/adoption/plan.ts +4 -1
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +556 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +128 -1
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +277 -0
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +169 -0
- package/src/runtime/control-plane/facade/capability-registry.ts +0 -10
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +44 -25
- package/src/runtime/control-plane/facade/suggested-actions.ts +7 -1
- package/src/runtime/control-plane/facade/types.ts +0 -1
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +30 -8
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +114 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +175 -0
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +161 -0
- package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +64 -0
- package/src/runtime/control-plane/launcher/thin-launcher.ts +117 -24
- package/src/runtime/control-plane/routing/route-policy.ts +14 -14
- package/src/runtime/control-plane/runtime-cleanup.ts +0 -1
- package/src/runtime/evidence/event-ledger.ts +1 -1
- package/src/runtime/{workflow/campaigns/workspace.ts → execution/managed-workspace.ts} +41 -43
- package/src/runtime/execution/process-runtime/check-facade.ts +6 -0
- package/src/runtime/execution/process-runtime/check-result.ts +45 -0
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -16
- package/src/runtime/execution/process-runtime/command-facade.ts +4 -2
- package/src/runtime/execution/process-runtime/gc.ts +1 -1
- package/src/runtime/execution/process-runtime/index.ts +3 -0
- package/src/runtime/execution/process-runtime/interactive-admission.ts +14 -5
- package/src/runtime/execution/process-runtime/persisted-check.ts +285 -0
- package/src/runtime/execution/process-runtime/resource-claims.ts +30 -0
- package/src/runtime/execution/process-runtime/runtime.ts +18 -1
- package/src/runtime/execution/process-runtime/store.ts +1 -19
- package/src/runtime/execution/process-runtime/types.ts +4 -0
- package/src/runtime/execution/thin-harness/execution-router.ts +0 -1
- package/src/runtime/execution/thin-harness/fast-executor.ts +1 -1
- package/src/runtime/execution/thin-harness/lightweight-lanes.ts +1 -2
- package/src/runtime/execution/thin-harness/types.ts +0 -1
- package/src/runtime/execution/workers/executor.ts +1 -1
- package/src/runtime/gateway/mcp/execution-tools.ts +1 -1
- package/src/runtime/gateway/mcp/persisted-check-process.ts +3 -255
- package/src/runtime/gateway/mcp/process-tools.ts +16 -3
- package/src/runtime/gateway/mcp/resource-policy.ts +0 -9
- package/src/runtime/gateway/mcp/router.ts +117 -182
- package/src/runtime/gateway/mcp/runtime-tools.ts +331 -409
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +1 -219
- package/src/runtime/plugins/browser-adapter.ts +5 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +1 -1
- package/src/runtime/plugins/store.ts +14 -6
- package/src/runtime/plugins/types.ts +1 -1
- package/src/runtime/projections/materialized-view.ts +1 -23
- package/src/runtime/recovery/maintenance-executor.ts +9 -0
- package/src/runtime/resources/ownership.ts +0 -1
- package/src/runtime/root/gateway-adapter.ts +2 -0
- package/src/runtime/root/mcp-transport.ts +10 -2
- package/src/runtime/root/package-connector-service.ts +136 -0
- package/src/runtime/root/package-runtime-release.ts +161 -0
- package/src/runtime/root/package-runtime-service.ts +185 -0
- package/src/runtime/root/release-materialize.ts +19 -0
- package/src/runtime/root/runtime.ts +2 -1
- package/src/runtime/root/scheduler.ts +5 -0
- package/src/runtime/root/service.ts +18 -5
- package/src/runtime/standalone-recovery/core.ts +65 -16
- package/src/runtime/workflow/findings/store.ts +145 -106
- package/src/runtime/workflow/findings/types.ts +22 -0
- package/src/runtime/workflow/schedules/engine.ts +132 -2
- package/src/runtime/workflow/schedules/pending-activation.ts +139 -0
- package/src/runtime/workflow/schedules/work-continuation.ts +218 -0
- package/docs/CHANGELOG.md +0 -978
- package/docs/README.zh-CN.md +0 -58
- package/scripts/smoke-chatgpt-supervised-automation.ts +0 -208
- package/src/cli/commands/controller.ts +0 -680
- package/src/runtime/workflow/campaigns/cleanup.ts +0 -263
- package/src/runtime/workflow/campaigns/engine.ts +0 -112
- package/src/runtime/workflow/campaigns/normalize.ts +0 -100
- package/src/runtime/workflow/campaigns/review.ts +0 -315
- package/src/runtime/workflow/campaigns/store.ts +0 -526
- package/src/runtime/workflow/campaigns/supervisor.ts +0 -112
- package/src/runtime/workflow/campaigns/types.ts +0 -290
- /package/src/runtime/{workflow/campaigns → execution}/workspace-agent.ts +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'fs';
|
|
3
|
+
import { dirname, join, relative, resolve } from 'path';
|
|
4
|
+
import { ensureControllerHome } from '../../cli/repositories/controller-home';
|
|
5
|
+
import { CONTROL_PLANE_SCHEMA_VERSION } from '../control-plane/persistence/sqlite-store';
|
|
6
|
+
import { packageRuntimeSourceRoot } from '../control-plane/runtime-generation';
|
|
7
|
+
import { FORGE_VERSION } from '../../version';
|
|
8
|
+
import { publishRuntimeRelease, type RuntimeReleaseAuthority } from './release-store';
|
|
9
|
+
import type { RuntimeReleaseManifest } from './types';
|
|
10
|
+
|
|
11
|
+
export interface PackageRuntimeFileRecord {
|
|
12
|
+
path: string;
|
|
13
|
+
sha256: string;
|
|
14
|
+
bytes: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PackageRuntimeRelease {
|
|
18
|
+
releaseId: string;
|
|
19
|
+
releaseRoot: string;
|
|
20
|
+
packageRoot: string;
|
|
21
|
+
packageVersion: string;
|
|
22
|
+
packageFingerprint: string;
|
|
23
|
+
artifactIdentity: string;
|
|
24
|
+
entrypointPath: string;
|
|
25
|
+
manifestPath: string;
|
|
26
|
+
indexPath: string;
|
|
27
|
+
fileCount: number;
|
|
28
|
+
authority: RuntimeReleaseAuthority;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const PACKAGE_RUNTIME_ROOTS = ['src', 'bin', 'assets', 'scripts'] as const;
|
|
32
|
+
const PACKAGE_RUNTIME_FILES = ['package.json'] as const;
|
|
33
|
+
|
|
34
|
+
function sha256(value: Buffer | string): string {
|
|
35
|
+
return createHash('sha256').update(value).digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function atomicWrite(path: string, content: string, mode = 0o600): void {
|
|
39
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
40
|
+
const temporary = `${path}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
41
|
+
writeFileSync(temporary, content, { encoding: 'utf8', mode });
|
|
42
|
+
renameSync(temporary, path);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function walkRegularFiles(root: string, current: string, output: string[]): void {
|
|
46
|
+
if (!existsSync(current)) return;
|
|
47
|
+
const stat = lstatSync(current);
|
|
48
|
+
if (stat.isSymbolicLink()) return;
|
|
49
|
+
if (stat.isFile()) {
|
|
50
|
+
output.push(relative(root, current).split('\\').join('/'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!stat.isDirectory()) return;
|
|
54
|
+
for (const entry of readdirSync(current).sort()) walkRegularFiles(root, join(current, entry), output);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function packageRuntimeFileIndex(packageRoot = packageRuntimeSourceRoot()): PackageRuntimeFileRecord[] {
|
|
58
|
+
const root = resolve(packageRoot);
|
|
59
|
+
const paths: string[] = [];
|
|
60
|
+
for (const directory of PACKAGE_RUNTIME_ROOTS) walkRegularFiles(root, join(root, directory), paths);
|
|
61
|
+
for (const file of PACKAGE_RUNTIME_FILES) walkRegularFiles(root, join(root, file), paths);
|
|
62
|
+
const unique = [...new Set(paths)].sort();
|
|
63
|
+
if (!unique.includes('package.json') || !unique.some((path) => path === 'bin/forge-runtime.mjs')) {
|
|
64
|
+
throw new Error(`PACKAGE_RUNTIME_SURFACE_INCOMPLETE: ${root}`);
|
|
65
|
+
}
|
|
66
|
+
return unique.map((path) => {
|
|
67
|
+
const bytes = readFileSync(join(root, path));
|
|
68
|
+
return { path, sha256: sha256(bytes), bytes: bytes.length };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function packageRuntimeFingerprint(records: PackageRuntimeFileRecord[]): string {
|
|
73
|
+
const hash = createHash('sha256');
|
|
74
|
+
for (const record of records) hash.update(record.path).update('\0').update(record.sha256).update('\0').update(String(record.bytes)).update('\n');
|
|
75
|
+
return hash.digest('hex');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function packageVersion(root: string): string {
|
|
79
|
+
try {
|
|
80
|
+
const value = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { name?: unknown; version?: unknown };
|
|
81
|
+
if (value.name !== '@moretea-labs/forge') throw new Error('package name mismatch');
|
|
82
|
+
if (typeof value.version !== 'string' || !value.version.trim()) throw new Error('package version missing');
|
|
83
|
+
return value.version.trim();
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new Error(`PACKAGE_RUNTIME_IDENTITY_INVALID: ${error instanceof Error ? error.message : String(error)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function launcherSource(input: { packageRoot: string; indexPath: string; indexSha256: string }): string {
|
|
90
|
+
return `#!/usr/bin/env node\n`
|
|
91
|
+
+ `import { createHash } from 'node:crypto';\n`
|
|
92
|
+
+ `import { existsSync, readFileSync } from 'node:fs';\n`
|
|
93
|
+
+ `import { join } from 'node:path';\n`
|
|
94
|
+
+ `import { spawn } from 'node:child_process';\n`
|
|
95
|
+
+ `const packageRoot=${JSON.stringify(input.packageRoot)};\n`
|
|
96
|
+
+ `const indexPath=${JSON.stringify(input.indexPath)};\n`
|
|
97
|
+
+ `const expectedIndex=${JSON.stringify(input.indexSha256)};\n`
|
|
98
|
+
+ `const digest=(v)=>createHash('sha256').update(v).digest('hex');\n`
|
|
99
|
+
+ `let raw; try { raw=readFileSync(indexPath); } catch (error) { console.error('FORGE_PACKAGE_RUNTIME_INDEX_MISSING: '+error.message); process.exit(78); }\n`
|
|
100
|
+
+ `if(digest(raw)!==expectedIndex){console.error('FORGE_PACKAGE_RUNTIME_INDEX_CHANGED');process.exit(78);}\n`
|
|
101
|
+
+ `let records; try { records=JSON.parse(raw.toString('utf8')).files; } catch { console.error('FORGE_PACKAGE_RUNTIME_INDEX_INVALID'); process.exit(78); }\n`
|
|
102
|
+
+ `for(const record of records){const path=join(packageRoot,record.path);if(!existsSync(path)||digest(readFileSync(path))!==record.sha256){console.error('FORGE_PACKAGE_RUNTIME_SOURCE_CHANGED: '+record.path);process.exit(78);}}\n`
|
|
103
|
+
+ `const runtime=join(packageRoot,'bin','forge-runtime.mjs');\n`
|
|
104
|
+
+ `const child=spawn(process.execPath,[runtime,...process.argv.slice(2)],{stdio:'inherit',env:{...process.env,FORGE_FORCE_NODE:'1'}});\n`
|
|
105
|
+
+ `for(const signal of ['SIGINT','SIGTERM','SIGHUP']){try{process.on(signal,()=>child.kill(signal));}catch{}}\n`
|
|
106
|
+
+ `child.on('error',(error)=>{console.error('FORGE_PACKAGE_RUNTIME_LAUNCH_FAILED: '+error.message);process.exit(1);});\n`
|
|
107
|
+
+ `child.on('exit',(code)=>process.exit(code??1));\n`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function materializePackageRuntimeRelease(input: {
|
|
111
|
+
controllerHome?: string;
|
|
112
|
+
packageRoot?: string;
|
|
113
|
+
operationId?: string;
|
|
114
|
+
} = {}): PackageRuntimeRelease {
|
|
115
|
+
const controllerHome = ensureControllerHome(input.controllerHome);
|
|
116
|
+
const packageRoot = resolve(input.packageRoot ?? packageRuntimeSourceRoot());
|
|
117
|
+
const version = packageVersion(packageRoot);
|
|
118
|
+
const records = packageRuntimeFileIndex(packageRoot);
|
|
119
|
+
const fingerprint = packageRuntimeFingerprint(records);
|
|
120
|
+
const safeVersion = version.replace(/[^A-Za-z0-9._-]+/g, '-');
|
|
121
|
+
const releaseId = `package-${safeVersion}-${fingerprint.slice(0, 16)}`;
|
|
122
|
+
const releaseRoot = join(controllerHome, 'runtime', 'releases', releaseId);
|
|
123
|
+
const indexPath = join(releaseRoot, 'package-files.json');
|
|
124
|
+
const indexJson = `${JSON.stringify({ schemaVersion: 1, package: '@moretea-labs/forge', version, fingerprint, files: records }, null, 2)}\n`;
|
|
125
|
+
atomicWrite(indexPath, indexJson);
|
|
126
|
+
const entrypointPath = join(releaseRoot, 'forge-runtime');
|
|
127
|
+
const launcher = launcherSource({ packageRoot, indexPath, indexSha256: sha256(Buffer.from(indexJson)) });
|
|
128
|
+
atomicWrite(entrypointPath, launcher, 0o700);
|
|
129
|
+
chmodSync(entrypointPath, 0o700);
|
|
130
|
+
const artifactIdentity = `sha256:${sha256(Buffer.from(launcher))}`;
|
|
131
|
+
const manifest: RuntimeReleaseManifest = {
|
|
132
|
+
schemaVersion: 1,
|
|
133
|
+
releaseId,
|
|
134
|
+
artifactIdentity,
|
|
135
|
+
entrypoint: 'forge-runtime',
|
|
136
|
+
arguments: [],
|
|
137
|
+
configurationSchemaVersion: 1,
|
|
138
|
+
controllerHome,
|
|
139
|
+
databaseSchemaCompatibility: { minimum: CONTROL_PLANE_SCHEMA_VERSION, maximum: CONTROL_PLANE_SCHEMA_VERSION },
|
|
140
|
+
workerProtocolVersion: 1,
|
|
141
|
+
releaseRevision: `package:${version}:${fingerprint}`,
|
|
142
|
+
cleanWorkspace: true,
|
|
143
|
+
createdAt: new Date().toISOString(),
|
|
144
|
+
};
|
|
145
|
+
const manifestPath = join(releaseRoot, 'manifest.json');
|
|
146
|
+
atomicWrite(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
147
|
+
const authority = publishRuntimeRelease(controllerHome, manifestPath, input.operationId ?? `package-runtime-${FORGE_VERSION}-${Date.now()}`);
|
|
148
|
+
return {
|
|
149
|
+
releaseId,
|
|
150
|
+
releaseRoot,
|
|
151
|
+
packageRoot,
|
|
152
|
+
packageVersion: version,
|
|
153
|
+
packageFingerprint: fingerprint,
|
|
154
|
+
artifactIdentity,
|
|
155
|
+
entrypointPath,
|
|
156
|
+
manifestPath,
|
|
157
|
+
indexPath,
|
|
158
|
+
fileCount: records.length,
|
|
159
|
+
authority,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { spawn, spawnSync, type SpawnSyncReturns } from 'child_process';
|
|
2
|
+
import { closeSync, mkdirSync, openSync, renameSync, writeFileSync } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { dirname, join, resolve } from 'path';
|
|
5
|
+
import { resolveControllerHome } from '../../cli/repositories/controller-home';
|
|
6
|
+
import { RUNTIME_WRITE_CLAIM_ENV } from './write-fence';
|
|
7
|
+
import {
|
|
8
|
+
activeRuntimeEntrypoint,
|
|
9
|
+
activeRuntimeLaunchSpec,
|
|
10
|
+
forgeRuntimeServicePaths,
|
|
11
|
+
installForgeRuntimeService,
|
|
12
|
+
syncForgeRuntimeActiveEntrypoint,
|
|
13
|
+
writeForgeRuntimeServiceConfig,
|
|
14
|
+
type ForgeRuntimeServiceConfig,
|
|
15
|
+
} from './service';
|
|
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';
|
|
19
|
+
|
|
20
|
+
export type PackageRuntimeServiceMode = 'launchd' | 'systemd-user' | 'portable';
|
|
21
|
+
|
|
22
|
+
export interface PackageRuntimeServiceInstallResult {
|
|
23
|
+
status: 'installed';
|
|
24
|
+
mode: PackageRuntimeServiceMode;
|
|
25
|
+
persistent: boolean;
|
|
26
|
+
controllerHome: string;
|
|
27
|
+
release: PackageRuntimeRelease;
|
|
28
|
+
servicePath?: string;
|
|
29
|
+
pid?: number;
|
|
30
|
+
warnings: string[];
|
|
31
|
+
connector?: PackageConnectorServiceResult;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PackageRuntimeServiceOptions {
|
|
35
|
+
controllerHome?: string;
|
|
36
|
+
packageRoot?: string;
|
|
37
|
+
host?: string;
|
|
38
|
+
port?: number;
|
|
39
|
+
authTokenFile: string;
|
|
40
|
+
exclusiveWorkId?: string;
|
|
41
|
+
platform?: NodeJS.Platform;
|
|
42
|
+
env?: NodeJS.ProcessEnv;
|
|
43
|
+
forcePortable?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function atomicWrite(path: string, content: string, mode = 0o600): void {
|
|
47
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
48
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
49
|
+
writeFileSync(temporary, content, { encoding: 'utf8', mode });
|
|
50
|
+
renameSync(temporary, path);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function systemdEscape(value: string): string {
|
|
54
|
+
return `"${value.replaceAll('%', '%%').replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function renderForgeRuntimeSystemdUserUnit(input: {
|
|
58
|
+
description?: string;
|
|
59
|
+
executable: string;
|
|
60
|
+
args: string[];
|
|
61
|
+
environment: Record<string, string>;
|
|
62
|
+
}): string {
|
|
63
|
+
const environment = Object.entries(input.environment)
|
|
64
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
65
|
+
.map(([key, value]) => `Environment=${systemdEscape(`${key}=${value}`)}`)
|
|
66
|
+
.join('\n');
|
|
67
|
+
return [
|
|
68
|
+
'[Unit]',
|
|
69
|
+
`Description=${input.description ?? 'Forge Runtime'}`,
|
|
70
|
+
'After=network-online.target',
|
|
71
|
+
'',
|
|
72
|
+
'[Service]',
|
|
73
|
+
'Type=simple',
|
|
74
|
+
`ExecStart=${[input.executable, ...input.args].map(systemdEscape).join(' ')}`,
|
|
75
|
+
...(environment ? [environment] : []),
|
|
76
|
+
'Restart=on-failure',
|
|
77
|
+
'RestartSec=5',
|
|
78
|
+
'',
|
|
79
|
+
'[Install]',
|
|
80
|
+
'WantedBy=default.target',
|
|
81
|
+
'',
|
|
82
|
+
].join('\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function commandSucceeded(command: string, args: string[], env: NodeJS.ProcessEnv): SpawnSyncReturns<string> {
|
|
86
|
+
return spawnSync(command, args, { encoding: 'utf8', env, timeout: 30_000 });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function systemdUserAvailable(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
90
|
+
const result = commandSucceeded('systemctl', ['--user', 'show-environment'], env);
|
|
91
|
+
return result.status === 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function cleanRuntimeInstallerEnvironment(env: NodeJS.ProcessEnv, releaseEnvironment: Record<string, string>): NodeJS.ProcessEnv {
|
|
95
|
+
const next = { ...env };
|
|
96
|
+
for (const key of Object.values(RUNTIME_WRITE_CLAIM_ENV)) delete next[key];
|
|
97
|
+
delete next.FORGE_CONTROLLER_LIFECYCLE_OWNER;
|
|
98
|
+
delete next.FORGE_RELEASE_PATH;
|
|
99
|
+
delete next.FORGE_RELEASE_REVISION;
|
|
100
|
+
delete next.FORGE_RELEASE_SOURCE_COMMIT;
|
|
101
|
+
delete next.FORGE_RELEASE_CLEAN_WORKSPACE;
|
|
102
|
+
return { ...next, ...releaseEnvironment };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function installSystemdUserService(controllerHome: string, env: NodeJS.ProcessEnv): string {
|
|
106
|
+
const entrypoint = activeRuntimeEntrypoint(controllerHome);
|
|
107
|
+
const launch = activeRuntimeLaunchSpec(controllerHome);
|
|
108
|
+
if (!entrypoint || !launch) throw new Error('FORGE_PACKAGE_RUNTIME_LAUNCH_SPEC_MISSING');
|
|
109
|
+
const label = forgeRuntimeServicePaths(controllerHome).label;
|
|
110
|
+
const unitName = `${label}.service`;
|
|
111
|
+
const unitPath = join(env.HOME ?? homedir(), '.config', 'systemd', 'user', unitName);
|
|
112
|
+
atomicWrite(unitPath, renderForgeRuntimeSystemdUserUnit({ executable: entrypoint, args: launch.args, environment: launch.environment }), 0o644);
|
|
113
|
+
for (const args of [['--user', 'daemon-reload'], ['--user', 'enable', '--now', unitName]]) {
|
|
114
|
+
const result = commandSucceeded('systemctl', args, env);
|
|
115
|
+
if (result.status !== 0) throw new Error(`FORGE_RUNTIME_SYSTEMD_INSTALL_FAILED: systemctl ${args.join(' ')}: ${(result.stderr || result.stdout || '').trim()}`);
|
|
116
|
+
}
|
|
117
|
+
return unitPath;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function startPortableRuntime(controllerHome: string, env: NodeJS.ProcessEnv): number {
|
|
121
|
+
const entrypoint = activeRuntimeEntrypoint(controllerHome);
|
|
122
|
+
const launch = activeRuntimeLaunchSpec(controllerHome);
|
|
123
|
+
if (!entrypoint || !launch) throw new Error('FORGE_PACKAGE_RUNTIME_LAUNCH_SPEC_MISSING');
|
|
124
|
+
const paths = forgeRuntimeServicePaths(controllerHome);
|
|
125
|
+
mkdirSync(join(paths.serviceRoot, 'logs'), { recursive: true, mode: 0o700 });
|
|
126
|
+
const stdout = openSync(paths.stdoutPath, 'a', 0o600);
|
|
127
|
+
const stderr = openSync(paths.stderrPath, 'a', 0o600);
|
|
128
|
+
try {
|
|
129
|
+
const child = spawn(entrypoint, launch.args, {
|
|
130
|
+
detached: true,
|
|
131
|
+
stdio: ['ignore', stdout, stderr],
|
|
132
|
+
env: cleanRuntimeInstallerEnvironment(env, launch.environment),
|
|
133
|
+
});
|
|
134
|
+
if (!child.pid) throw new Error('FORGE_PORTABLE_RUNTIME_START_FAILED: child pid unavailable');
|
|
135
|
+
child.unref();
|
|
136
|
+
atomicWrite(join(paths.serviceRoot, 'portable.json'), `${JSON.stringify({ schemaVersion: 1, pid: child.pid, startedAt: new Date().toISOString() }, null, 2)}\n`);
|
|
137
|
+
return child.pid;
|
|
138
|
+
} finally {
|
|
139
|
+
closeSync(stdout);
|
|
140
|
+
closeSync(stderr);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function installPackageRuntimeService(options: PackageRuntimeServiceOptions): Promise<PackageRuntimeServiceInstallResult> {
|
|
145
|
+
const controllerHome = resolveControllerHome(options.controllerHome);
|
|
146
|
+
const release = materializePackageRuntimeRelease({ controllerHome, packageRoot: options.packageRoot });
|
|
147
|
+
const config: ForgeRuntimeServiceConfig = {
|
|
148
|
+
schemaVersion: 1,
|
|
149
|
+
controllerHome,
|
|
150
|
+
repositoryRoot: release.packageRoot,
|
|
151
|
+
host: options.host ?? '127.0.0.1',
|
|
152
|
+
port: options.port ?? 8765,
|
|
153
|
+
authTokenFile: options.authTokenFile,
|
|
154
|
+
...(options.exclusiveWorkId?.trim() ? { exclusiveWorkId: options.exclusiveWorkId.trim() } : {}),
|
|
155
|
+
};
|
|
156
|
+
writeForgeRuntimeServiceConfig(config);
|
|
157
|
+
syncForgeRuntimeActiveEntrypoint(controllerHome);
|
|
158
|
+
const platform = options.platform ?? process.platform;
|
|
159
|
+
const env = options.env ?? process.env;
|
|
160
|
+
|
|
161
|
+
let base: PackageRuntimeServiceInstallResult;
|
|
162
|
+
if (!options.forcePortable && platform === 'darwin') {
|
|
163
|
+
const paths = await installForgeRuntimeService({ config, runnerPath: join(release.packageRoot, 'bin', 'forge-runtime-service.mjs'), nodeExecutable: process.execPath });
|
|
164
|
+
base = { status: 'installed', mode: 'launchd', persistent: true, controllerHome, release, servicePath: paths.installedPlistPath, warnings: [] };
|
|
165
|
+
} else if (!options.forcePortable && platform === 'linux' && systemdUserAvailable(env)) {
|
|
166
|
+
const unitPath = installSystemdUserService(controllerHome, env);
|
|
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
|
+
};
|
|
176
|
+
}
|
|
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 };
|
|
185
|
+
}
|
|
@@ -29,6 +29,7 @@ export interface StagedRuntimeRelease {
|
|
|
29
29
|
codeGraphNodeArtifactIdentity?: string;
|
|
30
30
|
codeGraphSidecarArtifactIdentity?: string;
|
|
31
31
|
codeGraphLibraryArtifactIdentity?: string;
|
|
32
|
+
controllerUiArtifactIdentity?: string;
|
|
32
33
|
manifestSha256: string;
|
|
33
34
|
sourceCommit: string;
|
|
34
35
|
}
|
|
@@ -395,6 +396,17 @@ export function stageRuntimeRelease(input: {
|
|
|
395
396
|
const codeGraphSidecarArtifactIdentity = `sha256:${sha256(codeGraphSidecarPath)}`;
|
|
396
397
|
const codeGraphLibraryArtifactIdentity = `sha256:${sha256Directory(codeGraphLibraryPath)}`;
|
|
397
398
|
|
|
399
|
+
const controllerUiRoot = 'ui-dist' as const;
|
|
400
|
+
const sourceControllerUiPath = join(sourceRoot, 'src', 'cli', 'local-bridge', controllerUiRoot);
|
|
401
|
+
const sourceControllerUiJs = join(sourceControllerUiPath, 'app.js');
|
|
402
|
+
const sourceControllerUiCss = join(sourceControllerUiPath, 'app.css');
|
|
403
|
+
if (!existsSync(sourceControllerUiJs) || !existsSync(sourceControllerUiCss)) {
|
|
404
|
+
throw new Error(`RUNTIME_RELEASE_CONTROLLER_UI_SOURCE_MISSING: ${sourceControllerUiPath}`);
|
|
405
|
+
}
|
|
406
|
+
const controllerUiPath = join(staging, controllerUiRoot);
|
|
407
|
+
cpSync(sourceControllerUiPath, controllerUiPath, { recursive: true, force: false });
|
|
408
|
+
const controllerUiArtifactIdentity = `sha256:${sha256Directory(controllerUiPath)}`;
|
|
409
|
+
|
|
398
410
|
const desktopHelperEntrypoint = 'forge-desktop-helper.mjs' as const;
|
|
399
411
|
const sourceDesktopHelperPath = join(sourceRoot, 'bin', desktopHelperEntrypoint);
|
|
400
412
|
if (!existsSync(sourceDesktopHelperPath)) {
|
|
@@ -429,6 +441,8 @@ export function stageRuntimeRelease(input: {
|
|
|
429
441
|
codeGraphSidecarArtifactIdentity,
|
|
430
442
|
codeGraphLibraryRoot,
|
|
431
443
|
codeGraphLibraryArtifactIdentity,
|
|
444
|
+
controllerUiRoot,
|
|
445
|
+
controllerUiArtifactIdentity,
|
|
432
446
|
arguments: [],
|
|
433
447
|
configurationSchemaVersion: 1,
|
|
434
448
|
controllerHome: resolve(input.controllerHome),
|
|
@@ -460,6 +474,7 @@ export function stageRuntimeRelease(input: {
|
|
|
460
474
|
codeGraphNodeArtifactIdentity,
|
|
461
475
|
codeGraphSidecarArtifactIdentity,
|
|
462
476
|
codeGraphLibraryArtifactIdentity,
|
|
477
|
+
controllerUiArtifactIdentity,
|
|
463
478
|
manifestSha256: createHash('sha256').update(`${JSON.stringify(manifest, null, 2)}\n`).digest('hex'),
|
|
464
479
|
sourceCommit,
|
|
465
480
|
};
|
|
@@ -506,4 +521,8 @@ export function assertRuntimeReleaseFiles(release: StagedRuntimeRelease): void {
|
|
|
506
521
|
if (release.codeGraphLibraryArtifactIdentity && !existsSync(join(release.releasePath, 'codegraph-lib', 'dist', 'index.js'))) {
|
|
507
522
|
throw new Error(`RUNTIME_RELEASE_CODEGRAPH_LIBRARY_MISSING: ${join(release.releasePath, 'codegraph-lib', 'dist', 'index.js')}`);
|
|
508
523
|
}
|
|
524
|
+
if (release.controllerUiArtifactIdentity
|
|
525
|
+
&& (!existsSync(join(release.releasePath, 'ui-dist', 'app.js')) || !existsSync(join(release.releasePath, 'ui-dist', 'app.css')))) {
|
|
526
|
+
throw new Error(`RUNTIME_RELEASE_CONTROLLER_UI_MISSING: ${join(release.releasePath, 'ui-dist')}`);
|
|
527
|
+
}
|
|
509
528
|
}
|
|
@@ -228,11 +228,12 @@ export class CanonicalForgeRuntime {
|
|
|
228
228
|
port: this.config.port,
|
|
229
229
|
authToken: this.config.authToken,
|
|
230
230
|
readiness: () => this.readiness(),
|
|
231
|
-
createServer: (principalId, sessionId) => createRuntimeGatewayServer(this.controller!, principalId, {
|
|
231
|
+
createServer: (principalId, sessionId, controllerType) => createRuntimeGatewayServer(this.controller!, principalId, {
|
|
232
232
|
controllerHome: this.config.controllerHome,
|
|
233
233
|
runtimeInstanceId: this.runtimeInstanceId,
|
|
234
234
|
runtimeSourceRoot,
|
|
235
235
|
sessionId,
|
|
236
|
+
controllerType,
|
|
236
237
|
}),
|
|
237
238
|
onFatal: (error) => this.failCore('MCP_TRANSPORT_FAILED', error.message),
|
|
238
239
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GlobalScheduler, readSchedulerHealthSnapshot } from '../control-plane/global-scheduler/scheduler';
|
|
2
|
+
import { applyPendingContinuationActivations } from '../workflow/schedules/pending-activation';
|
|
2
3
|
|
|
3
4
|
export interface RuntimeSchedulerHandle {
|
|
4
5
|
ready: Promise<void>;
|
|
@@ -34,6 +35,10 @@ export function startInProcessScheduler(
|
|
|
34
35
|
readyTimeoutMs = 5_000,
|
|
35
36
|
): RuntimeSchedulerHandle {
|
|
36
37
|
const abort = new AbortController();
|
|
38
|
+
// Old Runtime releases ignore pending activation markers entirely. A naturally
|
|
39
|
+
// restarted Runtime that contains this code consumes them once, before its
|
|
40
|
+
// sole Scheduler starts, so no second scheduler or rollout hook is required.
|
|
41
|
+
applyPendingContinuationActivations(controllerHome);
|
|
37
42
|
const startedAfterMs = Date.now();
|
|
38
43
|
const scheduler = new GlobalScheduler(controllerHome, {}, {
|
|
39
44
|
controllerPid: process.pid,
|
|
@@ -97,6 +97,7 @@ interface RuntimeReleaseManifestRecord {
|
|
|
97
97
|
controllerHome: string;
|
|
98
98
|
artifactIdentity: string;
|
|
99
99
|
arguments?: string[];
|
|
100
|
+
diagnosticEntrypoint?: string;
|
|
100
101
|
browserAutomationHelperEntrypoint?: string;
|
|
101
102
|
browserAutomationHelperArtifactIdentity?: string;
|
|
102
103
|
browserAutomationHelperContractIdentity?: string;
|
|
@@ -113,7 +114,7 @@ interface ActiveRuntimeReleaseRecord {
|
|
|
113
114
|
manifest: RuntimeReleaseManifestRecord;
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
interface ActiveRuntimeLaunchSpec {
|
|
117
|
+
export interface ActiveRuntimeLaunchSpec {
|
|
117
118
|
args: string[];
|
|
118
119
|
environment: Record<string, string>;
|
|
119
120
|
}
|
|
@@ -168,7 +169,7 @@ async function retireLegacyBrowserAutomationLaunchAgent(controllerHome: string):
|
|
|
168
169
|
rmSync(legacy.installedPlistPath, { force: true });
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
function activeRuntimeLaunchSpec(controllerHome: string): ActiveRuntimeLaunchSpec | undefined {
|
|
172
|
+
export function activeRuntimeLaunchSpec(controllerHome: string): ActiveRuntimeLaunchSpec | undefined {
|
|
172
173
|
const home = resolveControllerHome(controllerHome);
|
|
173
174
|
const active = readActiveRuntimeRelease(home);
|
|
174
175
|
if (!active) return undefined;
|
|
@@ -178,6 +179,12 @@ function activeRuntimeLaunchSpec(controllerHome: string): ActiveRuntimeLaunchSpe
|
|
|
178
179
|
if (!Array.isArray(manifestArguments) || !manifestArguments.every((argument) => typeof argument === 'string')) {
|
|
179
180
|
throw new Error('FORGE_RUNTIME_RELEASE_ARGUMENTS_INVALID');
|
|
180
181
|
}
|
|
182
|
+
const diagnosticEntrypoint = typeof active.manifest.diagnosticEntrypoint === 'string' && active.manifest.diagnosticEntrypoint.trim()
|
|
183
|
+
? resolve(active.releaseRoot, active.manifest.diagnosticEntrypoint)
|
|
184
|
+
: undefined;
|
|
185
|
+
if (diagnosticEntrypoint && (!isInside(active.releaseRoot, diagnosticEntrypoint) || !existsSync(diagnosticEntrypoint))) {
|
|
186
|
+
throw new Error('FORGE_RUNTIME_RELEASE_DIAGNOSTIC_ENTRYPOINT_INVALID');
|
|
187
|
+
}
|
|
181
188
|
return {
|
|
182
189
|
args: [
|
|
183
190
|
'--controller-home', home,
|
|
@@ -192,6 +199,7 @@ function activeRuntimeLaunchSpec(controllerHome: string): ActiveRuntimeLaunchSpe
|
|
|
192
199
|
environment: {
|
|
193
200
|
FORGE_CONTROLLER_HOME: home,
|
|
194
201
|
FORGE_CONTROLLER_RUNTIME_SOURCE_ROOT: active.releaseRoot,
|
|
202
|
+
...(diagnosticEntrypoint ? { FORGE_CLI_EXECUTABLE: diagnosticEntrypoint } : {}),
|
|
195
203
|
FORGE_RELEASE_PATH: active.releaseRoot,
|
|
196
204
|
FORGE_RELEASE_ID: active.releaseId,
|
|
197
205
|
FORGE_RELEASE_REVISION: active.manifest.releaseRevision ?? active.releaseId,
|
|
@@ -261,12 +269,17 @@ export function renderForgeRuntimeLaunchAgent(input: { paths: ForgeRuntimeServic
|
|
|
261
269
|
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${environmentXml} <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <dict>\n <key>SuccessfulExit</key>\n <false/>\n </dict>\n <key>ThrottleInterval</key>\n <integer>5</integer>\n <key>ProcessType</key>\n <string>Interactive</string>\n <key>StandardOutPath</key>\n <string>${xml(input.paths.stdoutPath)}</string>\n <key>StandardErrorPath</key>\n <string>${xml(input.paths.stderrPath)}</string>\n</dict>\n</plist>\n`;
|
|
262
270
|
}
|
|
263
271
|
|
|
264
|
-
export
|
|
265
|
-
|
|
266
|
-
const config = validateForgeRuntimeServiceConfig(input.config);
|
|
272
|
+
export function writeForgeRuntimeServiceConfig(input: ForgeRuntimeServiceConfig): { config: ForgeRuntimeServiceConfig; paths: ForgeRuntimeServicePaths } {
|
|
273
|
+
const config = validateForgeRuntimeServiceConfig(input);
|
|
267
274
|
const paths = forgeRuntimeServicePaths(config.controllerHome);
|
|
268
275
|
mkdirSync(join(paths.serviceRoot, 'logs'), { recursive: true, mode: 0o700 });
|
|
269
276
|
atomicWrite(paths.configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
277
|
+
return { config, paths };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function installForgeRuntimeService(input: { config: ForgeRuntimeServiceConfig; runnerPath: string; nodeExecutable?: string }): Promise<ForgeRuntimeServicePaths> {
|
|
281
|
+
if (process.platform !== 'darwin') throw new Error('FORGE_RUNTIME_SERVICE_PLATFORM_UNSUPPORTED: launchd requires macOS');
|
|
282
|
+
const { config, paths } = writeForgeRuntimeServiceConfig(input.config);
|
|
270
283
|
ensureForgeRuntimeLaunchAgentContract({
|
|
271
284
|
controllerHome: config.controllerHome,
|
|
272
285
|
bootstrapNodeExecutable: input.nodeExecutable ?? process.execPath,
|
|
@@ -16,6 +16,12 @@ import {
|
|
|
16
16
|
} from '../root/release-store';
|
|
17
17
|
import type { RuntimeReleaseManifest } from '../root/types';
|
|
18
18
|
import { createRecoveryHttpTransport, type RecoveryHttpTransport } from './http-transport';
|
|
19
|
+
import {
|
|
20
|
+
migrateStoppedRepoLocalControllerHomeStorage,
|
|
21
|
+
repoLocalControllerHomeStorageNeedsMigration,
|
|
22
|
+
rollbackStoppedRepoLocalControllerHomeStorage,
|
|
23
|
+
type ControllerHomeStorageMigration,
|
|
24
|
+
} from '../../cli/repositories/controller-home';
|
|
19
25
|
|
|
20
26
|
/** Standalone recovery reads only canonical Runtime observation and whole-release authority. */
|
|
21
27
|
export interface PublicTunnelServiceConfig {
|
|
@@ -1590,7 +1596,8 @@ export async function activateRuntimeRelease(
|
|
|
1590
1596
|
audit(config, 'runtime_release_activation_candidate_invalid', { detail, ...(guard.requestId ? { requestId: guard.requestId } : {}) });
|
|
1591
1597
|
return { ok: false, attempted: false, noOp: true, detail };
|
|
1592
1598
|
}
|
|
1593
|
-
|
|
1599
|
+
const platform = dependencies.platform ?? process.platform;
|
|
1600
|
+
if (platform !== 'darwin') {
|
|
1594
1601
|
return { ok: false, attempted: false, noOp: true, detail: 'Runtime release activation currently requires the configured macOS launchd service' };
|
|
1595
1602
|
}
|
|
1596
1603
|
const uid = await (dependencies.currentUid ?? currentUid)();
|
|
@@ -1638,7 +1645,19 @@ export async function activateRuntimeRelease(
|
|
|
1638
1645
|
});
|
|
1639
1646
|
return { ok: false, attempted: false, noOp: true, detail, operationId } satisfies RuntimeReleaseActivationResult;
|
|
1640
1647
|
}
|
|
1648
|
+
let storageMigrationRequired = false;
|
|
1649
|
+
try {
|
|
1650
|
+
storageMigrationRequired = repoLocalControllerHomeStorageNeedsMigration(config.controllerHome, platform);
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
const detail = error instanceof Error ? error.message : 'Controller Home storage migration preflight failed';
|
|
1653
|
+
audit(config, 'runtime_release_activation_storage_preflight_failed', { operationId, requestId: lockRequestId, detail });
|
|
1654
|
+
return { ok: false, attempted: false, noOp: true, detail, operationId } satisfies RuntimeReleaseActivationResult;
|
|
1655
|
+
}
|
|
1641
1656
|
if (current && current.active.releaseId === candidate.manifest.releaseId) {
|
|
1657
|
+
if (storageMigrationRequired) {
|
|
1658
|
+
const detail = 'CONTROLLER_HOME_STORAGE_MIGRATION_REQUIRES_STAGED_RELEASE: stage a fresh immutable Runtime release so Recovery can migrate storage inside the activation transaction';
|
|
1659
|
+
return { ok: false, attempted: false, noOp: true, detail, operationId } satisfies RuntimeReleaseActivationResult;
|
|
1660
|
+
}
|
|
1642
1661
|
return {
|
|
1643
1662
|
ok: true,
|
|
1644
1663
|
attempted: false,
|
|
@@ -1649,7 +1668,7 @@ export async function activateRuntimeRelease(
|
|
|
1649
1668
|
} satisfies RuntimeReleaseActivationResult;
|
|
1650
1669
|
}
|
|
1651
1670
|
if (current) {
|
|
1652
|
-
if (runtimeBehaviorEquivalent(current.active.manifestPath, candidate.manifestPath)) {
|
|
1671
|
+
if (runtimeBehaviorEquivalent(current.active.manifestPath, candidate.manifestPath) && !storageMigrationRequired) {
|
|
1653
1672
|
const verify = await verifyLocal(config);
|
|
1654
1673
|
audit(config, 'runtime_release_activation_behavior_identical', {
|
|
1655
1674
|
operationId,
|
|
@@ -1727,24 +1746,44 @@ export async function activateRuntimeRelease(
|
|
|
1727
1746
|
audit(config, 'runtime_release_activation_launchd_contract_failed', { serviceTarget: service.target, operationId, detail });
|
|
1728
1747
|
return { ok: false, attempted: true, detail, serviceTarget: service.target, verify: await verifyLocal(config) } satisfies RuntimeReleaseActivationResult;
|
|
1729
1748
|
}
|
|
1730
|
-
|
|
1749
|
+
let storageMigration: ControllerHomeStorageMigration | undefined;
|
|
1731
1750
|
let after: VerifyResult;
|
|
1732
1751
|
let activationFailureDetail: string | undefined;
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1752
|
+
try {
|
|
1753
|
+
storageMigration = migrateStoppedRepoLocalControllerHomeStorage(config.controllerHome, platform);
|
|
1754
|
+
if (storageMigration.migrated) {
|
|
1755
|
+
audit(config, 'runtime_controller_home_noindex_migrated', {
|
|
1756
|
+
serviceTarget: service.target,
|
|
1757
|
+
operationId,
|
|
1758
|
+
logicalHome: storageMigration.logicalHome,
|
|
1759
|
+
physicalHome: storageMigration.physicalHome,
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
} catch (error) {
|
|
1763
|
+
activationFailureDetail = `Controller Home .noindex migration failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1764
|
+
audit(config, 'runtime_controller_home_noindex_migration_failed', { serviceTarget: service.target, operationId, detail: activationFailureDetail });
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (activationFailureDetail) {
|
|
1736
1768
|
after = await verifyLocal(config);
|
|
1737
1769
|
} else {
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1770
|
+
const started = await ensureLaunchdServiceStarted(service, runCommand);
|
|
1771
|
+
if (!started.ok) {
|
|
1772
|
+
activationFailureDetail = started.detail;
|
|
1773
|
+
audit(config, 'runtime_release_activation_start_failed', { serviceTarget: service.target, operationId, detail: started.detail });
|
|
1774
|
+
after = await verifyLocal(config);
|
|
1775
|
+
} else {
|
|
1776
|
+
after = await verifyPrimaryRuntimeAfterStart({
|
|
1777
|
+
config,
|
|
1778
|
+
timeoutMs: configuredPrimaryRuntimeService(config).postRestartVerifyTimeoutMs ?? 60_000,
|
|
1779
|
+
now,
|
|
1780
|
+
wait,
|
|
1781
|
+
verifyLocal,
|
|
1782
|
+
});
|
|
1783
|
+
if (after.ok && after.releases.active?.revision === candidate.manifest.releaseId) {
|
|
1784
|
+
audit(config, 'runtime_release_activation_succeeded', { serviceTarget: service.target, operationId, requestId: lockRequestId, activeRevision: after.releases.active?.revision, expectedAuthorityRevision: guard.expectedAuthorityRevision, expectedActiveReleaseId: guard.expectedActiveReleaseId, controllerHomeStorageMigrated: storageMigration?.migrated === true });
|
|
1785
|
+
return { ok: true, attempted: true, detail: storageMigration?.migrated ? 'requested Runtime release activated, Controller Home migrated to .noindex storage, and whole-Runtime verification passed' : 'requested Runtime release activated and passed whole-Runtime verification', serviceTarget: service.target, operationId, verify: after } satisfies RuntimeReleaseActivationResult;
|
|
1786
|
+
}
|
|
1748
1787
|
}
|
|
1749
1788
|
}
|
|
1750
1789
|
|
|
@@ -1772,6 +1811,16 @@ export async function activateRuntimeRelease(
|
|
|
1772
1811
|
try {
|
|
1773
1812
|
const rollbackOperationId = `recovery-activate-runtime-rollback-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
1774
1813
|
const restored = rollbackRuntimeRelease(config.controllerHome, rollbackOperationId);
|
|
1814
|
+
if (storageMigration?.migrated) {
|
|
1815
|
+
rollbackStoppedRepoLocalControllerHomeStorage(storageMigration);
|
|
1816
|
+
audit(config, 'runtime_controller_home_noindex_migration_rolled_back', {
|
|
1817
|
+
serviceTarget: service.target,
|
|
1818
|
+
operationId,
|
|
1819
|
+
rollbackOperationId,
|
|
1820
|
+
logicalHome: storageMigration.logicalHome,
|
|
1821
|
+
physicalHome: storageMigration.physicalHome,
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1775
1824
|
ensureForgeRuntimeLaunchAgentContract({ controllerHome: config.controllerHome });
|
|
1776
1825
|
const previousRevision = previousActive?.releaseId ?? '';
|
|
1777
1826
|
const previousIdentity = previousActive?.artifactIdentity;
|