@moretea-labs/forge 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/DESIGN.md +7 -2
- package/README.md +4 -4
- package/README.zh-CN.md +2 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/assets/hooks/pre-edit-guard.sh +8 -39
- package/assets/hooks/prompt-guard.sh +2 -2
- package/assets/plugin-registry.v1.json +54 -3
- package/assets/reference-configs/hook-operations.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +2 -2
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/ensure-task-workflow.sh +2 -2
- package/assets/templates/helpers/sprint-backlog.sh +6 -6
- package/docs/architecture/current/automation-and-schedule-engine.md +9 -3
- package/docs/architecture/current/codegraph-context-provider.md +15 -3
- package/docs/architecture/current/dispatch-and-agent-strategy.md +32 -13
- package/docs/architecture/current/entity-model.md +20 -0
- package/docs/architecture/current/external-plugin-governance.md +3 -3
- package/docs/architecture/current/system-overview.md +26 -1
- package/docs/architecture/current/thin-harness-v1.md +10 -0
- package/docs/operations/chatgpt-personal-assistant-runtime.md +12 -2
- package/docs/operations/controller-browser-plugin.md +12 -16
- package/docs/operations/features.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/tutorials/01-install-and-start.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/wiki/Installation.md +1 -1
- package/docs/wiki/Quick-Start.md +1 -1
- package/package.json +1 -1
- package/scripts/check-public-docs.sh +14 -1
- package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
- package/scripts/sprint-backlog.sh +1 -1
- package/src/cli/commands/chatgpt.ts +31 -2
- package/src/cli/commands/plugin.ts +36 -9
- package/src/cli/controller/context-pack.ts +158 -6
- package/src/cli/hook/prompt-guard-decision.ts +17 -18
- package/src/cli/hook/prompt-intents.ts +1 -1
- package/src/cli/local-bridge/console-automations.ts +205 -23
- package/src/cli/local-bridge/console-view-models.ts +2 -0
- package/src/cli/local-bridge/facade-api.ts +2 -0
- package/src/cli/local-bridge/frontend/src/App.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +143 -3
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +181 -3
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
- package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -6
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -1
- package/src/cli/local-bridge/frontend/src/styles.css +27 -11
- package/src/cli/local-bridge/frontend/src/types.ts +6 -2
- package/src/cli/local-bridge/server.ts +29 -19
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +5003 -4815
- package/src/cli/mcp/paths.ts +8 -2
- package/src/cli/mcp/repository-tools.ts +124 -9
- package/src/cli/mcp/server.ts +164 -19
- package/src/cli/mcp/transports/http.ts +25 -2
- package/src/cli/mcp/transports/session-registry.ts +24 -0
- package/src/cli/repositories/command-classifier.ts +113 -0
- package/src/cli/repositories/safe-patch.ts +6 -4
- package/src/cli/repositories/structured-git.ts +49 -7
- package/src/cli/repository/inspector.ts +16 -3
- package/src/runtime/assistant/intent.ts +3 -3
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +157 -2
- package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
- package/src/runtime/control-plane/execution/validation.ts +15 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +32 -0
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +6 -7
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +2 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +1 -1
- package/src/runtime/control-plane/facade/controller-session-store.ts +12 -13
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +7 -2
- package/src/runtime/control-plane/facade/goal-workloop.ts +231 -8
- package/src/runtime/control-plane/facade/plan-contract-store.ts +32 -12
- package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
- package/src/runtime/control-plane/facade/types.ts +33 -6
- package/src/runtime/control-plane/facade/work-contract-store.ts +31 -2
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +375 -23
- package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
- package/src/runtime/diagnostics/mcp-timing.ts +6 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +1 -10
- package/src/runtime/execution/process-runtime/check-result.ts +10 -2
- package/src/runtime/execution/process-runtime/command-facade.ts +1 -0
- package/src/runtime/execution/process-runtime/gc.ts +64 -10
- package/src/runtime/execution/process-runtime/persisted-check.ts +10 -2
- package/src/runtime/execution/process-runtime/resource-claims.ts +87 -2
- package/src/runtime/execution/process-runtime/runtime.ts +16 -1
- package/src/runtime/execution/process-runtime/store.ts +6 -0
- package/src/runtime/gateway/mcp/execution-tools.ts +27 -12
- package/src/runtime/gateway/mcp/router.ts +1 -1
- package/src/runtime/gateway/mcp/runtime-tools.ts +482 -178
- package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
- package/src/runtime/plugins/browser-adapter.ts +19 -72
- package/src/runtime/plugins/browser-handoff-host.ts +6 -31
- package/src/runtime/plugins/browser-handoff.ts +0 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +8 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +8 -11
- package/src/runtime/plugins/external-registration.ts +84 -5
- package/src/runtime/plugins/first-party-registry.ts +2 -2
- package/src/runtime/plugins/gmail-adapter.ts +28 -2
- package/src/runtime/plugins/local-system-adapter.ts +2 -2
- package/src/runtime/plugins/plugin-management-adapter.ts +140 -0
- package/src/runtime/plugins/store.ts +107 -5
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/recovery/actions.ts +0 -10
- package/src/runtime/recovery/classifier.ts +0 -9
- package/src/runtime/recovery/maintenance-executor.ts +43 -11
- package/src/runtime/recovery/probe.ts +0 -1
- package/src/runtime/recovery/types.ts +0 -1
- package/src/runtime/root/release-materialize.ts +143 -19
- package/src/runtime/root/service.ts +6 -4
- package/src/runtime/safe-tooling/index.ts +0 -1
- package/src/runtime/safe-tooling/types.ts +0 -30
- package/src/runtime/standalone-recovery/core.ts +2 -2
- package/src/runtime/workflow/schedules/browser-probe.ts +180 -0
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
- package/src/runtime/workflow/schedules/engine.ts +357 -64
- package/src/runtime/workflow/schedules/types.ts +5 -0
- package/src/runtime/workflow/schedules/work-continuation.ts +100 -27
- package/bin/forge-desktop-helper.mjs +0 -204
- package/docs/operations/controller-desktop-plugin.md +0 -112
- package/src/runtime/plugins/desktop-adapter.ts +0 -374
- package/src/runtime/safe-tooling/browser-safe.ts +0 -133
|
@@ -14,6 +14,15 @@ import { CONTROL_PLANE_SCHEMA_VERSION } from '../control-plane/persistence/sqlit
|
|
|
14
14
|
* `forge runtime service install` operation; staging alone never starts or
|
|
15
15
|
* publishes anything.
|
|
16
16
|
*/
|
|
17
|
+
export const FORGE_MACOS_RUNTIME_SIGNING_IDENTIFIER = 'com.moretea.forge.runtime';
|
|
18
|
+
|
|
19
|
+
export interface MacOSRuntimeCodeSigning {
|
|
20
|
+
identifier: string;
|
|
21
|
+
teamIdentifier: string;
|
|
22
|
+
designatedRequirement: string;
|
|
23
|
+
authority: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
export interface StagedRuntimeRelease {
|
|
18
27
|
releasePath: string;
|
|
19
28
|
manifestPath: string;
|
|
@@ -22,7 +31,6 @@ export interface StagedRuntimeRelease {
|
|
|
22
31
|
diagnosticArtifactIdentity?: string;
|
|
23
32
|
browserNodeBridgeArtifactIdentity?: string;
|
|
24
33
|
browserHandoffArtifactIdentity?: string;
|
|
25
|
-
desktopHelperArtifactIdentity?: string;
|
|
26
34
|
processRunnerArtifactIdentity?: string;
|
|
27
35
|
checkRunnerArtifactIdentity?: string;
|
|
28
36
|
externalPluginProbeArtifactIdentity?: string;
|
|
@@ -30,12 +38,15 @@ export interface StagedRuntimeRelease {
|
|
|
30
38
|
codeGraphSidecarArtifactIdentity?: string;
|
|
31
39
|
codeGraphLibraryArtifactIdentity?: string;
|
|
32
40
|
controllerUiArtifactIdentity?: string;
|
|
41
|
+
macosCodeSigning?: MacOSRuntimeCodeSigning;
|
|
33
42
|
manifestSha256: string;
|
|
34
43
|
sourceCommit: string;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
export interface RuntimeReleaseMaterializerDependencies {
|
|
38
47
|
now?: () => number;
|
|
48
|
+
platform?: NodeJS.Platform;
|
|
49
|
+
signMacOSRuntime?: (input: { executable: string; controllerHome: string }) => MacOSRuntimeCodeSigning;
|
|
39
50
|
uuid?: () => string;
|
|
40
51
|
compileBinary?: (input: { sourceRoot: string; outputPath: string; entryPath?: string }) => { ok: boolean; stderr?: string; stdout?: string; error?: string };
|
|
41
52
|
bundleNodeHost?: (input: { sourceRoot: string; outputPath: string; entryPath: string }) => { ok: boolean; stderr?: string; stdout?: string; error?: string };
|
|
@@ -59,6 +70,8 @@ export interface CandidateRuntimeStageReceiptV1 {
|
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
export interface CandidateRuntimeReleaseStagerDependencies {
|
|
73
|
+
platform?: NodeJS.Platform;
|
|
74
|
+
inspectMacOSRuntime?: (executable: string) => MacOSRuntimeCodeSigning;
|
|
62
75
|
runCandidateStager?: (input: {
|
|
63
76
|
bunExecutable: string;
|
|
64
77
|
scriptPath: string;
|
|
@@ -78,6 +91,95 @@ function sha256(path: string): string {
|
|
|
78
91
|
return createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
79
92
|
}
|
|
80
93
|
|
|
94
|
+
function parseMacOSRuntimeCodeSigning(value: unknown, context: string): MacOSRuntimeCodeSigning | undefined {
|
|
95
|
+
if (value === undefined) return undefined;
|
|
96
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${context}: macosCodeSigning must be an object`);
|
|
97
|
+
const record = value as Record<string, unknown>;
|
|
98
|
+
const field = (name: keyof MacOSRuntimeCodeSigning): string => {
|
|
99
|
+
const entry = record[name];
|
|
100
|
+
if (typeof entry !== 'string' || !entry.trim()) throw new Error(`${context}: macosCodeSigning.${name} is required`);
|
|
101
|
+
return entry.trim();
|
|
102
|
+
};
|
|
103
|
+
const signing = {
|
|
104
|
+
identifier: field('identifier'),
|
|
105
|
+
teamIdentifier: field('teamIdentifier'),
|
|
106
|
+
designatedRequirement: field('designatedRequirement'),
|
|
107
|
+
authority: field('authority'),
|
|
108
|
+
};
|
|
109
|
+
if (signing.identifier !== FORGE_MACOS_RUNTIME_SIGNING_IDENTIFIER) {
|
|
110
|
+
throw new Error(`${context}: macosCodeSigning.identifier must be ${FORGE_MACOS_RUNTIME_SIGNING_IDENTIFIER}`);
|
|
111
|
+
}
|
|
112
|
+
if (!/^[A-Z0-9]{10}$/.test(signing.teamIdentifier)) throw new Error(`${context}: macosCodeSigning.teamIdentifier is invalid`);
|
|
113
|
+
if (!signing.authority.startsWith('Developer ID Application: ')) throw new Error(`${context}: macosCodeSigning.authority must be Developer ID Application`);
|
|
114
|
+
if (!signing.designatedRequirement.includes(`identifier \"${FORGE_MACOS_RUNTIME_SIGNING_IDENTIFIER}\"`)
|
|
115
|
+
|| !signing.designatedRequirement.includes('anchor apple generic')
|
|
116
|
+
|| !signing.designatedRequirement.includes(`certificate leaf[subject.OU] = ${signing.teamIdentifier}`)) {
|
|
117
|
+
throw new Error(`${context}: macosCodeSigning.designatedRequirement is not the stable Developer ID contract`);
|
|
118
|
+
}
|
|
119
|
+
return signing;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readActiveMacOSRuntimeTeam(controllerHome: string): string | undefined {
|
|
123
|
+
const authorityPath = join(resolve(controllerHome), 'runtime', 'releases', 'authority.json');
|
|
124
|
+
if (!existsSync(authorityPath)) return undefined;
|
|
125
|
+
try {
|
|
126
|
+
const authority = JSON.parse(readFileSync(authorityPath, 'utf8')) as { active?: { manifestPath?: string } };
|
|
127
|
+
if (!authority.active?.manifestPath || !existsSync(authority.active.manifestPath)) return undefined;
|
|
128
|
+
const manifest = JSON.parse(readFileSync(authority.active.manifestPath, 'utf8')) as Record<string, unknown>;
|
|
129
|
+
return parseMacOSRuntimeCodeSigning(manifest.macosCodeSigning, 'RUNTIME_RELEASE_ACTIVE_SIGNING_INVALID')?.teamIdentifier;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error instanceof Error && error.message.startsWith('RUNTIME_RELEASE_ACTIVE_SIGNING_INVALID')) throw error;
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function inspectMacOSRuntimeCodeSigning(executable: string): MacOSRuntimeCodeSigning {
|
|
137
|
+
const verify = runProcess('codesign', ['--verify', '--strict', executable], { timeoutMs: 30_000, maxOutputBytes: 64 * 1024 });
|
|
138
|
+
if (!verify.ok) throw new Error(`RUNTIME_RELEASE_MACOS_SIGNATURE_INVALID: ${verify.stderr || verify.stdout || verify.error}`.slice(0, 2_000));
|
|
139
|
+
const detail = runProcess('codesign', ['-dv', '--verbose=4', executable], { timeoutMs: 30_000, maxOutputBytes: 64 * 1024 });
|
|
140
|
+
if (!detail.ok) throw new Error(`RUNTIME_RELEASE_MACOS_SIGNATURE_INSPECTION_FAILED: ${detail.stderr || detail.stdout || detail.error}`.slice(0, 2_000));
|
|
141
|
+
const detailText = `${detail.stdout}\n${detail.stderr}`;
|
|
142
|
+
const identifier = detailText.match(/^Identifier=(.+)$/m)?.[1]?.trim() ?? '';
|
|
143
|
+
const teamIdentifier = detailText.match(/^TeamIdentifier=(.+)$/m)?.[1]?.trim() ?? '';
|
|
144
|
+
const authority = detailText.match(/^Authority=(Developer ID Application:.+)$/m)?.[1]?.trim() ?? '';
|
|
145
|
+
const requirementResult = runProcess('codesign', ['-d', '-r-', executable], { timeoutMs: 30_000, maxOutputBytes: 64 * 1024 });
|
|
146
|
+
if (!requirementResult.ok) throw new Error(`RUNTIME_RELEASE_MACOS_REQUIREMENT_INSPECTION_FAILED: ${requirementResult.stderr || requirementResult.stdout || requirementResult.error}`.slice(0, 2_000));
|
|
147
|
+
const requirementText = `${requirementResult.stdout}\n${requirementResult.stderr}`;
|
|
148
|
+
const designatedRequirement = requirementText.match(/^designated => (.+)$/m)?.[1]?.trim() ?? '';
|
|
149
|
+
return parseMacOSRuntimeCodeSigning({ identifier, teamIdentifier, authority, designatedRequirement }, 'RUNTIME_RELEASE_MACOS_SIGNATURE_INVALID')!;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function defaultSignMacOSRuntime(input: { executable: string; controllerHome: string }): MacOSRuntimeCodeSigning {
|
|
153
|
+
const identities = runProcess('security', ['find-identity', '-v', '-p', 'codesigning'], { timeoutMs: 30_000, maxOutputBytes: 128 * 1024 });
|
|
154
|
+
if (!identities.ok) throw new Error(`RUNTIME_RELEASE_MACOS_SIGNING_IDENTITY_LOOKUP_FAILED: ${identities.stderr || identities.stdout || identities.error}`.slice(0, 2_000));
|
|
155
|
+
const candidates = identities.stdout.split(/\r?\n/).flatMap((line) => {
|
|
156
|
+
const match = line.match(/^\s*\d+\)\s+([A-Fa-f0-9]{40})\s+\"(Developer ID Application: .+ \(([A-Z0-9]{10})\))\"/);
|
|
157
|
+
return match ? [{ hash: match[1]!, label: match[2]!, teamIdentifier: match[3]! }] : [];
|
|
158
|
+
});
|
|
159
|
+
const configured = process.env.FORGE_MACOS_RUNTIME_SIGNING_IDENTITY?.trim();
|
|
160
|
+
const expectedTeam = readActiveMacOSRuntimeTeam(input.controllerHome);
|
|
161
|
+
let selected = configured
|
|
162
|
+
? candidates.find((candidate) => candidate.hash.toLowerCase() === configured.toLowerCase() || candidate.label === configured)
|
|
163
|
+
: undefined;
|
|
164
|
+
if (configured && !selected) throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_IDENTITY_NOT_FOUND');
|
|
165
|
+
if (!selected) {
|
|
166
|
+
const eligible = expectedTeam ? candidates.filter((candidate) => candidate.teamIdentifier === expectedTeam) : candidates;
|
|
167
|
+
const teams = Array.from(new Set(eligible.map((candidate) => candidate.teamIdentifier)));
|
|
168
|
+
if (teams.length !== 1 || eligible.length === 0) {
|
|
169
|
+
throw new Error(expectedTeam ? 'RUNTIME_RELEASE_MACOS_SIGNING_IDENTITY_FOR_TEAM_REQUIRED' : 'RUNTIME_RELEASE_MACOS_SIGNING_IDENTITY_REQUIRED');
|
|
170
|
+
}
|
|
171
|
+
selected = eligible[0];
|
|
172
|
+
}
|
|
173
|
+
if (expectedTeam && selected.teamIdentifier !== expectedTeam) throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_TEAM_CHANGED');
|
|
174
|
+
const signed = runProcess('codesign', [
|
|
175
|
+
'--force', '--sign', selected.hash, '--identifier', FORGE_MACOS_RUNTIME_SIGNING_IDENTIFIER, '--options', 'runtime', input.executable,
|
|
176
|
+
], { timeoutMs: 120_000, maxOutputBytes: 128 * 1024 });
|
|
177
|
+
if (!signed.ok) throw new Error(`RUNTIME_RELEASE_MACOS_SIGNING_FAILED: ${signed.stderr || signed.stdout || signed.error}`.slice(0, 2_000));
|
|
178
|
+
const inspected = inspectMacOSRuntimeCodeSigning(input.executable);
|
|
179
|
+
if (inspected.teamIdentifier !== selected.teamIdentifier) throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_TEAM_MISMATCH');
|
|
180
|
+
return inspected;
|
|
181
|
+
}
|
|
182
|
+
|
|
81
183
|
function sha256Directory(root: string): string {
|
|
82
184
|
const hash = createHash('sha256');
|
|
83
185
|
const visit = (directory: string): void => {
|
|
@@ -203,12 +305,28 @@ export function stageRuntimeReleaseFromCandidateSource(input: {
|
|
|
203
305
|
if (sha256(manifestPath) !== receipt.manifestSha256) {
|
|
204
306
|
throw new Error('RUNTIME_RELEASE_CANDIDATE_MANIFEST_IDENTITY_MISMATCH');
|
|
205
307
|
}
|
|
308
|
+
let rawManifest: Record<string, unknown>;
|
|
309
|
+
try { rawManifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Record<string, unknown>; }
|
|
310
|
+
catch (error) { throw new Error(`RUNTIME_RELEASE_CANDIDATE_MANIFEST_INVALID: ${error instanceof Error ? error.message : String(error)}`); }
|
|
311
|
+
const platform = dependencies.platform ?? process.platform;
|
|
312
|
+
const macosCodeSigning = parseMacOSRuntimeCodeSigning(rawManifest.macosCodeSigning, 'RUNTIME_RELEASE_CANDIDATE_SIGNING_INVALID');
|
|
313
|
+
if (platform === 'darwin') {
|
|
314
|
+
if (!macosCodeSigning) throw new Error('RUNTIME_RELEASE_CANDIDATE_MACOS_SIGNING_REQUIRED');
|
|
315
|
+
const actual = (dependencies.inspectMacOSRuntime ?? inspectMacOSRuntimeCodeSigning)(runtimePath);
|
|
316
|
+
if (actual.identifier !== macosCodeSigning.identifier
|
|
317
|
+
|| actual.teamIdentifier !== macosCodeSigning.teamIdentifier
|
|
318
|
+
|| actual.authority !== macosCodeSigning.authority
|
|
319
|
+
|| actual.designatedRequirement !== macosCodeSigning.designatedRequirement) {
|
|
320
|
+
throw new Error('RUNTIME_RELEASE_CANDIDATE_MACOS_SIGNING_MISMATCH');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
206
323
|
|
|
207
324
|
return {
|
|
208
325
|
releasePath,
|
|
209
326
|
manifestPath,
|
|
210
327
|
releaseId: receipt.releaseId,
|
|
211
328
|
artifactIdentity: receipt.artifactIdentity,
|
|
329
|
+
...(macosCodeSigning ? { macosCodeSigning } : {}),
|
|
212
330
|
manifestSha256: receipt.manifestSha256,
|
|
213
331
|
sourceCommit: receipt.sourceCommit,
|
|
214
332
|
};
|
|
@@ -297,6 +415,14 @@ export function stageRuntimeRelease(input: {
|
|
|
297
415
|
throw new Error(`RUNTIME_RELEASE_BUILD_FAILED: ${compile.stderr || compile.stdout || compile.error}`.slice(0, 2_000));
|
|
298
416
|
}
|
|
299
417
|
chmodSync(executable, 0o700);
|
|
418
|
+
const platform = dependencies.platform ?? process.platform;
|
|
419
|
+
const macosCodeSigning = platform === 'darwin'
|
|
420
|
+
? (dependencies.signMacOSRuntime ?? defaultSignMacOSRuntime)({ executable, controllerHome: resolve(input.controllerHome) })
|
|
421
|
+
: undefined;
|
|
422
|
+
if (platform === 'darwin' && !macosCodeSigning) throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_REQUIRED');
|
|
423
|
+
const normalizedMacOSCodeSigning = macosCodeSigning
|
|
424
|
+
? parseMacOSRuntimeCodeSigning(macosCodeSigning, 'RUNTIME_RELEASE_MACOS_SIGNING_INVALID')
|
|
425
|
+
: undefined;
|
|
300
426
|
const artifactIdentity = `sha256:${sha256(executable)}`;
|
|
301
427
|
|
|
302
428
|
const diagnosticExecutable = join(staging, 'forge-cli');
|
|
@@ -407,28 +533,18 @@ export function stageRuntimeRelease(input: {
|
|
|
407
533
|
cpSync(sourceControllerUiPath, controllerUiPath, { recursive: true, force: false });
|
|
408
534
|
const controllerUiArtifactIdentity = `sha256:${sha256Directory(controllerUiPath)}`;
|
|
409
535
|
|
|
410
|
-
const desktopHelperEntrypoint = 'forge-desktop-helper.mjs' as const;
|
|
411
|
-
const sourceDesktopHelperPath = join(sourceRoot, 'bin', desktopHelperEntrypoint);
|
|
412
|
-
if (!existsSync(sourceDesktopHelperPath)) {
|
|
413
|
-
throw new Error(`RUNTIME_RELEASE_DESKTOP_HELPER_SOURCE_MISSING: ${sourceDesktopHelperPath}`);
|
|
414
|
-
}
|
|
415
|
-
const desktopHelperPath = join(staging, desktopHelperEntrypoint);
|
|
416
|
-
copyFileSync(sourceDesktopHelperPath, desktopHelperPath);
|
|
417
|
-
chmodSync(desktopHelperPath, 0o700);
|
|
418
|
-
const desktopHelperArtifactIdentity = `sha256:${sha256(desktopHelperPath)}`;
|
|
419
536
|
const manifest = {
|
|
420
537
|
schemaVersion: 1,
|
|
421
538
|
releaseId,
|
|
422
539
|
artifactIdentity,
|
|
423
540
|
entrypoint: 'forge-runtime',
|
|
541
|
+
...(normalizedMacOSCodeSigning ? { macosCodeSigning: normalizedMacOSCodeSigning } : {}),
|
|
424
542
|
diagnosticEntrypoint: 'forge-cli',
|
|
425
543
|
diagnosticArtifactIdentity,
|
|
426
544
|
browserNodeBridgeEntrypoint,
|
|
427
545
|
browserNodeBridgeArtifactIdentity,
|
|
428
546
|
browserHandoffEntrypoint,
|
|
429
547
|
browserHandoffArtifactIdentity,
|
|
430
|
-
desktopHelperEntrypoint,
|
|
431
|
-
desktopHelperArtifactIdentity,
|
|
432
548
|
processRunnerEntrypoint,
|
|
433
549
|
processRunnerArtifactIdentity,
|
|
434
550
|
checkRunnerEntrypoint,
|
|
@@ -464,10 +580,10 @@ export function stageRuntimeRelease(input: {
|
|
|
464
580
|
manifestPath: join(releasePath, 'manifest.json'),
|
|
465
581
|
releaseId,
|
|
466
582
|
artifactIdentity,
|
|
583
|
+
...(normalizedMacOSCodeSigning ? { macosCodeSigning: normalizedMacOSCodeSigning } : {}),
|
|
467
584
|
diagnosticArtifactIdentity,
|
|
468
585
|
browserNodeBridgeArtifactIdentity,
|
|
469
586
|
browserHandoffArtifactIdentity,
|
|
470
|
-
desktopHelperArtifactIdentity,
|
|
471
587
|
processRunnerArtifactIdentity,
|
|
472
588
|
checkRunnerArtifactIdentity,
|
|
473
589
|
externalPluginProbeArtifactIdentity,
|
|
@@ -484,12 +600,23 @@ export function stageRuntimeRelease(input: {
|
|
|
484
600
|
}
|
|
485
601
|
}
|
|
486
602
|
|
|
487
|
-
export function assertRuntimeReleaseFiles(release: StagedRuntimeRelease): void {
|
|
603
|
+
export function assertRuntimeReleaseFiles(release: StagedRuntimeRelease, dependencies: { platform?: NodeJS.Platform; inspectMacOSRuntime?: (executable: string) => MacOSRuntimeCodeSigning } = {}): void {
|
|
488
604
|
if (!existsSync(release.releasePath) || !existsSync(release.manifestPath)) {
|
|
489
605
|
throw new Error(`RUNTIME_RELEASE_FILES_MISSING: ${release.releasePath}`);
|
|
490
606
|
}
|
|
491
|
-
|
|
492
|
-
|
|
607
|
+
const runtimePath = join(release.releasePath, 'forge-runtime');
|
|
608
|
+
if (!existsSync(runtimePath)) {
|
|
609
|
+
throw new Error(`RUNTIME_RELEASE_ENTRYPOINT_MISSING: ${runtimePath}`);
|
|
610
|
+
}
|
|
611
|
+
const platform = dependencies.platform ?? process.platform;
|
|
612
|
+
if (platform === 'darwin' && release.macosCodeSigning) {
|
|
613
|
+
const actual = (dependencies.inspectMacOSRuntime ?? inspectMacOSRuntimeCodeSigning)(runtimePath);
|
|
614
|
+
if (actual.identifier !== release.macosCodeSigning.identifier
|
|
615
|
+
|| actual.teamIdentifier !== release.macosCodeSigning.teamIdentifier
|
|
616
|
+
|| actual.authority !== release.macosCodeSigning.authority
|
|
617
|
+
|| actual.designatedRequirement !== release.macosCodeSigning.designatedRequirement) {
|
|
618
|
+
throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_MISMATCH');
|
|
619
|
+
}
|
|
493
620
|
}
|
|
494
621
|
if (release.diagnosticArtifactIdentity && !existsSync(join(release.releasePath, 'forge-cli'))) {
|
|
495
622
|
throw new Error(`RUNTIME_RELEASE_DIAGNOSTIC_ENTRYPOINT_MISSING: ${join(release.releasePath, 'forge-cli')}`);
|
|
@@ -500,9 +627,6 @@ export function assertRuntimeReleaseFiles(release: StagedRuntimeRelease): void {
|
|
|
500
627
|
if (release.browserHandoffArtifactIdentity && !existsSync(join(release.releasePath, 'browser-handoff-host.js'))) {
|
|
501
628
|
throw new Error(`RUNTIME_RELEASE_BROWSER_HANDOFF_HOST_MISSING: ${join(release.releasePath, 'browser-handoff-host.js')}`);
|
|
502
629
|
}
|
|
503
|
-
if (release.desktopHelperArtifactIdentity && !existsSync(join(release.releasePath, 'forge-desktop-helper.mjs'))) {
|
|
504
|
-
throw new Error(`RUNTIME_RELEASE_DESKTOP_HELPER_MISSING: ${join(release.releasePath, 'forge-desktop-helper.mjs')}`);
|
|
505
|
-
}
|
|
506
630
|
if (release.processRunnerArtifactIdentity && !existsSync(join(release.releasePath, 'process-runner.js'))) {
|
|
507
631
|
throw new Error(`RUNTIME_RELEASE_PROCESS_RUNNER_MISSING: ${join(release.releasePath, 'process-runner.js')}`);
|
|
508
632
|
}
|
|
@@ -232,7 +232,7 @@ export function syncForgeRuntimeActiveEntrypoint(controllerHome: string): { path
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
export function ensureForgeRuntimeLaunchAgentContract(
|
|
235
|
-
input: { controllerHome: string; bootstrapNodeExecutable?: string; bootstrapRunnerPath?: string },
|
|
235
|
+
input: { controllerHome: string; bootstrapNodeExecutable?: string; bootstrapRunnerPath?: string; installUserLaunchAgent?: boolean },
|
|
236
236
|
): { paths: ForgeRuntimeServicePaths; mode: 'release' | 'bootstrap'; changed: boolean } {
|
|
237
237
|
const paths = forgeRuntimeServicePaths(input.controllerHome);
|
|
238
238
|
const active = activeRuntimeEntrypoint(input.controllerHome);
|
|
@@ -250,10 +250,12 @@ export function ensureForgeRuntimeLaunchAgentContract(
|
|
|
250
250
|
: { nodeExecutable: input.bootstrapNodeExecutable!, runnerPath: input.bootstrapRunnerPath! }),
|
|
251
251
|
});
|
|
252
252
|
const existingSource = existsSync(paths.sourcePlistPath) ? readFileSync(paths.sourcePlistPath, 'utf8') : undefined;
|
|
253
|
-
const existingInstalled =
|
|
254
|
-
|
|
253
|
+
const existingInstalled = input.installUserLaunchAgent && existsSync(paths.installedPlistPath)
|
|
254
|
+
? readFileSync(paths.installedPlistPath, 'utf8')
|
|
255
|
+
: undefined;
|
|
256
|
+
const changed = existingSource !== plist || (input.installUserLaunchAgent === true && existingInstalled !== plist);
|
|
255
257
|
if (existingSource !== plist) atomicWrite(paths.sourcePlistPath, plist);
|
|
256
|
-
if (existingInstalled !== plist) atomicWrite(paths.installedPlistPath, plist, 0o644);
|
|
258
|
+
if (input.installUserLaunchAgent === true && existingInstalled !== plist) atomicWrite(paths.installedPlistPath, plist, 0o644);
|
|
257
259
|
return { paths, mode: useRelease ? 'release' : 'bootstrap', changed };
|
|
258
260
|
}
|
|
259
261
|
|
|
@@ -44,36 +44,6 @@ export interface SafePluginSummary {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export interface WebTarget {
|
|
48
|
-
targetKey: string;
|
|
49
|
-
domain: string;
|
|
50
|
-
origin: string;
|
|
51
|
-
allowed: true;
|
|
52
|
-
defaultPath: '/';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface WebDomainAccessPreview {
|
|
56
|
-
ticketId: string;
|
|
57
|
-
normalizedDomain: string;
|
|
58
|
-
domainKey: string;
|
|
59
|
-
reason: string;
|
|
60
|
-
expiresAt: string;
|
|
61
|
-
alreadyAllowed: boolean;
|
|
62
|
-
currentAllowedDomainCount: number;
|
|
63
|
-
risk: 'workspace_write';
|
|
64
|
-
confirmation: 'authorization';
|
|
65
|
-
localOnly: true;
|
|
66
|
-
willChange: {
|
|
67
|
-
pluginId: 'browser';
|
|
68
|
-
configField: 'allowedDomains';
|
|
69
|
-
addDomain: string;
|
|
70
|
-
};
|
|
71
|
-
safety: {
|
|
72
|
-
arbitraryUrlAccepted: false;
|
|
73
|
-
domainOnly: true;
|
|
74
|
-
sensitiveConfigReturned: false;
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
47
|
|
|
78
48
|
export interface SafeJobResultSummary {
|
|
79
49
|
jobId: string;
|
|
@@ -1740,7 +1740,7 @@ export async function activateRuntimeRelease(
|
|
|
1740
1740
|
return { ok: false, attempted: true, detail, serviceTarget: service.target, verify: await verifyLocal(config) } satisfies RuntimeReleaseActivationResult;
|
|
1741
1741
|
}
|
|
1742
1742
|
try {
|
|
1743
|
-
ensureForgeRuntimeLaunchAgentContract({ controllerHome: config.controllerHome });
|
|
1743
|
+
ensureForgeRuntimeLaunchAgentContract({ controllerHome: config.controllerHome, installUserLaunchAgent: true });
|
|
1744
1744
|
} catch (error) {
|
|
1745
1745
|
const detail = `runtime launchd stable-release contract update failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
1746
1746
|
audit(config, 'runtime_release_activation_launchd_contract_failed', { serviceTarget: service.target, operationId, detail });
|
|
@@ -1821,7 +1821,7 @@ export async function activateRuntimeRelease(
|
|
|
1821
1821
|
physicalHome: storageMigration.physicalHome,
|
|
1822
1822
|
});
|
|
1823
1823
|
}
|
|
1824
|
-
ensureForgeRuntimeLaunchAgentContract({ controllerHome: config.controllerHome });
|
|
1824
|
+
ensureForgeRuntimeLaunchAgentContract({ controllerHome: config.controllerHome, installUserLaunchAgent: true });
|
|
1825
1825
|
const previousRevision = previousActive?.releaseId ?? '';
|
|
1826
1826
|
const previousIdentity = previousActive?.artifactIdentity;
|
|
1827
1827
|
if (
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import type { RepositoryRecord } from '../../../cli/repositories/types';
|
|
3
|
+
import { executeBrowserPluginAction } from '../../plugins/browser-adapter';
|
|
4
|
+
|
|
5
|
+
export type ScheduledBrowserProbeStatus = 'observed' | 'keepalive' | 'auth_required';
|
|
6
|
+
|
|
7
|
+
export interface ScheduledBrowserProbeResult {
|
|
8
|
+
status: ScheduledBrowserProbeStatus;
|
|
9
|
+
fingerprint?: string;
|
|
10
|
+
url?: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
projectedLineCount: number;
|
|
13
|
+
observedChars: number;
|
|
14
|
+
truncated: boolean;
|
|
15
|
+
authReason?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function stringValue(value: unknown): string | undefined {
|
|
19
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function recordValue(value: unknown): Record<string, unknown> {
|
|
23
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stringList(value: unknown): string[] {
|
|
27
|
+
return Array.isArray(value) ? value.map((entry) => String(entry).trim()).filter(Boolean) : [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function boundedNumber(value: unknown, fallback: number, min: number, max: number): number {
|
|
31
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
32
|
+
? Math.max(min, Math.min(Math.trunc(value), max))
|
|
33
|
+
: fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildScheduledBrowserFingerprint(text: string, includeTerms: string[] = [], ignorePatterns: string[] = []): { fingerprint: string; lineCount: number } {
|
|
37
|
+
const terms = includeTerms.map((term) => term.toLocaleLowerCase());
|
|
38
|
+
let lines = text
|
|
39
|
+
.split(/\r?\n/)
|
|
40
|
+
.map((line) => line.replace(/\s+/g, ' ').trim())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
if (terms.length > 0) {
|
|
43
|
+
lines = lines.filter((line) => {
|
|
44
|
+
const normalized = line.toLocaleLowerCase();
|
|
45
|
+
return terms.some((term) => normalized.includes(term));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
for (const pattern of ignorePatterns) {
|
|
49
|
+
try {
|
|
50
|
+
const expression = new RegExp(pattern, 'giu');
|
|
51
|
+
lines = lines.map((line) => line.replace(expression, ' ').replace(/\s+/g, ' ').trim()).filter(Boolean);
|
|
52
|
+
} catch {
|
|
53
|
+
throw new Error(`SCHEDULE_BROWSER_PROBE_IGNORE_PATTERN_INVALID: ${pattern}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
lines = [...new Set(lines)].sort((left, right) => left.localeCompare(right));
|
|
57
|
+
return { fingerprint: createHash('sha256').update(lines.join('\n')).digest('hex'), lineCount: lines.length };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function classifyScheduledBrowserObservation(
|
|
61
|
+
previousFingerprint: string | undefined,
|
|
62
|
+
currentFingerprint: string | undefined,
|
|
63
|
+
wakeOnFirstObservation = false,
|
|
64
|
+
): { status: 'baseline' | 'unchanged' | 'changed'; shouldWake: boolean } {
|
|
65
|
+
const firstObservation = !previousFingerprint;
|
|
66
|
+
const changed = Boolean(previousFingerprint && currentFingerprint && previousFingerprint !== currentFingerprint);
|
|
67
|
+
return {
|
|
68
|
+
status: firstObservation ? 'baseline' : changed ? 'changed' : 'unchanged',
|
|
69
|
+
shouldWake: changed || (firstObservation && wakeOnFirstObservation),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function containsAny(value: string | undefined, terms: string[]): string | undefined {
|
|
74
|
+
if (!value) return undefined;
|
|
75
|
+
const normalized = value.toLocaleLowerCase();
|
|
76
|
+
return terms.find((term) => normalized.includes(term.toLocaleLowerCase()));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function browserAction(
|
|
80
|
+
controllerHome: string,
|
|
81
|
+
repository: RepositoryRecord,
|
|
82
|
+
occurrenceId: string,
|
|
83
|
+
actionId: string,
|
|
84
|
+
args: Record<string, unknown>,
|
|
85
|
+
): Promise<Record<string, unknown>> {
|
|
86
|
+
return executeBrowserPluginAction({
|
|
87
|
+
controllerHome,
|
|
88
|
+
repoId: repository.repoId,
|
|
89
|
+
repoRoot: repository.canonicalRoot,
|
|
90
|
+
pluginId: 'browser',
|
|
91
|
+
actionId,
|
|
92
|
+
requestId: `schedule-browser-probe:${occurrenceId}:${actionId}`,
|
|
93
|
+
args,
|
|
94
|
+
origin: { surface: 'schedule', actor: 'browser-probe', correlationId: occurrenceId },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Bounded read-only browser observation for Schedule. It never interprets page
|
|
100
|
+
* meaning: it only refreshes/navigates, extracts bounded text, projects matching
|
|
101
|
+
* lines, detects configured login markers, and returns a stable fingerprint.
|
|
102
|
+
*/
|
|
103
|
+
export async function executeScheduledBrowserProbe(input: {
|
|
104
|
+
controllerHome: string;
|
|
105
|
+
repository: RepositoryRecord;
|
|
106
|
+
occurrenceId: string;
|
|
107
|
+
args: Record<string, unknown>;
|
|
108
|
+
}): Promise<ScheduledBrowserProbeResult> {
|
|
109
|
+
const requestedSessionId = stringValue(input.args.probe_session_id) ?? stringValue(input.args.session_id);
|
|
110
|
+
const url = stringValue(input.args.probe_url) ?? stringValue(input.args.url);
|
|
111
|
+
if (!requestedSessionId && !url) throw new Error('SCHEDULE_BROWSER_PROBE_TARGET_REQUIRED');
|
|
112
|
+
|
|
113
|
+
const waitUntil = stringValue(input.args.wait_until) ?? 'domcontentloaded';
|
|
114
|
+
const timeoutMs = boundedNumber(input.args.timeout_ms, 60_000, 1_000, 120_000);
|
|
115
|
+
let navigation: Record<string, unknown>;
|
|
116
|
+
if (url) {
|
|
117
|
+
navigation = await browserAction(input.controllerHome, input.repository, input.occurrenceId, 'navigate', {
|
|
118
|
+
...(requestedSessionId ? { session_id: requestedSessionId } : {}),
|
|
119
|
+
url,
|
|
120
|
+
wait_until: waitUntil,
|
|
121
|
+
timeout_ms: timeoutMs,
|
|
122
|
+
retries: 1,
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
navigation = await browserAction(input.controllerHome, input.repository, input.occurrenceId, 'reload', {
|
|
126
|
+
session_id: requestedSessionId!,
|
|
127
|
+
wait_until: waitUntil,
|
|
128
|
+
timeout_ms: timeoutMs,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const session = recordValue(navigation.session);
|
|
133
|
+
const sessionId = stringValue(navigation.sessionId) ?? stringValue(session.sessionId) ?? stringValue(session.id) ?? requestedSessionId;
|
|
134
|
+
if (!sessionId) throw new Error('SCHEDULE_BROWSER_PROBE_SESSION_UNAVAILABLE');
|
|
135
|
+
const observedUrl = stringValue(navigation.url) ?? stringValue(session.url) ?? url;
|
|
136
|
+
const loginUrlTerms = stringList(input.args.login_url_terms);
|
|
137
|
+
const urlLoginMarker = containsAny(observedUrl, loginUrlTerms);
|
|
138
|
+
|
|
139
|
+
const keepaliveOnly = input.args.keepalive_only === true;
|
|
140
|
+
const loginTextTerms = stringList(input.args.login_text_terms);
|
|
141
|
+
const mustExtractText = !keepaliveOnly || loginTextTerms.length > 0;
|
|
142
|
+
let rawText = '';
|
|
143
|
+
let truncated = false;
|
|
144
|
+
if (mustExtractText) {
|
|
145
|
+
const textResult = await browserAction(input.controllerHome, input.repository, input.occurrenceId, 'get_text', {
|
|
146
|
+
session_id: sessionId,
|
|
147
|
+
...(stringValue(input.args.selector) ? { selector: stringValue(input.args.selector) } : {}),
|
|
148
|
+
max_chars: boundedNumber(input.args.max_chars, 20_000, 256, 100_000),
|
|
149
|
+
});
|
|
150
|
+
rawText = stringValue(textResult.text) ?? '';
|
|
151
|
+
truncated = textResult.truncated === true;
|
|
152
|
+
}
|
|
153
|
+
const textLoginMarker = containsAny(rawText, loginTextTerms);
|
|
154
|
+
const authMarker = urlLoginMarker ?? textLoginMarker;
|
|
155
|
+
if (authMarker) {
|
|
156
|
+
return {
|
|
157
|
+
status: 'auth_required',
|
|
158
|
+
url: observedUrl,
|
|
159
|
+
sessionId,
|
|
160
|
+
projectedLineCount: 0,
|
|
161
|
+
observedChars: rawText.length,
|
|
162
|
+
truncated,
|
|
163
|
+
authReason: `matched configured login marker: ${authMarker}`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (keepaliveOnly) {
|
|
167
|
+
return { status: 'keepalive', url: observedUrl, sessionId, projectedLineCount: 0, observedChars: rawText.length, truncated };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const projected = buildScheduledBrowserFingerprint(rawText, stringList(input.args.include_terms), stringList(input.args.ignore_patterns));
|
|
171
|
+
return {
|
|
172
|
+
status: 'observed',
|
|
173
|
+
fingerprint: projected.fingerprint,
|
|
174
|
+
url: observedUrl,
|
|
175
|
+
sessionId,
|
|
176
|
+
projectedLineCount: projected.lineCount,
|
|
177
|
+
observedChars: rawText.length,
|
|
178
|
+
truncated,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { RepositorySchedule } from './types';
|
|
2
|
+
|
|
3
|
+
export const CHATGPT_AUTOMATION_EXECUTION_PROFILE = 'chatgpt_browser_v1';
|
|
4
|
+
export const CHATGPT_AUTOMATION_DEFAULT_MODEL = 'gpt-5.6';
|
|
5
|
+
export const CHATGPT_AUTOMATION_DEFAULT_REASONING = 'high';
|
|
6
|
+
export const CHATGPT_AUTOMATION_DEFAULT_TAB_POLICY = 'auto';
|
|
7
|
+
|
|
8
|
+
export interface ChatgptAutomationScheduleMigrationResult {
|
|
9
|
+
changed: boolean;
|
|
10
|
+
schedule: RepositorySchedule;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
14
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function migrateChatgptAutomationSchedule(schedule: RepositorySchedule): ChatgptAutomationScheduleMigrationResult {
|
|
18
|
+
if (schedule.action.operation !== 'external_controller_wake') return { changed: false, schedule };
|
|
19
|
+
const current = schedule.action.arguments ?? {};
|
|
20
|
+
const controllerType = nonEmptyString(current.controller_type) ?? 'chatgpt';
|
|
21
|
+
if (controllerType !== 'chatgpt') return { changed: false, schedule };
|
|
22
|
+
|
|
23
|
+
const argumentsNext: Record<string, unknown> = {
|
|
24
|
+
...current,
|
|
25
|
+
controller_type: controllerType,
|
|
26
|
+
model: nonEmptyString(current.model) ?? CHATGPT_AUTOMATION_DEFAULT_MODEL,
|
|
27
|
+
reasoning: nonEmptyString(current.reasoning) ?? CHATGPT_AUTOMATION_DEFAULT_REASONING,
|
|
28
|
+
tab_policy: nonEmptyString(current.tab_policy) ?? CHATGPT_AUTOMATION_DEFAULT_TAB_POLICY,
|
|
29
|
+
execution_profile: nonEmptyString(current.execution_profile) ?? CHATGPT_AUTOMATION_EXECUTION_PROFILE,
|
|
30
|
+
};
|
|
31
|
+
const changed = Object.entries(argumentsNext).some(([key, value]) => current[key] !== value);
|
|
32
|
+
if (!changed) return { changed: false, schedule };
|
|
33
|
+
return {
|
|
34
|
+
changed: true,
|
|
35
|
+
schedule: {
|
|
36
|
+
...schedule,
|
|
37
|
+
action: { ...schedule.action, arguments: argumentsNext },
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|