@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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { existsSync, writeFileSync } from 'fs';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
3
|
import { dirname, join, resolve } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
-
import { resolveControllerHome } from '../repositories/controller-home';
|
|
5
|
+
import { resolveControllerHome, resolveRepoPreferredControllerHome } from '../repositories/controller-home';
|
|
6
6
|
import { findExecutionJob, listActiveExecutionJobs, listExecutionJobs } from '../../runtime/execution/jobs/store';
|
|
7
7
|
import { readJobEvents } from '../../runtime/evidence/event-ledger';
|
|
8
8
|
import { getRepository, listRepositories } from '../repositories/registry';
|
|
@@ -13,20 +13,73 @@ import { observeRuntimeStatus } from '../../runtime/root/status';
|
|
|
13
13
|
import { readMcpServiceBearerToken } from '../mcp/auth';
|
|
14
14
|
import { publishRuntimeRelease } from '../../runtime/root/release-store';
|
|
15
15
|
import { installForgeRuntimeService } from '../../runtime/root/service';
|
|
16
|
+
import { installPackageRuntimeService } from '../../runtime/root/package-runtime-service';
|
|
16
17
|
import { assertRuntimeReleaseFiles, stageRuntimeReleaseFromCandidateSource } from '../../runtime/root/release-materialize';
|
|
17
18
|
|
|
18
19
|
function output(value: unknown, json = true): void {
|
|
19
20
|
console.log(json ? JSON.stringify(value, null, 2) : String(value));
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
export function resolveRuntimeStateControllerHome(explicit?: string, cwd = process.cwd()): string {
|
|
24
|
+
return resolveRepoPreferredControllerHome(cwd, explicit);
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
export function buildRuntimeCommand(): Command {
|
|
23
28
|
const command = new Command('runtime').description('Inspect the canonical Runtime and durable execution state');
|
|
24
29
|
|
|
25
30
|
const service = command.command('service')
|
|
26
|
-
.description('Install
|
|
31
|
+
.description('Install or inspect the user-level Forge Runtime owner');
|
|
32
|
+
|
|
33
|
+
service.command('install-package')
|
|
34
|
+
.description('Install the current packaged Forge Runtime without requiring a Git checkout, Bun compilation, CodeGraph, or Standalone Recovery')
|
|
35
|
+
.option('--controller-home <path>', 'Controller Home; defaults to the user-level Forge Controller Home')
|
|
36
|
+
.option('--host <host>', 'MCP listener host', '127.0.0.1')
|
|
37
|
+
.option('--port <port>', 'MCP listener port', '8765')
|
|
38
|
+
.option('--auth-token-file <path>', 'Raw bearer token file (created from user-level MCP config when missing)')
|
|
39
|
+
.option('--portable', 'Force a detached session process instead of launchd/systemd user persistence')
|
|
40
|
+
.action(async (opts: { controllerHome?: string; host: string; port: string; authTokenFile?: string; portable?: boolean }) => {
|
|
41
|
+
const home = resolveControllerHome(opts.controllerHome);
|
|
42
|
+
const port = Number(opts.port);
|
|
43
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error('RUNTIME_SERVICE_PORT_INVALID');
|
|
44
|
+
const tokenPath = resolve(opts.authTokenFile ?? join(home, 'mcp', 'runtime-token'));
|
|
45
|
+
if (!existsSync(tokenPath)) {
|
|
46
|
+
const token = readMcpServiceBearerToken(home);
|
|
47
|
+
if (!token) throw new Error('RUNTIME_SERVICE_AUTH_TOKEN_UNAVAILABLE: run forge mcp setup chatgpt --user-level first');
|
|
48
|
+
mkdirSync(dirname(tokenPath), { recursive: true, mode: 0o700 });
|
|
49
|
+
writeFileSync(tokenPath, `${token}\n`, { mode: 0o600 });
|
|
50
|
+
}
|
|
51
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
52
|
+
const result = await installPackageRuntimeService({
|
|
53
|
+
controllerHome: home,
|
|
54
|
+
packageRoot,
|
|
55
|
+
host: opts.host,
|
|
56
|
+
port,
|
|
57
|
+
authTokenFile: tokenPath,
|
|
58
|
+
forcePortable: opts.portable === true,
|
|
59
|
+
});
|
|
60
|
+
output({
|
|
61
|
+
status: result.status,
|
|
62
|
+
mode: result.mode,
|
|
63
|
+
persistent: result.persistent,
|
|
64
|
+
controllerHome: result.controllerHome,
|
|
65
|
+
release: {
|
|
66
|
+
releaseId: result.release.releaseId,
|
|
67
|
+
packageVersion: result.release.packageVersion,
|
|
68
|
+
packageFingerprint: result.release.packageFingerprint,
|
|
69
|
+
artifactIdentity: result.release.artifactIdentity,
|
|
70
|
+
manifestPath: result.release.manifestPath,
|
|
71
|
+
fileCount: result.release.fileCount,
|
|
72
|
+
},
|
|
73
|
+
servicePath: result.servicePath,
|
|
74
|
+
pid: result.pid,
|
|
75
|
+
connector: result.connector,
|
|
76
|
+
warnings: result.warnings,
|
|
77
|
+
next: `forge runtime status --controller-home ${home}`,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
27
80
|
|
|
28
81
|
service.command('install')
|
|
29
|
-
.description('
|
|
82
|
+
.description('Advanced/source mode: build an immutable Git Runtime release and install the macOS launchd owner')
|
|
30
83
|
.requiredOption('--controller-home <path>', 'Explicit Controller Home')
|
|
31
84
|
.requiredOption('--repo <path>', 'Repository root used as the immutable release source')
|
|
32
85
|
.option('--host <host>', 'MCP listener host', '127.0.0.1')
|
|
@@ -115,7 +168,7 @@ export function buildRuntimeCommand(): Command {
|
|
|
115
168
|
.requiredOption('--controller-home <path>', 'Explicit Controller Home')
|
|
116
169
|
.option('--json', 'Output JSON')
|
|
117
170
|
.action((opts: { controllerHome: string; json?: boolean }) => {
|
|
118
|
-
const home =
|
|
171
|
+
const home = resolveRuntimeStateControllerHome(opts.controllerHome);
|
|
119
172
|
const runtime = observeRuntimeStatus(home);
|
|
120
173
|
const repositories = listRepositories(home, { includeRemoved: true });
|
|
121
174
|
const value = {
|
|
@@ -138,7 +191,7 @@ export function buildRuntimeCommand(): Command {
|
|
|
138
191
|
.argument('<job-id>', 'Execution Job ID')
|
|
139
192
|
.option('--controller-home <path>', 'Controller state root')
|
|
140
193
|
.action((jobId: string, opts: { controllerHome?: string }) => {
|
|
141
|
-
const home =
|
|
194
|
+
const home = resolveRuntimeStateControllerHome(opts.controllerHome);
|
|
142
195
|
const job = findExecutionJob(home, jobId);
|
|
143
196
|
if (!job) throw new Error(`JOB_NOT_FOUND: ${jobId}`);
|
|
144
197
|
output({ job, events: readJobEvents(home, job.repoId, job.jobId) });
|
|
@@ -150,7 +203,7 @@ export function buildRuntimeCommand(): Command {
|
|
|
150
203
|
.option('--repo-id <id>', 'Repository id')
|
|
151
204
|
.option('--limit <count>', 'Maximum records', '100')
|
|
152
205
|
.action((opts: { controllerHome?: string; repoId?: string; limit?: string }) => {
|
|
153
|
-
const home =
|
|
206
|
+
const home = resolveRuntimeStateControllerHome(opts.controllerHome);
|
|
154
207
|
if (opts.repoId) return output({ jobs: listExecutionJobs(home, opts.repoId, Number(opts.limit ?? 100)) });
|
|
155
208
|
output({ jobs: listActiveExecutionJobs(home) });
|
|
156
209
|
});
|
|
@@ -183,7 +236,7 @@ export function buildRuntimeCommand(): Command {
|
|
|
183
236
|
.requiredOption('--repo-id <id>', 'Repository id')
|
|
184
237
|
.option('--controller-home <path>', 'Controller state root')
|
|
185
238
|
.action((opts: { controllerHome?: string; repoId: string }) => {
|
|
186
|
-
const home =
|
|
239
|
+
const home = resolveRuntimeStateControllerHome(opts.controllerHome);
|
|
187
240
|
output({ schedules: listSchedules(home, opts.repoId), occurrences: listOccurrences(home, opts.repoId, undefined, 100) });
|
|
188
241
|
});
|
|
189
242
|
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { loadMcpServiceLocalConfig } from '../mcp/auth';
|
|
5
|
+
import { resolveControllerHome } from '../repositories/controller-home';
|
|
6
|
+
import { observeRuntimeStatus } from '../../runtime/root/status';
|
|
7
|
+
import { dirname, join, resolve } from 'path';
|
|
8
|
+
|
|
9
|
+
export type SetupControllerKind = 'chatgpt' | 'codex' | 'claude' | 'mcp';
|
|
10
|
+
export type SetupTunnelProvider = 'auto' | 'openai' | 'cloudflare' | 'tailscale' | 'existing' | 'none';
|
|
11
|
+
export type SetupHostTarget = 'none' | 'codex' | 'claude' | 'both';
|
|
12
|
+
|
|
13
|
+
export interface SetupProfile {
|
|
14
|
+
schemaVersion: 1;
|
|
15
|
+
primaryController: SetupControllerKind;
|
|
16
|
+
controllers: SetupControllerKind[];
|
|
17
|
+
tunnel: {
|
|
18
|
+
provider: SetupTunnelProvider;
|
|
19
|
+
endpoint?: string;
|
|
20
|
+
tunnelId?: string;
|
|
21
|
+
};
|
|
22
|
+
createdAt: string;
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SetupPlatformSnapshot {
|
|
27
|
+
platform: NodeJS.Platform;
|
|
28
|
+
arch: string;
|
|
29
|
+
environment: 'macos' | 'linux' | 'wsl2' | 'windows' | 'other';
|
|
30
|
+
serviceManager: 'launchd' | 'systemd-user' | 'windows-preview' | 'foreground';
|
|
31
|
+
commands: {
|
|
32
|
+
brew: boolean;
|
|
33
|
+
cloudflared: boolean;
|
|
34
|
+
tailscale: boolean;
|
|
35
|
+
tunnelClient: boolean;
|
|
36
|
+
systemctl: boolean;
|
|
37
|
+
winget: boolean;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SetupProfileOptions {
|
|
42
|
+
setupRoot?: string;
|
|
43
|
+
env?: NodeJS.ProcessEnv;
|
|
44
|
+
now?: () => Date;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ConfigureSetupProfileOptions extends SetupProfileOptions {
|
|
48
|
+
controller?: string;
|
|
49
|
+
addControllers?: string[];
|
|
50
|
+
tunnel?: string;
|
|
51
|
+
endpoint?: string;
|
|
52
|
+
tunnelId?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface TunnelGuidance {
|
|
56
|
+
provider: SetupTunnelProvider;
|
|
57
|
+
ready: boolean;
|
|
58
|
+
title: string;
|
|
59
|
+
detail: string;
|
|
60
|
+
command?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const CONTROLLERS: readonly SetupControllerKind[] = ['chatgpt', 'codex', 'claude', 'mcp'];
|
|
64
|
+
const TUNNELS: readonly SetupTunnelProvider[] = ['auto', 'openai', 'cloudflare', 'tailscale', 'existing', 'none'];
|
|
65
|
+
|
|
66
|
+
function setupHome(options: SetupProfileOptions = {}): string {
|
|
67
|
+
const env = options.env ?? process.env;
|
|
68
|
+
return resolve(options.setupRoot ?? env.FORGE_SETUP_HOME ?? join(env.HOME ?? homedir(), '.forge'));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function setupProfilePath(options: SetupProfileOptions = {}): string {
|
|
72
|
+
return join(setupHome(options), 'setup', 'profile.json');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeController(value: string | undefined, label: string): SetupControllerKind | undefined {
|
|
76
|
+
if (value === undefined) return undefined;
|
|
77
|
+
const normalized = value.trim().toLowerCase();
|
|
78
|
+
if (!CONTROLLERS.includes(normalized as SetupControllerKind)) {
|
|
79
|
+
throw new Error(`${label}: invalid controller "${value}" (expected: ${CONTROLLERS.join(', ')})`);
|
|
80
|
+
}
|
|
81
|
+
return normalized as SetupControllerKind;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeTunnel(value: string | undefined): SetupTunnelProvider | undefined {
|
|
85
|
+
if (value === undefined) return undefined;
|
|
86
|
+
const normalized = value.trim().toLowerCase();
|
|
87
|
+
if (!TUNNELS.includes(normalized as SetupTunnelProvider)) {
|
|
88
|
+
throw new Error(`invalid tunnel provider "${value}" (expected: ${TUNNELS.join(', ')})`);
|
|
89
|
+
}
|
|
90
|
+
return normalized as SetupTunnelProvider;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeTunnelId(value: string | undefined): string | undefined {
|
|
94
|
+
const trimmed = value?.trim();
|
|
95
|
+
if (!trimmed) return undefined;
|
|
96
|
+
if (!/^tunnel_[0-9a-f]{32}$/.test(trimmed)) throw new Error('invalid OpenAI tunnel id: expected tunnel_ followed by 32 lowercase hexadecimal characters');
|
|
97
|
+
return trimmed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeEndpoint(value: string | undefined): string | undefined {
|
|
101
|
+
const trimmed = value?.trim();
|
|
102
|
+
if (!trimmed) return undefined;
|
|
103
|
+
let parsed: URL;
|
|
104
|
+
try { parsed = new URL(trimmed); } catch { throw new Error(`invalid setup endpoint "${value}": expected HTTPS URL ending in /mcp`); }
|
|
105
|
+
if (parsed.protocol !== 'https:' || parsed.pathname !== '/mcp' || parsed.search || parsed.hash || parsed.username || parsed.password) {
|
|
106
|
+
throw new Error(`invalid setup endpoint "${value}": expected HTTPS URL ending in /mcp`);
|
|
107
|
+
}
|
|
108
|
+
return parsed.toString();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function readSetupProfile(options: SetupProfileOptions = {}): SetupProfile | undefined {
|
|
112
|
+
try {
|
|
113
|
+
const value = JSON.parse(readFileSync(setupProfilePath(options), 'utf8')) as SetupProfile;
|
|
114
|
+
if (value.schemaVersion !== 1 || !CONTROLLERS.includes(value.primaryController)) return undefined;
|
|
115
|
+
const controllers = Array.isArray(value.controllers)
|
|
116
|
+
? value.controllers.filter((entry): entry is SetupControllerKind => CONTROLLERS.includes(entry))
|
|
117
|
+
: [];
|
|
118
|
+
return { ...value, controllers: Array.from(new Set([value.primaryController, ...controllers])) };
|
|
119
|
+
} catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function configureSetupProfile(options: ConfigureSetupProfileOptions): SetupProfile {
|
|
125
|
+
const previous = readSetupProfile(options);
|
|
126
|
+
const primaryController = normalizeController(options.controller, 'forge setup configure --controller')
|
|
127
|
+
?? previous?.primaryController
|
|
128
|
+
?? 'chatgpt';
|
|
129
|
+
const additions = (options.addControllers ?? []).map((entry) => normalizeController(entry, 'forge setup configure --add-controller')!);
|
|
130
|
+
const controllers = Array.from(new Set([primaryController, ...(previous?.controllers ?? []), ...additions]));
|
|
131
|
+
const provider = normalizeTunnel(options.tunnel)
|
|
132
|
+
?? previous?.tunnel.provider
|
|
133
|
+
?? (controllers.some((entry) => entry === 'chatgpt' || entry === 'mcp') ? 'auto' : 'none');
|
|
134
|
+
const explicitTunnel = normalizeTunnel(options.tunnel);
|
|
135
|
+
const endpoint = provider === 'openai' || provider === 'none'
|
|
136
|
+
? undefined
|
|
137
|
+
: normalizeEndpoint(options.endpoint) ?? (previous?.tunnel.provider === provider ? previous.tunnel.endpoint : undefined);
|
|
138
|
+
const tunnelId = provider === 'openai'
|
|
139
|
+
? normalizeTunnelId(options.tunnelId) ?? (previous?.tunnel.provider === 'openai' ? previous.tunnel.tunnelId : undefined)
|
|
140
|
+
: undefined;
|
|
141
|
+
if (provider === 'existing' && !endpoint) {
|
|
142
|
+
throw new Error('forge setup configure --tunnel existing requires --endpoint https://.../mcp');
|
|
143
|
+
}
|
|
144
|
+
const now = (options.now ?? (() => new Date()))().toISOString();
|
|
145
|
+
const profile: SetupProfile = {
|
|
146
|
+
schemaVersion: 1,
|
|
147
|
+
primaryController,
|
|
148
|
+
controllers,
|
|
149
|
+
tunnel: { provider, ...(endpoint ? { endpoint } : {}), ...(tunnelId ? { tunnelId } : {}) },
|
|
150
|
+
createdAt: previous?.createdAt ?? now,
|
|
151
|
+
updatedAt: now,
|
|
152
|
+
};
|
|
153
|
+
const path = setupProfilePath(options);
|
|
154
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
155
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
156
|
+
writeFileSync(temporary, `${JSON.stringify(profile, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
157
|
+
renameSync(temporary, path);
|
|
158
|
+
return profile;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function setupHostTarget(profile: SetupProfile | undefined): SetupHostTarget {
|
|
162
|
+
if (!profile) return 'none';
|
|
163
|
+
const codex = profile.controllers.includes('codex');
|
|
164
|
+
const claude = profile.controllers.includes('claude');
|
|
165
|
+
if (codex && claude) return 'both';
|
|
166
|
+
if (codex) return 'codex';
|
|
167
|
+
if (claude) return 'claude';
|
|
168
|
+
return 'none';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function setupNeedsRemoteAccess(profile: SetupProfile | undefined): boolean {
|
|
172
|
+
return Boolean(profile?.controllers.some((entry) => entry === 'chatgpt' || entry === 'mcp'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function commandExists(command: string, platform = process.platform): boolean {
|
|
176
|
+
const probe = platform === 'win32'
|
|
177
|
+
? spawnSync('where.exe', [command], { stdio: 'ignore' })
|
|
178
|
+
: spawnSync('sh', ['-lc', `command -v ${command}`], { stdio: 'ignore' });
|
|
179
|
+
return probe.status === 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isWsl(env: NodeJS.ProcessEnv): boolean {
|
|
183
|
+
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) return true;
|
|
184
|
+
try { return /microsoft/i.test(readFileSync('/proc/version', 'utf8')); } catch { return false; }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function detectSetupPlatform(options: { platform?: NodeJS.Platform; arch?: string; env?: NodeJS.ProcessEnv } = {}): SetupPlatformSnapshot {
|
|
188
|
+
const platform = options.platform ?? process.platform;
|
|
189
|
+
const env = options.env ?? process.env;
|
|
190
|
+
const wsl = platform === 'linux' && isWsl(env);
|
|
191
|
+
const systemctl = platform === 'linux' && commandExists('systemctl', platform);
|
|
192
|
+
return {
|
|
193
|
+
platform,
|
|
194
|
+
arch: options.arch ?? process.arch,
|
|
195
|
+
environment: platform === 'darwin' ? 'macos' : platform === 'linux' ? (wsl ? 'wsl2' : 'linux') : platform === 'win32' ? 'windows' : 'other',
|
|
196
|
+
serviceManager: platform === 'darwin' ? 'launchd' : platform === 'linux' && systemctl ? 'systemd-user' : platform === 'win32' ? 'windows-preview' : 'foreground',
|
|
197
|
+
commands: {
|
|
198
|
+
brew: platform === 'darwin' && commandExists('brew', platform),
|
|
199
|
+
cloudflared: commandExists('cloudflared', platform),
|
|
200
|
+
tailscale: commandExists('tailscale', platform),
|
|
201
|
+
tunnelClient: commandExists('tunnel-client', platform),
|
|
202
|
+
systemctl,
|
|
203
|
+
winget: platform === 'win32' && commandExists('winget', platform),
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
function isLoopbackMcpEndpoint(value: string | undefined): boolean {
|
|
210
|
+
if (!value) return false;
|
|
211
|
+
try {
|
|
212
|
+
const parsed = new URL(value);
|
|
213
|
+
return parsed.protocol === 'http:'
|
|
214
|
+
&& (parsed.hostname === '127.0.0.1' || parsed.hostname === 'localhost' || parsed.hostname === '::1')
|
|
215
|
+
&& parsed.pathname === '/mcp'
|
|
216
|
+
&& !parsed.search
|
|
217
|
+
&& !parsed.hash;
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface ControllerGuidance {
|
|
224
|
+
controller: SetupControllerKind;
|
|
225
|
+
ready: boolean;
|
|
226
|
+
title: string;
|
|
227
|
+
detail: string;
|
|
228
|
+
command?: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function codexUserControllerConfigured(home = homedir()): boolean {
|
|
232
|
+
const path = join(home, '.codex', 'config.toml');
|
|
233
|
+
if (!existsSync(path)) return false;
|
|
234
|
+
try {
|
|
235
|
+
const text = readFileSync(path, 'utf8');
|
|
236
|
+
const block = text.match(/\[mcp_servers\.forge\][\s\S]*?(?=\n\[|$)/)?.[0] ?? '';
|
|
237
|
+
return block.includes('"mcp"') && block.includes('"serve"') && block.includes('"controller"');
|
|
238
|
+
} catch { return false; }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function claudeUserControllerConfigured(): boolean {
|
|
242
|
+
if (!commandExists('claude')) return false;
|
|
243
|
+
const probe = spawnSync('claude', ['mcp', 'get', 'forge'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000 });
|
|
244
|
+
if (probe.status !== 0) return false;
|
|
245
|
+
const text = `${probe.stdout ?? ''}\n${probe.stderr ?? ''}`;
|
|
246
|
+
return /forge\s+mcp\s+serve|"mcp".*"serve"/is.test(text) && /controller/i.test(text);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function resolveControllerGuidance(
|
|
250
|
+
profile: SetupProfile | undefined,
|
|
251
|
+
options: { controllerHome?: string; home?: string } = {},
|
|
252
|
+
): ControllerGuidance | undefined {
|
|
253
|
+
if (!profile) return undefined;
|
|
254
|
+
const controllerHome = resolveControllerHome(options.controllerHome);
|
|
255
|
+
const localConfig = loadMcpServiceLocalConfig(controllerHome);
|
|
256
|
+
for (const controller of profile.controllers) {
|
|
257
|
+
if (controller === 'chatgpt' || controller === 'mcp') {
|
|
258
|
+
const localReady = localConfig?.profile === 'controller'
|
|
259
|
+
&& localConfig.server?.host === '127.0.0.1'
|
|
260
|
+
&& Number.isInteger(localConfig.server?.port);
|
|
261
|
+
const connectorReady = controller !== 'chatgpt' || isLoopbackMcpEndpoint(localConfig?.chatgpt?.localEndpoint);
|
|
262
|
+
const endpointReady = !profile.tunnel.endpoint || localConfig?.chatgpt?.endpoint === profile.tunnel.endpoint;
|
|
263
|
+
if (!localReady || !connectorReady || !endpointReady) {
|
|
264
|
+
const endpoint = profile.tunnel.endpoint ? ` --endpoint ${profile.tunnel.endpoint}` : '';
|
|
265
|
+
return {
|
|
266
|
+
controller,
|
|
267
|
+
ready: false,
|
|
268
|
+
title: controller === 'chatgpt' ? 'Configure the ChatGPT controller endpoint' : 'Configure the remote MCP controller endpoint',
|
|
269
|
+
detail: 'Create the user-level Forge OAuth/MCP configuration. This does not install Codex or Claude and does not require a repository.',
|
|
270
|
+
command: `forge mcp setup chatgpt --user-level${endpoint}`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (controller === 'codex') {
|
|
276
|
+
if (!codexUserControllerConfigured(options.home)) {
|
|
277
|
+
return {
|
|
278
|
+
controller, ready: false, title: 'Configure Codex as a Forge controller',
|
|
279
|
+
detail: 'Codex is optional. Configure it only because it was explicitly selected as a controller entry.',
|
|
280
|
+
command: 'forge mcp setup codex --scope user --profile controller',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (!commandExists('claude')) {
|
|
286
|
+
return {
|
|
287
|
+
controller, ready: false, title: 'Install Claude Code only for this selected controller',
|
|
288
|
+
detail: 'Claude is not a Forge dependency. Install and authenticate Claude Code, then continue setup.',
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (!claudeUserControllerConfigured()) {
|
|
292
|
+
return {
|
|
293
|
+
controller, ready: false, title: 'Configure Claude Code as a Forge controller',
|
|
294
|
+
detail: 'Forge delegates MCP registration to the official Claude CLI at user scope.',
|
|
295
|
+
command: 'forge mcp setup claude --scope user --profile controller',
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
export interface RuntimeGuidance {
|
|
304
|
+
ready: boolean;
|
|
305
|
+
title: string;
|
|
306
|
+
detail: string;
|
|
307
|
+
command?: string;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function resolveRuntimeGuidance(
|
|
311
|
+
profile: SetupProfile | undefined,
|
|
312
|
+
options: { controllerHome?: string } = {},
|
|
313
|
+
): RuntimeGuidance | undefined {
|
|
314
|
+
if (!profile || !setupNeedsRemoteAccess(profile)) return undefined;
|
|
315
|
+
const controllerHome = resolveControllerHome(options.controllerHome);
|
|
316
|
+
const status = observeRuntimeStatus(controllerHome);
|
|
317
|
+
if (status.ready) return { ready: true, title: 'Forge Runtime', detail: 'Canonical Runtime is running and ready.' };
|
|
318
|
+
if (status.running) {
|
|
319
|
+
return {
|
|
320
|
+
ready: false,
|
|
321
|
+
title: 'Forge Runtime is starting or unhealthy',
|
|
322
|
+
detail: `Runtime owner is running but not ready (${status.reasonCodes.join(', ') || 'unknown'}). Inspect status before continuing.`,
|
|
323
|
+
command: `forge runtime status --controller-home ${controllerHome}`,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
ready: false,
|
|
328
|
+
title: 'Install the user-level Forge Runtime',
|
|
329
|
+
detail: 'Use the packaged Runtime path. It does not require a Git checkout, Bun compilation, CodeGraph, or Standalone Recovery.',
|
|
330
|
+
command: 'forge runtime service install-package',
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function resolveTunnelGuidance(
|
|
335
|
+
profile: SetupProfile | undefined,
|
|
336
|
+
platform = detectSetupPlatform(),
|
|
337
|
+
options: { controllerHome?: string; env?: NodeJS.ProcessEnv } = {},
|
|
338
|
+
): TunnelGuidance {
|
|
339
|
+
if (!profile || !setupNeedsRemoteAccess(profile)) {
|
|
340
|
+
return { provider: 'none', ready: true, title: 'Remote controller connection', detail: 'Not required by the configured local controller set.' };
|
|
341
|
+
}
|
|
342
|
+
if (profile.tunnel.endpoint) {
|
|
343
|
+
return { provider: profile.tunnel.provider, ready: true, title: 'HTTPS endpoint configured', detail: `Configured: ${profile.tunnel.endpoint}. Live connector reachability is verified in the ChatGPT connection step.` };
|
|
344
|
+
}
|
|
345
|
+
let provider = profile.tunnel.provider;
|
|
346
|
+
if (provider === 'auto') {
|
|
347
|
+
// ChatGPT's private outbound path is the default. Do not silently choose a
|
|
348
|
+
// public ingress provider merely because cloudflared/tailscale happens to
|
|
349
|
+
// be installed; users without Secure Tunnel access can explicitly select a
|
|
350
|
+
// fallback provider.
|
|
351
|
+
provider = 'openai';
|
|
352
|
+
}
|
|
353
|
+
if (provider === 'openai') {
|
|
354
|
+
if (!profile.tunnel.tunnelId) {
|
|
355
|
+
return {
|
|
356
|
+
provider, ready: false, title: 'Create an OpenAI Secure MCP Tunnel',
|
|
357
|
+
detail: 'Create a tunnel in OpenAI Platform Tunnels, associate it with the target ChatGPT workspace, then record the non-secret tunnel_id in Forge. Do not paste a runtime API key into Forge setup.',
|
|
358
|
+
command: 'forge setup configure --controller chatgpt --tunnel openai --tunnel-id tunnel_...',
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (!platform.commands.tunnelClient) {
|
|
362
|
+
return {
|
|
363
|
+
provider, ready: false, title: 'Install OpenAI tunnel-client',
|
|
364
|
+
detail: 'Install the supported tunnel-client binary from OpenAI Platform Tunnels or the official openai/tunnel-client release. The runtime API key remains an environment/file reference owned by tunnel-client; Forge does not store it.',
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const preferredAlias = 'forge';
|
|
368
|
+
const aliases = [preferredAlias];
|
|
369
|
+
const listed = spawnSync('tunnel-client', ['runtimes', 'list', '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15_000, env: options.env ?? process.env });
|
|
370
|
+
if (listed.status === 0) {
|
|
371
|
+
try {
|
|
372
|
+
const value = JSON.parse(listed.stdout || '{}') as { aliases?: Array<{ alias?: string; tunnel_id?: string }> };
|
|
373
|
+
for (const entry of value.aliases ?? []) {
|
|
374
|
+
if (entry.tunnel_id === profile.tunnel.tunnelId && entry.alias) aliases.push(entry.alias);
|
|
375
|
+
}
|
|
376
|
+
} catch {
|
|
377
|
+
// Older clients may not expose local aliases as JSON. Keep the preferred alias fallback.
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (const alias of Array.from(new Set(aliases))) {
|
|
381
|
+
const status = spawnSync('tunnel-client', ['runtimes', 'status', alias, '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15_000, env: options.env ?? process.env });
|
|
382
|
+
if (status.status !== 0) continue;
|
|
383
|
+
try {
|
|
384
|
+
const value = JSON.parse(status.stdout || '{}') as { process_running?: boolean; healthy?: boolean; ready?: boolean; tunnel_id?: string };
|
|
385
|
+
if (value.process_running === true && value.healthy === true && value.ready === true
|
|
386
|
+
&& (!value.tunnel_id || value.tunnel_id === profile.tunnel.tunnelId)) {
|
|
387
|
+
return {
|
|
388
|
+
provider, ready: true, title: 'OpenAI Secure MCP Tunnel',
|
|
389
|
+
detail: `Managed runtime ${alias} is running, healthy, and ready for ${profile.tunnel.tunnelId}.`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
// Treat malformed or older status output as not-ready instead of claiming success.
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const alias = preferredAlias;
|
|
397
|
+
const localConfig = loadMcpServiceLocalConfig(resolveControllerHome(options.controllerHome));
|
|
398
|
+
const localEndpoint = localConfig?.chatgpt?.localEndpoint;
|
|
399
|
+
if (!isLoopbackMcpEndpoint(localEndpoint)) {
|
|
400
|
+
return {
|
|
401
|
+
provider, ready: false, title: 'Prepare the local ChatGPT OAuth endpoint',
|
|
402
|
+
detail: 'Secure Tunnel must terminate at Forge’s loopback OAuth Gateway, not the bearer-only Canonical Runtime. Refresh the user-level ChatGPT MCP configuration first.',
|
|
403
|
+
command: 'forge mcp setup chatgpt --user-level',
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
provider, ready: false, title: 'Connect OpenAI Secure MCP Tunnel',
|
|
408
|
+
detail: 'Create a supervised tunnel-client runtime and verify it with runtimes status. Keep the runtime API key in an environment or file reference owned by tunnel-client; Forge never reads or stores the key.',
|
|
409
|
+
command: `tunnel-client runtimes connect --alias ${alias} --tunnel-id ${profile.tunnel.tunnelId} --runtime-api-key env:CONTROL_PLANE_API_KEY --mcp-server-url ${localEndpoint}`,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
if (provider === 'none') {
|
|
413
|
+
return {
|
|
414
|
+
provider, ready: false, title: 'Remote controller access is deferred',
|
|
415
|
+
detail: 'The selected controller set includes ChatGPT/remote MCP, but remote connectivity is deferred.',
|
|
416
|
+
command: 'forge setup configure --controller chatgpt --tunnel auto',
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (provider === 'existing') {
|
|
420
|
+
return {
|
|
421
|
+
provider, ready: false, title: 'Record existing HTTPS endpoint',
|
|
422
|
+
detail: 'Provide the existing public HTTPS URL ending in /mcp.',
|
|
423
|
+
command: 'forge setup configure --tunnel existing --endpoint https://forge.example.com/mcp',
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (provider === 'cloudflare') {
|
|
427
|
+
if (!platform.commands.cloudflared) {
|
|
428
|
+
const install = platform.environment === 'macos' && platform.commands.brew ? 'brew install cloudflared' : undefined;
|
|
429
|
+
return {
|
|
430
|
+
provider, ready: false, title: 'Install Cloudflare Tunnel',
|
|
431
|
+
detail: platform.environment === 'windows'
|
|
432
|
+
? 'Install the official cloudflared executable or MSI, then run forge setup next.'
|
|
433
|
+
: platform.environment === 'linux' || platform.environment === 'wsl2'
|
|
434
|
+
? 'Install cloudflared from Cloudflare’s official package repository for this Linux distribution, then run forge setup next.'
|
|
435
|
+
: 'Install cloudflared, then run forge setup next.',
|
|
436
|
+
...(install ? { command: install } : {}),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
provider, ready: false, title: 'Create a stable Cloudflare Tunnel',
|
|
441
|
+
detail: 'Authenticate cloudflared, create/route a named tunnel to 127.0.0.1:8765, then record its HTTPS /mcp URL.',
|
|
442
|
+
command: 'cloudflared tunnel login',
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (!platform.commands.tailscale) {
|
|
446
|
+
return {
|
|
447
|
+
provider: 'tailscale', ready: false, title: 'Install Tailscale',
|
|
448
|
+
detail: platform.environment === 'macos'
|
|
449
|
+
? 'Install Tailscale for macOS and enable CLI integration, then run forge setup next.'
|
|
450
|
+
: platform.environment === 'windows'
|
|
451
|
+
? 'Install the official Tailscale Windows client, sign in, then run forge setup next.'
|
|
452
|
+
: 'Install Tailscale for Linux, sign in, then run forge setup next.',
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
provider: 'tailscale', ready: false, title: 'Enable Tailscale Funnel',
|
|
457
|
+
detail: 'Expose only the local Forge MCP service and then record the generated HTTPS /mcp URL.',
|
|
458
|
+
command: 'tailscale funnel --bg 8765',
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function formatSetupProfile(profile: SetupProfile, platform = detectSetupPlatform()): string {
|
|
463
|
+
const tunnel = resolveTunnelGuidance(profile, platform);
|
|
464
|
+
return [
|
|
465
|
+
`Primary controller: ${profile.primaryController}`,
|
|
466
|
+
`Configured controllers: ${profile.controllers.join(', ')}`,
|
|
467
|
+
`Platform: ${platform.environment}/${platform.arch}; service=${platform.serviceManager}`,
|
|
468
|
+
`Remote access: ${profile.tunnel.provider}${profile.tunnel.endpoint ? ` (${profile.tunnel.endpoint})` : profile.tunnel.tunnelId ? ` (${profile.tunnel.tunnelId})` : ''}`,
|
|
469
|
+
`Tunnel readiness: ${tunnel.ready ? 'ready' : 'needs setup'} — ${tunnel.detail}`,
|
|
470
|
+
'Forge itself remains the execution/runtime layer; it does not choose semantic actions without an external controller.',
|
|
471
|
+
].join('\n');
|
|
472
|
+
}
|
|
@@ -21,6 +21,8 @@ export interface ControllerCheckEffects {
|
|
|
21
21
|
temp?: 'isolated' | 'shared';
|
|
22
22
|
git?: 'read' | 'index' | 'refs' | 'write';
|
|
23
23
|
network?: 'read' | 'write';
|
|
24
|
+
/** Host-wide coordination resources used by checks that exercise shared local services/devices. */
|
|
25
|
+
hostServices?: string[];
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export interface ControllerCheck {
|
|
@@ -131,7 +133,7 @@ function normalizeCwd(repoRoot: string, value: string | undefined): string {
|
|
|
131
133
|
return back || '.';
|
|
132
134
|
}
|
|
133
135
|
|
|
134
|
-
const CHECK_EFFECT_KEYS = new Set(['reads', 'writes', 'cache', 'temp', 'git', 'network']);
|
|
136
|
+
const CHECK_EFFECT_KEYS = new Set(['reads', 'writes', 'cache', 'temp', 'git', 'network', 'hostServices']);
|
|
135
137
|
|
|
136
138
|
function normalizeEffectPaths(repoRoot: string, field: 'reads' | 'writes', value: unknown): string[] | undefined {
|
|
137
139
|
if (value === undefined) return undefined;
|
|
@@ -141,6 +143,16 @@ function normalizeEffectPaths(repoRoot: string, field: 'reads' | 'writes', value
|
|
|
141
143
|
return [...new Set(value.map((entry) => normalizeCwd(repoRoot, String(entry))))].sort();
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
function normalizeHostServices(value: unknown): string[] | undefined {
|
|
147
|
+
if (value === undefined) return undefined;
|
|
148
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string' || entry.trim().length === 0)) {
|
|
149
|
+
throw new Error('check effects.hostServices must be an array of non-empty strings');
|
|
150
|
+
}
|
|
151
|
+
const normalized = value.map((entry) => String(entry).trim().toLowerCase().replace(/[^a-z0-9._:-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 120));
|
|
152
|
+
if (normalized.some((entry) => !entry)) throw new Error('check effects.hostServices contains an invalid service key');
|
|
153
|
+
return [...new Set(normalized)].sort();
|
|
154
|
+
}
|
|
155
|
+
|
|
144
156
|
function normalizeEffectMode<T extends string>(field: string, value: unknown, allowed: readonly T[]): T | undefined {
|
|
145
157
|
if (value === undefined) return undefined;
|
|
146
158
|
if (typeof value !== 'string' || !allowed.includes(value as T)) {
|
|
@@ -164,6 +176,7 @@ function normalizeCheckEffects(repoRoot: string, value: unknown): ControllerChec
|
|
|
164
176
|
...(raw.temp !== undefined ? { temp: normalizeEffectMode('temp', raw.temp, ['isolated', 'shared'] as const)! } : {}),
|
|
165
177
|
...(raw.git !== undefined ? { git: normalizeEffectMode('git', raw.git, ['read', 'index', 'refs', 'write'] as const)! } : {}),
|
|
166
178
|
...(raw.network !== undefined ? { network: normalizeEffectMode('network', raw.network, ['read', 'write'] as const)! } : {}),
|
|
179
|
+
...(raw.hostServices !== undefined ? { hostServices: normalizeHostServices(raw.hostServices)! } : {}),
|
|
167
180
|
};
|
|
168
181
|
}
|
|
169
182
|
|
|
@@ -350,7 +363,7 @@ function validateControllerCheckSnapshot(repoRoot: string, snapshot: ControllerC
|
|
|
350
363
|
};
|
|
351
364
|
}
|
|
352
365
|
|
|
353
|
-
const CONTROLLER_CHECK_EXECUTION_IDENTITY_VERSION = 'controller-check-execution-
|
|
366
|
+
const CONTROLLER_CHECK_EXECUTION_IDENTITY_VERSION = 'controller-check-execution-v2';
|
|
354
367
|
|
|
355
368
|
function checkEnvironmentFingerprint(check: ControllerCheck): string {
|
|
356
369
|
const env = repositoryChildProcessEnvironment(process.env);
|
|
@@ -360,7 +373,10 @@ function checkEnvironmentFingerprint(check: ControllerCheck): string {
|
|
|
360
373
|
arch: process.arch,
|
|
361
374
|
node: process.version,
|
|
362
375
|
bun: typeof Bun !== 'undefined' ? Bun.version : undefined,
|
|
363
|
-
|
|
376
|
+
// Fingerprint the executable that the check actually runs, not the
|
|
377
|
+
// Controller launcher binary. Compiled Runtime uses active-forge-runtime
|
|
378
|
+
// for the parent and forge-check-runner for the sidecar; process.execPath
|
|
379
|
+
// therefore differs even though both execute the exact same check/toolchain.
|
|
364
380
|
commandExecutable: check.command[0],
|
|
365
381
|
path: env.PATH ?? '',
|
|
366
382
|
home: env.HOME ?? env.USERPROFILE ?? '',
|
|
@@ -396,6 +412,7 @@ function checkEffectsAllowCrossCheckoutReuse(check: ControllerCheck): boolean {
|
|
|
396
412
|
if ((effects.writes?.length ?? 0) > 0) return false;
|
|
397
413
|
if (effects.git !== undefined && effects.git !== 'read') return false;
|
|
398
414
|
if (effects.network !== undefined) return false;
|
|
415
|
+
if ((effects.hostServices?.length ?? 0) > 0) return false;
|
|
399
416
|
if (effects.temp === 'shared') return false;
|
|
400
417
|
return true;
|
|
401
418
|
}
|