@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,219 +1 @@
|
|
|
1
|
-
|
|
2
|
-
getWorkContract,
|
|
3
|
-
transitionWorkContractPhase,
|
|
4
|
-
updateWorkContract,
|
|
5
|
-
} from '../../control-plane/facade/work-contract-store';
|
|
6
|
-
import {
|
|
7
|
-
transitionWorkHandle,
|
|
8
|
-
type WorkHandleState,
|
|
9
|
-
} from '../../control-plane/execution/work-handle-store';
|
|
10
|
-
import { processCheckCompletionReceipt } from '../../execution/process-runtime/check-receipt';
|
|
11
|
-
import { processCheckSemanticScopeKey } from '../../execution/process-runtime/check-facade';
|
|
12
|
-
import { getProcessRecord } from '../../execution/process-runtime/store';
|
|
13
|
-
import { controllerCheckExecutionIdentity } from '../../../cli/controller/check-runner';
|
|
14
|
-
|
|
15
|
-
export type WorkValidationOutcome =
|
|
16
|
-
| 'not_validating'
|
|
17
|
-
| 'running'
|
|
18
|
-
| 'passed'
|
|
19
|
-
| 'failed'
|
|
20
|
-
| 'infrastructure_failure';
|
|
21
|
-
|
|
22
|
-
export interface WorkValidationReconciliation {
|
|
23
|
-
handle: WorkHandleState;
|
|
24
|
-
changed: boolean;
|
|
25
|
-
outcome: WorkValidationOutcome;
|
|
26
|
-
summary?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function hasCurrentWorkValidationAuthority(input: {
|
|
30
|
-
finalizationValidation: WorkHandleState['finalization']['validation'];
|
|
31
|
-
validatedInputFingerprint?: string;
|
|
32
|
-
evidenceState: string;
|
|
33
|
-
expectedFingerprint: string;
|
|
34
|
-
}): boolean {
|
|
35
|
-
return input.finalizationValidation === 'done'
|
|
36
|
-
&& input.evidenceState === 'valid'
|
|
37
|
-
&& input.validatedInputFingerprint === input.expectedFingerprint;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* A new validation run invalidates delivery authority without deleting or
|
|
42
|
-
* rewriting the prior receipts. `stale` is the only valid transition from an
|
|
43
|
-
* already-valid evidence set; all other retryable states converge through
|
|
44
|
-
* `partial`.
|
|
45
|
-
*/
|
|
46
|
-
export function markWorkValidationPending(controllerHome: string, handle: WorkHandleState): void {
|
|
47
|
-
const contractId = handle.workContractId;
|
|
48
|
-
if (!contractId) return;
|
|
49
|
-
const options = { controllerHome, repoId: handle.repositoryId };
|
|
50
|
-
const contract = getWorkContract(options, contractId);
|
|
51
|
-
if (!contract || contract.completionReceipt) return;
|
|
52
|
-
const evidenceState = contract.evidenceState === 'valid' || contract.evidenceState === 'stale'
|
|
53
|
-
? 'stale'
|
|
54
|
-
: 'partial';
|
|
55
|
-
if (contract.evidenceState !== evidenceState) updateWorkContract(options, contractId, { evidenceState });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Project durable validation evidence into the Work-owned contract lifecycle.
|
|
60
|
-
* Process and Check records contribute evidence only; they never become a
|
|
61
|
-
* second completion or lifecycle authority.
|
|
62
|
-
*/
|
|
63
|
-
export function projectWorkValidationOutcome(
|
|
64
|
-
controllerHome: string,
|
|
65
|
-
handle: WorkHandleState,
|
|
66
|
-
outcome: Exclude<WorkValidationOutcome, 'not_validating' | 'running'>,
|
|
67
|
-
summary?: string,
|
|
68
|
-
): void {
|
|
69
|
-
const contractId = handle.workContractId;
|
|
70
|
-
if (!contractId) return;
|
|
71
|
-
const options = { controllerHome, repoId: handle.repositoryId };
|
|
72
|
-
const contract = getWorkContract(options, contractId);
|
|
73
|
-
if (!contract || contract.completionReceipt) return;
|
|
74
|
-
|
|
75
|
-
if (outcome === 'passed') {
|
|
76
|
-
transitionWorkContractPhase(options, contractId, {
|
|
77
|
-
phase: 'delivery',
|
|
78
|
-
status: 'running',
|
|
79
|
-
state: 'active',
|
|
80
|
-
summary: summary ?? 'All requested validation receipts passed; delivery is next.',
|
|
81
|
-
evidenceRefs: contract.evidenceRefs,
|
|
82
|
-
});
|
|
83
|
-
updateWorkContract(options, contractId, { evidenceState: 'valid' });
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (outcome === 'failed') {
|
|
88
|
-
transitionWorkContractPhase(options, contractId, {
|
|
89
|
-
phase: 'cleanup',
|
|
90
|
-
status: 'failed',
|
|
91
|
-
state: 'failed',
|
|
92
|
-
summary: summary ?? 'A requested validation check failed; terminal cleanup is next.',
|
|
93
|
-
evidenceRefs: contract.evidenceRefs,
|
|
94
|
-
});
|
|
95
|
-
updateWorkContract(options, contractId, { evidenceState: 'failed' });
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
transitionWorkContractPhase(options, contractId, {
|
|
100
|
-
phase: 'cleanup',
|
|
101
|
-
status: 'failed',
|
|
102
|
-
state: 'failed',
|
|
103
|
-
summary: summary ?? 'Validation infrastructure failed; terminal cleanup is required without treating this as an acceptance failure.',
|
|
104
|
-
evidenceRefs: contract.evidenceRefs,
|
|
105
|
-
});
|
|
106
|
-
updateWorkContract(options, contractId, {
|
|
107
|
-
evidenceState: contract.evidenceState === 'valid' || contract.evidenceState === 'stale' ? 'stale' : 'partial',
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function settleInfrastructureFailure(
|
|
112
|
-
controllerHome: string,
|
|
113
|
-
handle: WorkHandleState,
|
|
114
|
-
summary: string,
|
|
115
|
-
): WorkValidationReconciliation {
|
|
116
|
-
const next = transitionWorkHandle(controllerHome, handle, 'failed', {
|
|
117
|
-
finalization: { ...handle.finalization, validation: 'failed', lastError: summary },
|
|
118
|
-
validationRun: undefined,
|
|
119
|
-
failureReason: summary,
|
|
120
|
-
});
|
|
121
|
-
projectWorkValidationOutcome(controllerHome, next, 'infrastructure_failure', summary);
|
|
122
|
-
return { handle: next, changed: true, outcome: 'infrastructure_failure', summary };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Converge a persisted Work validation run from durable Process receipts.
|
|
127
|
-
*
|
|
128
|
-
* No command is launched here. Missing/running bindings stay pending; timeout,
|
|
129
|
-
* cancellation, missing process state, or malformed receipt makes the Work
|
|
130
|
-
* terminally failed for cleanup without classifying it as an acceptance failure.
|
|
131
|
-
*/
|
|
132
|
-
export function reconcileWorkValidation(
|
|
133
|
-
controllerHome: string,
|
|
134
|
-
handle: WorkHandleState,
|
|
135
|
-
): WorkValidationReconciliation {
|
|
136
|
-
const run = handle.validationRun;
|
|
137
|
-
if (handle.state !== 'validating' || !run) {
|
|
138
|
-
return { handle, changed: false, outcome: 'not_validating' };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (run.requestedChecks.length === 0) {
|
|
142
|
-
const next = transitionWorkHandle(controllerHome, handle, run.resumeState, {
|
|
143
|
-
finalization: { ...handle.finalization, validation: 'done', lastError: undefined },
|
|
144
|
-
validationRun: undefined,
|
|
145
|
-
validatedInputFingerprint: run.fingerprint,
|
|
146
|
-
failureReason: undefined,
|
|
147
|
-
});
|
|
148
|
-
projectWorkValidationOutcome(controllerHome, next, 'passed', 'No validation checks were required.');
|
|
149
|
-
return { handle: next, changed: true, outcome: 'passed' };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
for (const checkId of run.requestedChecks) {
|
|
153
|
-
const binding = run.processes[checkId];
|
|
154
|
-
if (!binding) return { handle, changed: false, outcome: 'running' };
|
|
155
|
-
const record = getProcessRecord(controllerHome, handle.repositoryId, binding.processId);
|
|
156
|
-
if (!record) {
|
|
157
|
-
return settleInfrastructureFailure(
|
|
158
|
-
controllerHome,
|
|
159
|
-
handle,
|
|
160
|
-
`Validation process record is unavailable: ${binding.processId}`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
if (record.status === 'running' || record.status === 'starting' || record.status === 'running_recovered') {
|
|
164
|
-
return { handle, changed: false, outcome: 'running' };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
const currentIdentity = record.checkExecution
|
|
169
|
-
? controllerCheckExecutionIdentity(handle.worktreePath, checkId, record.checkExecution.timeoutMs)
|
|
170
|
-
: undefined;
|
|
171
|
-
const receipt = processCheckCompletionReceipt(record, {
|
|
172
|
-
repoId: handle.repositoryId,
|
|
173
|
-
checkoutId: handle.checkoutId,
|
|
174
|
-
workId: handle.workId,
|
|
175
|
-
executionSessionId: handle.sessionId,
|
|
176
|
-
checkId,
|
|
177
|
-
processId: binding.processId,
|
|
178
|
-
...(currentIdentity ? {
|
|
179
|
-
checkExecution: {
|
|
180
|
-
cacheKey: currentIdentity.cacheKey,
|
|
181
|
-
revision: currentIdentity.revision,
|
|
182
|
-
definitionDigest: currentIdentity.definitionDigest,
|
|
183
|
-
environmentFingerprint: currentIdentity.environmentFingerprint,
|
|
184
|
-
timeoutMs: currentIdentity.timeoutMs,
|
|
185
|
-
scopeKey: processCheckSemanticScopeKey({
|
|
186
|
-
checkoutId: handle.checkoutId,
|
|
187
|
-
workId: handle.workId,
|
|
188
|
-
verificationBinding: { executionSessionId: handle.sessionId },
|
|
189
|
-
}, currentIdentity.reuseScope),
|
|
190
|
-
},
|
|
191
|
-
} : {}),
|
|
192
|
-
});
|
|
193
|
-
if (!receipt.ok) {
|
|
194
|
-
if (receipt.status === 'timed_out' || receipt.status === 'cancelled') {
|
|
195
|
-
return settleInfrastructureFailure(controllerHome, handle, receipt.summary);
|
|
196
|
-
}
|
|
197
|
-
const next = transitionWorkHandle(controllerHome, handle, 'failed', {
|
|
198
|
-
finalization: { ...handle.finalization, validation: 'failed', lastError: receipt.summary },
|
|
199
|
-
validationRun: undefined,
|
|
200
|
-
failureReason: receipt.summary,
|
|
201
|
-
});
|
|
202
|
-
projectWorkValidationOutcome(controllerHome, next, 'failed', receipt.summary);
|
|
203
|
-
return { handle: next, changed: true, outcome: 'failed', summary: receipt.summary };
|
|
204
|
-
}
|
|
205
|
-
} catch (error) {
|
|
206
|
-
const summary = error instanceof Error ? error.message : String(error);
|
|
207
|
-
return settleInfrastructureFailure(controllerHome, handle, summary);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const next = transitionWorkHandle(controllerHome, handle, run.resumeState, {
|
|
212
|
-
finalization: { ...handle.finalization, validation: 'done', lastError: undefined },
|
|
213
|
-
validationRun: undefined,
|
|
214
|
-
validatedInputFingerprint: run.fingerprint,
|
|
215
|
-
failureReason: undefined,
|
|
216
|
-
});
|
|
217
|
-
projectWorkValidationOutcome(controllerHome, next, 'passed', 'All requested validation receipts passed.');
|
|
218
|
-
return { handle: next, changed: true, outcome: 'passed' };
|
|
219
|
-
}
|
|
1
|
+
export * from '../../control-plane/execution/work-validation-reconciler';
|
|
@@ -383,6 +383,11 @@ function loadConfig(repoRoot: string): BrowserPluginConfig {
|
|
|
383
383
|
return normalizeConfig(readJson<Partial<BrowserPluginConfig>>(configPath(repoRoot)) ?? {});
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
export function readBrowserPluginConfiguration(repoRoot: string): { enabled: boolean; allowedDomains: string[] } {
|
|
387
|
+
const config = loadConfig(repoRoot);
|
|
388
|
+
return { enabled: config.enabled, allowedDomains: [...(config.allowedDomains ?? [])] };
|
|
389
|
+
}
|
|
390
|
+
|
|
386
391
|
function saveConfig(repoRoot: string, patch: Partial<BrowserPluginConfig>): BrowserPluginConfig {
|
|
387
392
|
const next = normalizeConfig({ ...loadConfig(repoRoot), ...patch });
|
|
388
393
|
writeJson(configPath(repoRoot), next);
|
|
@@ -384,7 +384,7 @@ export function createDesktopOperatorRegistrationInput(
|
|
|
384
384
|
providerPluginId: 'desktop_operator',
|
|
385
385
|
displayName: 'Forge Desktop Operator',
|
|
386
386
|
provider: 'local-macos',
|
|
387
|
-
pluginVersion: options.pluginVersion ?? '0.1
|
|
387
|
+
pluginVersion: options.pluginVersion ?? '0.2.1',
|
|
388
388
|
protocolVersion: options.protocolVersion ?? '1.0',
|
|
389
389
|
scope: 'controller',
|
|
390
390
|
enabled: options.enabled !== false,
|
|
@@ -265,13 +265,21 @@ function writeRegistry(controllerHome: string, repoId: string, manifests: Assist
|
|
|
265
265
|
return index;
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
function
|
|
268
|
+
export function claimsForAssistantPluginAction(
|
|
269
|
+
action: AssistantPluginActionDescriptor,
|
|
270
|
+
repository: RepositoryRecord,
|
|
271
|
+
trustedPluginId: string,
|
|
272
|
+
): ResourceClaimSpec[] {
|
|
269
273
|
const controllerScoped = repository.repoId === CONTROLLER_SCOPE_REPO_ID;
|
|
274
|
+
const pluginId = trustedPluginId.trim().replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
275
|
+
if (!pluginId) throw new Error('PLUGIN_ID_REQUIRED_FOR_RESOURCE_CLAIMS');
|
|
270
276
|
return action.resourceClaims.map((claim) => ({
|
|
271
|
-
resourceKey:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
277
|
+
resourceKey: claim.resource === 'remote' || claim.resource === 'provider-state'
|
|
278
|
+
// The manifest/plugin id was resolved from the trusted registration; a
|
|
279
|
+
// provider never supplies arbitrary resource-key text.
|
|
280
|
+
? `provider-state:${pluginId}`
|
|
281
|
+
: controllerScoped
|
|
282
|
+
? `controller-system:${claim.resource}`
|
|
275
283
|
: claim.resource === 'workspace'
|
|
276
284
|
? `workspace:${repository.activeCheckoutId}`
|
|
277
285
|
: claim.resource === 'git-refs'
|
|
@@ -764,7 +772,7 @@ export async function submitAssistantPluginAction(
|
|
|
764
772
|
mode: 'mutating',
|
|
765
773
|
idempotent: action.idempotent,
|
|
766
774
|
replayable: action.idempotent,
|
|
767
|
-
resourceClaims:
|
|
775
|
+
resourceClaims: claimsForAssistantPluginAction(action, repository, manifest.pluginId),
|
|
768
776
|
},
|
|
769
777
|
objective: `Execute bounded controller-local effect ${request.pluginId}/${request.actionId}`,
|
|
770
778
|
mode: 'direct_control',
|
|
@@ -4,7 +4,7 @@ export type AssistantPluginLifecycleState = 'enabled' | 'disabled' | 'degraded'
|
|
|
4
4
|
export type AssistantPluginHealthState = 'ready' | 'disabled' | 'degraded' | 'error';
|
|
5
5
|
export type AssistantPluginActionRisk = 'readonly' | 'workspace_write' | 'remote_write' | 'destructive';
|
|
6
6
|
export type AssistantPluginActionConfirmation = 'none' | 'authorization' | 'strong_confirmation';
|
|
7
|
-
export type AssistantPluginResource = 'repo-state' | 'workspace' | 'remote' | 'git-refs';
|
|
7
|
+
export type AssistantPluginResource = 'repo-state' | 'workspace' | 'remote' | 'git-refs' | 'provider-state';
|
|
8
8
|
|
|
9
9
|
export interface AssistantPluginPermissionScope {
|
|
10
10
|
scope: string;
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
type ProjectionRefreshOwner,
|
|
23
23
|
} from './invalidation';
|
|
24
24
|
import { readRepositoryGitStatusSample } from './git-status-sampler';
|
|
25
|
-
import { listCampaigns } from '../workflow/campaigns/store';
|
|
26
25
|
import { listAssistantPluginManifests } from '../plugins/store';
|
|
27
26
|
import type { ProjectionObservation, ProjectionSourceReconciliation } from '../health';
|
|
28
27
|
import { RUNTIME_HEALTH_THRESHOLDS } from '../health/evaluator';
|
|
@@ -60,12 +59,6 @@ export interface RepositoryRuntimeProjection {
|
|
|
60
59
|
degraded: number;
|
|
61
60
|
error: number;
|
|
62
61
|
};
|
|
63
|
-
campaigns?: {
|
|
64
|
-
active: number;
|
|
65
|
-
waitingForSupervisor: number;
|
|
66
|
-
pendingReviews: number;
|
|
67
|
-
readyForHumanAcceptance: number;
|
|
68
|
-
};
|
|
69
62
|
}
|
|
70
63
|
|
|
71
64
|
interface DirtyProjectionReadCacheEntry {
|
|
@@ -112,12 +105,6 @@ function emptyProjection(repoId: string, reason?: string): RepositoryRuntimeProj
|
|
|
112
105
|
degraded: 0,
|
|
113
106
|
error: 0,
|
|
114
107
|
},
|
|
115
|
-
campaigns: {
|
|
116
|
-
active: 0,
|
|
117
|
-
waitingForSupervisor: 0,
|
|
118
|
-
pendingReviews: 0,
|
|
119
|
-
readyForHumanAcceptance: 0,
|
|
120
|
-
},
|
|
121
108
|
};
|
|
122
109
|
}
|
|
123
110
|
|
|
@@ -191,7 +178,7 @@ function projectionWithExecutionIndexOverlay(
|
|
|
191
178
|
}
|
|
192
179
|
|
|
193
180
|
function dirtyReasonImpliesActiveRisk(reason: string | undefined): boolean {
|
|
194
|
-
return Boolean(reason && /^(job:|leases-|
|
|
181
|
+
return Boolean(reason && /^(job:|leases-|schedule:|worker:|process:|cleanup:)/.test(reason));
|
|
195
182
|
}
|
|
196
183
|
|
|
197
184
|
function buildRepositoryProjection(
|
|
@@ -210,8 +197,6 @@ function buildRepositoryProjection(
|
|
|
210
197
|
// Terminal attention records remain in history for diagnosis and audit, but only
|
|
211
198
|
// active/unresolved records should influence "current readiness" decisions.
|
|
212
199
|
const currentAttentionJobs = attentionJobs.filter((job) => !job.finishedAt || activeJobIds.has(job.jobId));
|
|
213
|
-
const campaigns = listCampaigns(controllerHome, repoId, 1_000);
|
|
214
|
-
const reviewableCampaigns = campaigns.filter((campaign) => ['active', 'waiting_for_supervisor'].includes(campaign.status));
|
|
215
200
|
const repository = listRepositories(controllerHome).find((entry) => entry.repoId === repoId);
|
|
216
201
|
const plugins = repository ? listAssistantPluginManifests(controllerHome, repository, {
|
|
217
202
|
preferStored: true,
|
|
@@ -251,12 +236,6 @@ function buildRepositoryProjection(
|
|
|
251
236
|
degraded: plugins.filter((plugin) => plugin.health.state === 'degraded').length,
|
|
252
237
|
error: plugins.filter((plugin) => plugin.health.state === 'error').length,
|
|
253
238
|
},
|
|
254
|
-
campaigns: {
|
|
255
|
-
active: campaigns.filter((campaign) => campaign.status === 'active').length,
|
|
256
|
-
waitingForSupervisor: campaigns.filter((campaign) => campaign.status === 'waiting_for_supervisor').length,
|
|
257
|
-
pendingReviews: reviewableCampaigns.reduce((count, campaign) => count + campaign.checkpoints.filter((checkpoint) => checkpoint.status === 'open').length, 0),
|
|
258
|
-
readyForHumanAcceptance: campaigns.filter((campaign) => campaign.status === 'ready_for_human_acceptance').length,
|
|
259
|
-
},
|
|
260
239
|
};
|
|
261
240
|
projection.metadata = {
|
|
262
241
|
...projection.metadata!,
|
|
@@ -270,7 +249,6 @@ function buildRepositoryProjection(
|
|
|
270
249
|
currentAttention: projection.currentAttention,
|
|
271
250
|
attention: projection.attention,
|
|
272
251
|
plugins: projection.plugins,
|
|
273
|
-
campaigns: projection.campaigns,
|
|
274
252
|
})).digest('hex'),
|
|
275
253
|
};
|
|
276
254
|
return projection;
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type RuntimeStorageRepairApplyResult,
|
|
20
20
|
type RuntimeStorageRepairPreview,
|
|
21
21
|
} from './local-jobs-repair';
|
|
22
|
+
import { gcTerminalProcesses, type ProcessGcResult } from '../execution/process-runtime/gc';
|
|
22
23
|
|
|
23
24
|
export type RuntimeMaintenanceActionId =
|
|
24
25
|
| 'local_jobs_reconcile'
|
|
@@ -118,6 +119,8 @@ export interface RuntimeMaintenanceApplyResult extends Omit<RuntimeMaintenanceSt
|
|
|
118
119
|
actionId: RuntimeMaintenanceActionId;
|
|
119
120
|
applied: Array<RuntimeMaintenanceCandidate & { applied: boolean; result?: string; error?: string }>;
|
|
120
121
|
runtimeStorageRepairApply?: RuntimeStorageRepairApplyResult;
|
|
122
|
+
/** Existing bounded terminal-process retention, run only during explicit full maintenance. */
|
|
123
|
+
processGc?: ProcessGcResult;
|
|
121
124
|
projection?: unknown;
|
|
122
125
|
}
|
|
123
126
|
|
|
@@ -801,6 +804,11 @@ export function applyRuntimeMaintenance(
|
|
|
801
804
|
return { ...candidate, applied: false, error: error instanceof Error ? error.message : String(error) };
|
|
802
805
|
}
|
|
803
806
|
});
|
|
807
|
+
// Reuse the existing Process GC within the explicit full-maintenance cadence.
|
|
808
|
+
// No scheduler, per-tool-call cleanup, or database compaction is introduced.
|
|
809
|
+
const processGc = options.actionId === 'full_maintenance_pass'
|
|
810
|
+
? gcTerminalProcesses({ controllerHome, repoId: repository.repoId })
|
|
811
|
+
: undefined;
|
|
804
812
|
|
|
805
813
|
if (options.actionId === 'runtime_storage_finalize_relocation' || options.actionId === 'full_maintenance_pass' || applied.some((candidate) => candidate.applied) || runtimeStorageRepairApply) {
|
|
806
814
|
rebuildActiveIndex(repository.canonicalRoot);
|
|
@@ -817,6 +825,7 @@ export function applyRuntimeMaintenance(
|
|
|
817
825
|
runtimeStorageError: after.runtimeStorageError ?? storage.error,
|
|
818
826
|
applied,
|
|
819
827
|
runtimeStorageRepairApply,
|
|
828
|
+
processGc,
|
|
820
829
|
projection,
|
|
821
830
|
};
|
|
822
831
|
}
|
|
@@ -8,6 +8,7 @@ export interface RuntimeGatewayServerOptions {
|
|
|
8
8
|
runtimeInstanceId: string;
|
|
9
9
|
runtimeSourceRoot: string;
|
|
10
10
|
sessionId?: string;
|
|
11
|
+
controllerType?: 'chatgpt' | 'codex' | 'claude' | 'grok';
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -28,6 +29,7 @@ export function createRuntimeGatewayServer(
|
|
|
28
29
|
profile: 'controller',
|
|
29
30
|
principalId,
|
|
30
31
|
sessionId: options.sessionId,
|
|
32
|
+
controllerType: options.controllerType,
|
|
31
33
|
controllerInstanceId: options.runtimeInstanceId,
|
|
32
34
|
runtimeSourceRoot: options.runtimeSourceRoot,
|
|
33
35
|
});
|
|
@@ -17,12 +17,14 @@ export interface RuntimeMcpTransportHandle {
|
|
|
17
17
|
close(): Promise<void>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export type ForwardedControllerType = 'chatgpt' | 'codex' | 'claude' | 'grok';
|
|
21
|
+
|
|
20
22
|
export interface StartRuntimeMcpTransportOptions {
|
|
21
23
|
host: string;
|
|
22
24
|
port: number;
|
|
23
25
|
authToken: string;
|
|
24
26
|
readiness: () => RuntimeReadiness;
|
|
25
|
-
createServer: (principalId: string, sessionId?: string) => Server;
|
|
27
|
+
createServer: (principalId: string, sessionId?: string, controllerType?: ForwardedControllerType) => Server;
|
|
26
28
|
onFatal?: (error: Error) => void;
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -99,8 +101,14 @@ export async function startRuntimeMcpTransport(
|
|
|
99
101
|
const forwardedSessionId = typeof req.headers['x-forge-forwarded-session-id'] === 'string'
|
|
100
102
|
? req.headers['x-forge-forwarded-session-id'].trim().slice(0, 512)
|
|
101
103
|
: '';
|
|
104
|
+
const forwardedControllerTypeRaw = typeof req.headers['x-forge-forwarded-controller-type'] === 'string'
|
|
105
|
+
? req.headers['x-forge-forwarded-controller-type'].trim().toLowerCase()
|
|
106
|
+
: '';
|
|
107
|
+
const forwardedControllerType = ['chatgpt', 'codex', 'claude', 'grok'].includes(forwardedControllerTypeRaw)
|
|
108
|
+
? forwardedControllerTypeRaw as ForwardedControllerType
|
|
109
|
+
: undefined;
|
|
102
110
|
const principal = forwardedPrincipalId || principalId(options.authToken);
|
|
103
|
-
const server = options.createServer(principal, forwardedSessionId || undefined);
|
|
111
|
+
const server = options.createServer(principal, forwardedSessionId || undefined, forwardedControllerType);
|
|
104
112
|
transport = new StreamableHTTPServerTransport({
|
|
105
113
|
sessionIdGenerator: () => randomUUID(),
|
|
106
114
|
onsessioninitialized: (createdSessionId) => {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { closeSync, mkdirSync, openSync, renameSync, writeFileSync } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { basename, dirname, join, resolve } from 'path';
|
|
5
|
+
import { spawn, spawnSync } from 'child_process';
|
|
6
|
+
import { bootstrapLaunchAgentWithRetryV2, installLaunchAgent, launchAgentPath } from '../../cli/controller/launch-agents';
|
|
7
|
+
import type { PackageRuntimeRelease } from './package-runtime-release';
|
|
8
|
+
|
|
9
|
+
export interface PackageConnectorServicePaths {
|
|
10
|
+
label: string;
|
|
11
|
+
serviceRoot: string;
|
|
12
|
+
sourcePlistPath: string;
|
|
13
|
+
installedPlistPath: string;
|
|
14
|
+
stdoutPath: string;
|
|
15
|
+
stderrPath: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PackageConnectorServiceResult {
|
|
19
|
+
endpoint: string;
|
|
20
|
+
mode: 'launchd' | 'systemd-user' | 'portable';
|
|
21
|
+
persistent: boolean;
|
|
22
|
+
servicePath?: string;
|
|
23
|
+
pid?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function xml(value: string): string {
|
|
27
|
+
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function atomicWrite(path: string, content: string, mode = 0o600): void {
|
|
31
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
32
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
33
|
+
writeFileSync(temporary, content, { encoding: 'utf8', mode });
|
|
34
|
+
renameSync(temporary, path);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function packageConnectorServicePaths(controllerHome: string, accountHome = process.env.HOME ?? homedir()): PackageConnectorServicePaths {
|
|
38
|
+
const home = resolve(controllerHome);
|
|
39
|
+
const suffix = createHash('sha256').update(home).digest('hex').slice(0, 12);
|
|
40
|
+
const label = `com.moretea.forge.mcp-gateway.${suffix}`;
|
|
41
|
+
const serviceRoot = join(home, 'runtime', 'connector-service');
|
|
42
|
+
return {
|
|
43
|
+
label,
|
|
44
|
+
serviceRoot,
|
|
45
|
+
sourcePlistPath: join(serviceRoot, `${label}.plist`),
|
|
46
|
+
installedPlistPath: launchAgentPath(label, accountHome),
|
|
47
|
+
stdoutPath: join(serviceRoot, 'logs', 'stdout.log'),
|
|
48
|
+
stderrPath: join(serviceRoot, 'logs', 'stderr.log'),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function packageConnectorLaunchSpec(input: { release: PackageRuntimeRelease; controllerHome: string; endpoint: string; executable?: string }): { executable: string; args: string[]; environment: Record<string, string>; port: number } {
|
|
53
|
+
const parsed = new URL(input.endpoint);
|
|
54
|
+
const port = Number(parsed.port);
|
|
55
|
+
if (parsed.protocol !== 'http:' || parsed.hostname !== '127.0.0.1' || parsed.pathname !== '/mcp' || !Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
56
|
+
throw new Error('FORGE_PACKAGE_CONNECTOR_ENDPOINT_INVALID');
|
|
57
|
+
}
|
|
58
|
+
const executable = resolve(input.executable ?? process.execPath);
|
|
59
|
+
const packageRoot = resolve(input.release.packageRoot);
|
|
60
|
+
const cliEntry = join(packageRoot, 'src', 'cli', 'index.ts');
|
|
61
|
+
const nodeLoader = join(packageRoot, 'src', 'runtime', 'shared', 'node-ts-loader.mjs');
|
|
62
|
+
const isBun = Boolean(process.versions.bun) || /(?:^|[/\\-])bun(?:$|[/\\]|\.exe$)/i.test(basename(executable));
|
|
63
|
+
const cliArgs = [
|
|
64
|
+
'mcp', 'serve', '--repo', packageRoot, '--controller-home', resolve(input.controllerHome),
|
|
65
|
+
'--transport', 'http', '--host', '127.0.0.1', '--port', String(port), '--profile', 'controller', '--auth', 'oauth',
|
|
66
|
+
];
|
|
67
|
+
return {
|
|
68
|
+
executable,
|
|
69
|
+
args: isBun ? [cliEntry, ...cliArgs] : ['--loader', nodeLoader, cliEntry, ...cliArgs],
|
|
70
|
+
environment: { FORGE_CONTROLLER_HOME: resolve(input.controllerHome), FORGE_CONTROLLER_LIFECYCLE_OWNER: '1' },
|
|
71
|
+
port,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function renderPackageConnectorLaunchAgent(input: { paths: PackageConnectorServicePaths; launch: ReturnType<typeof packageConnectorLaunchSpec> }): string {
|
|
76
|
+
const environmentXml = Object.entries(input.launch.environment).sort(([a], [b]) => a.localeCompare(b))
|
|
77
|
+
.map(([key, value]) => ` <key>${xml(key)}</key>\n <string>${xml(value)}</string>`).join('\n');
|
|
78
|
+
const args = [input.launch.executable, ...input.launch.args];
|
|
79
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key>\n <string>${xml(input.paths.label)}</string>\n <key>ProgramArguments</key>\n <array>\n${args.map((arg) => ` <string>${xml(arg)}</string>`).join('\n')}\n </array>\n <key>EnvironmentVariables</key>\n <dict>\n${environmentXml}\n </dict>\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <dict><key>SuccessfulExit</key><false/></dict>\n <key>ThrottleInterval</key><integer>5</integer>\n <key>StandardOutPath</key><string>${xml(input.paths.stdoutPath)}</string>\n <key>StandardErrorPath</key><string>${xml(input.paths.stderrPath)}</string>\n</dict>\n</plist>\n`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function renderPackageConnectorSystemdUserUnit(input: { launch: ReturnType<typeof packageConnectorLaunchSpec> }): string {
|
|
83
|
+
const quote = (value: string) => `"${value.replaceAll('%', '%%').replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
84
|
+
const env = Object.entries(input.launch.environment).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `Environment=${quote(`${k}=${v}`)}`);
|
|
85
|
+
return ['[Unit]', 'Description=Forge ChatGPT OAuth Gateway', 'After=network-online.target', '', '[Service]', 'Type=simple', `ExecStart=${[input.launch.executable, ...input.launch.args].map(quote).join(' ')}`, ...env, 'Restart=on-failure', 'RestartSec=5', '', '[Install]', 'WantedBy=default.target', ''].join('\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function installSystemd(paths: PackageConnectorServicePaths, launch: ReturnType<typeof packageConnectorLaunchSpec>, env: NodeJS.ProcessEnv): string {
|
|
89
|
+
const unitName = `${paths.label}.service`;
|
|
90
|
+
const unitPath = join(env.HOME ?? homedir(), '.config', 'systemd', 'user', unitName);
|
|
91
|
+
atomicWrite(unitPath, renderPackageConnectorSystemdUserUnit({ launch }), 0o644);
|
|
92
|
+
for (const args of [['--user', 'daemon-reload'], ['--user', 'enable', '--now', unitName]]) {
|
|
93
|
+
const result = spawnSync('systemctl', args, { encoding: 'utf8', env, timeout: 30_000 });
|
|
94
|
+
if (result.status !== 0) throw new Error(`FORGE_PACKAGE_CONNECTOR_SYSTEMD_INSTALL_FAILED: ${(result.stderr || result.stdout || '').trim()}`);
|
|
95
|
+
}
|
|
96
|
+
return unitPath;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function startPortable(paths: PackageConnectorServicePaths, launch: ReturnType<typeof packageConnectorLaunchSpec>, env: NodeJS.ProcessEnv): number {
|
|
100
|
+
mkdirSync(join(paths.serviceRoot, 'logs'), { recursive: true, mode: 0o700 });
|
|
101
|
+
const stdout = openSync(paths.stdoutPath, 'a', 0o600), stderr = openSync(paths.stderrPath, 'a', 0o600);
|
|
102
|
+
// A connector is not the Canonical Runtime writer. Never let a transient
|
|
103
|
+
// installer/worker write claim escape into this long-lived process; the claim
|
|
104
|
+
// would become stale as soon as its parent exits. Keep only the explicit
|
|
105
|
+
// Controller Home and lifecycle-role marker from launch.environment.
|
|
106
|
+
const childEnv = { ...env };
|
|
107
|
+
for (const key of [
|
|
108
|
+
'FORGE_RUNTIME_INSTANCE_ID', 'FORGE_RUNTIME_OWNER_PID', 'FORGE_RELEASE_AUTHORITY_REVISION',
|
|
109
|
+
'FORGE_RELEASE_FENCING_TOKEN', 'FORGE_RELEASE_ID', 'FORGE_ARTIFACT_IDENTITY', 'FORGE_WORKER_PROTOCOL_VERSION',
|
|
110
|
+
]) delete childEnv[key];
|
|
111
|
+
try {
|
|
112
|
+
const child = spawn(launch.executable, launch.args, { detached: true, stdio: ['ignore', stdout, stderr], env: { ...childEnv, ...launch.environment } });
|
|
113
|
+
if (!child.pid) throw new Error('FORGE_PACKAGE_CONNECTOR_START_FAILED');
|
|
114
|
+
child.unref();
|
|
115
|
+
return child.pid;
|
|
116
|
+
} finally { closeSync(stdout); closeSync(stderr); }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function installPackageConnectorService(input: { release: PackageRuntimeRelease; controllerHome: string; endpoint: string; platform?: NodeJS.Platform; env?: NodeJS.ProcessEnv; forcePortable?: boolean }): Promise<PackageConnectorServiceResult> {
|
|
120
|
+
const paths = packageConnectorServicePaths(input.controllerHome, input.env?.HOME);
|
|
121
|
+
mkdirSync(join(paths.serviceRoot, 'logs'), { recursive: true, mode: 0o700 });
|
|
122
|
+
const launch = packageConnectorLaunchSpec({ release: input.release, controllerHome: input.controllerHome, endpoint: input.endpoint });
|
|
123
|
+
const platform = input.platform ?? process.platform, env = input.env ?? process.env;
|
|
124
|
+
if (!input.forcePortable && platform === 'darwin') {
|
|
125
|
+
atomicWrite(paths.sourcePlistPath, renderPackageConnectorLaunchAgent({ paths, launch }), 0o600);
|
|
126
|
+
installLaunchAgent(paths.sourcePlistPath, paths.label);
|
|
127
|
+
const result = await bootstrapLaunchAgentWithRetryV2({ label: paths.label, plistPath: paths.installedPlistPath });
|
|
128
|
+
if (!result.ok) throw new Error(`FORGE_PACKAGE_CONNECTOR_LAUNCHD_INSTALL_FAILED: ${result.diagnostics.join('; ')}`);
|
|
129
|
+
return { endpoint: input.endpoint, mode: 'launchd', persistent: true, servicePath: paths.installedPlistPath };
|
|
130
|
+
}
|
|
131
|
+
if (!input.forcePortable && platform === 'linux') {
|
|
132
|
+
const probe = spawnSync('systemctl', ['--user', 'show-environment'], { encoding: 'utf8', env, timeout: 30_000 });
|
|
133
|
+
if (probe.status === 0) return { endpoint: input.endpoint, mode: 'systemd-user', persistent: true, servicePath: installSystemd(paths, launch, env) };
|
|
134
|
+
}
|
|
135
|
+
return { endpoint: input.endpoint, mode: 'portable', persistent: false, pid: startPortable(paths, launch, env) };
|
|
136
|
+
}
|