@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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import { basename } from 'path';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'fs';
|
|
3
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from 'path';
|
|
4
4
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
5
5
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
6
|
+
import type { CallToolResult as SdkCallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
6
7
|
import { collectRuntimePerformanceDiagnostics, inferLocalControllerProcess } from '../../diagnostics/performance';
|
|
7
8
|
import type { McpToolDefinition, CallToolResult } from '../../../cli/mcp/tools';
|
|
8
9
|
import type { MultiRepositoryMcpToolContext } from '../../../cli/mcp/multi-repository';
|
|
@@ -14,11 +15,11 @@ import { repositoryControllerRoot } from '../../../cli/repositories/controller-h
|
|
|
14
15
|
import { cancelExecutionJob, findExecutionJob, getExecutionJob, getExecutionJobByRequestId, listExecutionJobs } from '../../execution/jobs/store';
|
|
15
16
|
import { waitForExecutionJob } from '../../execution/jobs/wait';
|
|
16
17
|
import type { ExecutionJob } from '../../execution/jobs/types';
|
|
17
|
-
import { getProcessHandle, getProcessRecord, processCheckCompletionReceipt, processRuntimeResourceDiagnostics, readPersistedCheckResultReceipt, runPersistedCheckViaProcessRuntime, waitForProcess } from '../../execution/process-runtime';
|
|
18
|
+
import { getProcessHandle, getProcessRecord, isManagedProcessActive, listProcessRecords, processCheckCompletionReceipt, processRuntimeResourceDiagnostics, readPersistedCheckResultReceipt, runPersistedCheckViaProcessRuntime, waitForProcess } from '../../execution/process-runtime';
|
|
18
19
|
import { buildJobOperationDigest } from '../../control-plane/facade/operation-digest';
|
|
19
|
-
import { readWorkHandle, type WorkHandleState } from '../../control-plane/execution/work-handle-store';
|
|
20
|
+
import { readWorkHandle, transitionWorkHandle, writeWorkHandle, type WorkHandleState } from '../../control-plane/execution/work-handle-store';
|
|
20
21
|
import { executionIdentityForRepository } from '../../control-plane/execution/execution-identity';
|
|
21
|
-
import { reconcileFinalizedDirectEditWorksAfterCommit } from '../../control-plane/execution/direct-edit-work-completion';
|
|
22
|
+
import { acceptReviewedDirectEditWorkReconciliation, reconcileFinalizedDirectEditWorksAfterCommit } from '../../control-plane/execution/direct-edit-work-completion';
|
|
22
23
|
import { readJobEvents } from '../../evidence/event-ledger';
|
|
23
24
|
import { readExecutionArtifact } from '../../evidence/artifact-store';
|
|
24
25
|
import { readExecutionEvidence } from '../../evidence/evidence-store';
|
|
@@ -44,11 +45,6 @@ import { applyScheduleDedupe, buildScheduleDedupeReport, createSchedule, getSche
|
|
|
44
45
|
import { evaluateSchedule } from '../../workflow/schedules/engine';
|
|
45
46
|
import {
|
|
46
47
|
createWorkContinuationSchedule,
|
|
47
|
-
getWorkContinuationSchedule,
|
|
48
|
-
listWorkContinuationSchedules,
|
|
49
|
-
pauseWorkContinuationSchedule,
|
|
50
|
-
resumeWorkContinuationSchedule,
|
|
51
|
-
triggerWorkContinuationSchedule,
|
|
52
48
|
type ContinuationControllerType,
|
|
53
49
|
} from '../../workflow/schedules/work-continuation';
|
|
54
50
|
import { createPortfolioWorkflow, getPortfolioWorkflow, listPortfolioWorkflows } from '../../workflow/portfolio/store';
|
|
@@ -109,10 +105,6 @@ import { resolveLocalBridgeSurface, summarizeRecentJobs } from '../../shared/loc
|
|
|
109
105
|
import { assistantPluginScope, controllerPluginRepository, executeAssistantPluginReadDirect, getAssistantPluginManifest, isDirectPluginReadAction, listAssistantPluginManifests, submitAssistantPluginAction } from '../../plugins/store';
|
|
110
106
|
import { mcpPluginExecutionOrigin } from '../../plugins/execution-origin';
|
|
111
107
|
import {
|
|
112
|
-
listWebTargets,
|
|
113
|
-
mergeAllowedDomains,
|
|
114
|
-
previewBrowserDomainAccess,
|
|
115
|
-
resolveWebTargetUrl,
|
|
116
108
|
summarizeExecutionJobForMcp,
|
|
117
109
|
summarizeJobResultForLowInterception,
|
|
118
110
|
summarizePluginForLowInterception,
|
|
@@ -205,7 +197,9 @@ import {
|
|
|
205
197
|
releaseControllerSession,
|
|
206
198
|
resumeControllerSession,
|
|
207
199
|
} from '../../control-plane/facade';
|
|
208
|
-
import { currentControllerInstanceId, startExecutionSession, updateExecutionSession } from '../../control-plane/execution/session-store';
|
|
200
|
+
import { currentControllerInstanceId, readExecutionSession, startExecutionSession, updateExecutionSession } from '../../control-plane/execution/session-store';
|
|
201
|
+
import { ensureManagedWorkspace } from '../../execution/managed-workspace';
|
|
202
|
+
import { currentPermissionSnapshotVersion } from '../../control-plane/execution/validation';
|
|
209
203
|
import { observeRuntimeStatus } from '../../root/status';
|
|
210
204
|
import { reconcileWorkValidation } from './work-validation-reconciler';
|
|
211
205
|
import { callExecutionTool } from './execution-tools';
|
|
@@ -296,9 +290,15 @@ export const runtimeToolDefinitions: McpToolDefinition[] = [
|
|
|
296
290
|
}),
|
|
297
291
|
definition('rh_work', 'Preferred ChatGPT facade: bounded planning, direct control, controller ownership, and external SuperController launch.', {
|
|
298
292
|
repo_id: repoId,
|
|
299
|
-
operation: { type: 'string', enum: ['start', 'continue', 'verify', 'repair', 'finalize', 'stop', 'delegate', 'controller_claim', 'controller_release', 'controller_get_owner', 'launcher_start', 'plan_create', 'plan_get', 'plan_list', 'plan_approve', 'plan_supersede', 'schedule_create', 'schedule_list', 'schedule_get', 'schedule_pause', 'schedule_resume', 'schedule_trigger'], description: 'Defaults to start. Complex starts require plan_id and plan_step_id. schedule_* manages
|
|
293
|
+
operation: { type: 'string', enum: ['start', 'continue', 'verify', 'repair', 'finalize', 'stop', 'delegate', 'controller_claim', 'controller_release', 'controller_get_owner', 'launcher_start', 'plan_create', 'plan_get', 'plan_list', 'plan_approve', 'plan_supersede', 'schedule_create', 'schedule_list', 'schedule_get', 'schedule_pause', 'schedule_resume', 'schedule_trigger'], description: 'Defaults to start. Complex starts require plan_id and plan_step_id. schedule_* manages generic recurring ChatGPT workflow schedules plus Work-bound continuation/browser subtypes.' },
|
|
300
294
|
objective: { type: 'string' },
|
|
301
295
|
work_id: { type: 'string' },
|
|
296
|
+
related_work_id: { type: 'string', description: 'Optional explicit existing Work ownership target for continue/extend semantics. Unrelated active Work is never inferred as a semantic target.' },
|
|
297
|
+
work_relation: { type: 'string', enum: ['continue', 'extend', 'parallel', 'new_goal'], description: 'Optional semantic relation to an explicitly related Work. This field never selects Direct/Process/Durable execution depth; checkout conflicts are resolved by placement.' },
|
|
298
|
+
related_plan_id: { type: 'string', description: 'Existing active Plan selected when resolving another slice under the same Requirement.' },
|
|
299
|
+
plan_relation: { type: 'string', enum: ['extend', 'parallel'], description: 'Relationship to another active Plan under the same Requirement. Omit to receive candidates first. parallel explicitly permits a distinct Plan slice; extend reuses the existing Plan authority and requires replanning/supersession instead of creating another draft.' },
|
|
300
|
+
requested_by: { type: 'string', enum: ['chatgpt', 'user', 'system', 'scheduler'], description: 'Origin of the request. Scheduler-origin starts are continuation-only and cannot create new durable Work.' },
|
|
301
|
+
requirement_id: { type: 'string', description: 'Stable Requirement authority used to find related active Work before creation.' },
|
|
302
302
|
handoff_id: { type: 'string', description: 'Optional existing HandoffItem used to resume controller context.' },
|
|
303
303
|
controller_id: { type: 'string', description: 'Stable external SuperController identity for controller_claim/release.' },
|
|
304
304
|
controller_type: { type: 'string', enum: ['chatgpt', 'codex', 'grok', 'claude', 'human'], description: 'Controller kind for claims and launcher_start.' },
|
|
@@ -309,10 +309,23 @@ export const runtimeToolDefinitions: McpToolDefinition[] = [
|
|
|
309
309
|
launch_args: { type: 'array', items: { type: 'string' }, description: 'Provider-specific arguments for launcher_start.' },
|
|
310
310
|
browser_session_id: { type: 'string', description: 'Saved Forge ChatGPT browser session to continue when controller_type=chatgpt.' },
|
|
311
311
|
conversation_url: { type: 'string', description: 'Explicit https://chatgpt.com conversation URL used when no saved Forge browser session exists.' },
|
|
312
|
-
continuation_prompt: { type: 'string', description: '
|
|
312
|
+
continuation_prompt: { type: 'string', description: 'Bounded Work continuation instruction, or the recurring ChatGPT prompt when schedule_mode=workflow and objective is omitted.' },
|
|
313
313
|
schedule_id: { type: 'string', description: 'Schedule identity for schedule_get/pause/resume/trigger.' },
|
|
314
|
-
schedule_name: { type: 'string', description: 'Human-readable schedule name;
|
|
314
|
+
schedule_name: { type: 'string', description: 'Human-readable schedule name; generic workflow schedules use the objective/prompt when omitted.' },
|
|
315
315
|
schedule_request_id: { type: 'string', description: 'Stable idempotency key for schedule_create.' },
|
|
316
|
+
schedule_mode: { type: 'string', enum: ['workflow', 'continuation', 'browser_watch', 'browser_keepalive'], description: 'Defaults to workflow when work_id is omitted and continuation when work_id is present. workflow dispatches one bounded ChatGPT prompt per finite Occurrence without requiring Work; continuation/browser modes remain Work-bound.' },
|
|
317
|
+
probe_url: { type: 'string', description: 'Allowed URL to navigate or refresh for browser_watch/browser_keepalive.' },
|
|
318
|
+
probe_browser_session_id: { type: 'string', description: 'Existing Forge browser session used as the external observation target.' },
|
|
319
|
+
probe_selector: { type: 'string', description: 'Optional selector limiting text extraction for browser_watch.' },
|
|
320
|
+
probe_max_chars: { type: 'number', description: 'Bounded extracted text size for browser_watch.' },
|
|
321
|
+
probe_timeout_ms: { type: 'number', description: 'Bounded browser probe timeout.' },
|
|
322
|
+
include_terms: { type: 'array', items: { type: 'string' }, description: 'Optional case-insensitive terms; only matching normalized lines contribute to the observation fingerprint.' },
|
|
323
|
+
ignore_patterns: { type: 'array', items: { type: 'string' }, description: 'Optional regex patterns stripped before fingerprinting volatile text such as relative timestamps.' },
|
|
324
|
+
login_url_terms: { type: 'array', items: { type: 'string' }, description: 'Configured URL markers that classify the browser observation as authentication-required.' },
|
|
325
|
+
login_text_terms: { type: 'array', items: { type: 'string' }, description: 'Configured page-text markers that classify the browser observation as authentication-required.' },
|
|
326
|
+
wake_on_first_observation: { type: 'boolean', description: 'Defaults false so the first browser_watch occurrence records a silent baseline.' },
|
|
327
|
+
wake_on_auth_required: { type: 'boolean', description: 'Defaults true for Work-bound browser schedules.' },
|
|
328
|
+
auth_required_prompt: { type: 'string', description: 'Optional ChatGPT continuation instruction when the probe detects a login marker.' },
|
|
316
329
|
trigger_type: { type: 'string', enum: ['interval', 'cron', 'calendar', 'condition', 'repository-event', 'dependency-checkpoint', 'manual'] },
|
|
317
330
|
every_minutes: { type: 'number', description: 'Interval trigger cadence in minutes.' },
|
|
318
331
|
cron_expression: { type: 'string' },
|
|
@@ -383,12 +396,18 @@ export const runtimeToolDefinitions: McpToolDefinition[] = [
|
|
|
383
396
|
no_ff: { type: 'boolean' },
|
|
384
397
|
completion_outcome: { type: 'string', enum: ['completed_changed', 'completed_no_change'] },
|
|
385
398
|
no_change_evidence: { type: 'string', description: 'Optional explicit proof for a no-change completion; otherwise the lifecycle coordinator derives proof only for an unchanged validated HEAD.' },
|
|
399
|
+
reconcile_historical_delivery: { type: 'boolean', description: 'Explicitly request reviewed reconciliation for a historically delivered Direct Edit Work whose original Work binding/receipt is missing.' },
|
|
400
|
+
reconcile_target_revision: { type: 'string', description: 'Exact already-integrated commit accepted by the reviewer.' },
|
|
401
|
+
reconcile_compared_paths: { type: 'array', items: { type: 'string' }, description: 'Complete exact path set changed from the Work base revision to reconcile_target_revision.' },
|
|
402
|
+
reconcile_rationale: { type: 'string', description: 'Bounded reviewer rationale for accepting the historical delivery.' },
|
|
403
|
+
reconcile_cleanup_proof: { type: 'string', description: 'Concrete proof that no owned cleanup remains or that cleanup is complete.' },
|
|
386
404
|
detail_level: { type: 'string', enum: ['summary', 'detail'] },
|
|
387
405
|
reason: { type: 'string', description: 'Bounded reason for a work stop or repair record.' },
|
|
388
406
|
}, [], false),
|
|
389
407
|
definition('work_submit', 'Submit one durable repository operation and return a resumable Work handle.', {
|
|
390
408
|
repo_id: repoId,
|
|
391
|
-
request_id: { type: 'string', description: 'Stable idempotency key used to resume the same Work after reconnecting.' },
|
|
409
|
+
request_id: { type: 'string', description: 'Stable idempotency key used to resume the same execution-child Work after reconnecting.' },
|
|
410
|
+
parent_work_id: { type: 'string', description: 'Optional active primary Work that owns this low-level execution child. work_submit never creates a second business-workflow authority.' },
|
|
392
411
|
operation: { type: 'string', description: 'Existing durable controller operation, for example run_check, dispatch_task, or create_issue.' },
|
|
393
412
|
arguments: { type: 'object', description: 'Arguments passed to the durable operation.' },
|
|
394
413
|
timeout_ms: { type: 'number' },
|
|
@@ -718,6 +737,7 @@ export const runtimeToolDefinitions: McpToolDefinition[] = [
|
|
|
718
737
|
plugin_id: { type: 'string' },
|
|
719
738
|
action_id: { type: 'string' },
|
|
720
739
|
request_id: { type: 'string' },
|
|
740
|
+
work_id: { type: 'string', description: 'Optional existing remote-effect Work that should own the successful typed plugin receipt.' },
|
|
721
741
|
arguments: { type: 'object' },
|
|
722
742
|
timeout_ms: { type: 'number' },
|
|
723
743
|
confirm_authorization: { type: 'boolean' },
|
|
@@ -727,34 +747,6 @@ export const runtimeToolDefinitions: McpToolDefinition[] = [
|
|
|
727
747
|
repo_id: repoId,
|
|
728
748
|
plugin_id: { type: 'string' },
|
|
729
749
|
}, ['plugin_id']),
|
|
730
|
-
definition('web_targets_list', 'List pre-allowed web targets as domain keys. This avoids arbitrary URL parameters.', {
|
|
731
|
-
repo_id: repoId,
|
|
732
|
-
}),
|
|
733
|
-
definition('web_target_snapshot', 'Create a read-only browser snapshot for a pre-allowed web target by target_key and path. Does not accept arbitrary URLs.', {
|
|
734
|
-
repo_id: repoId,
|
|
735
|
-
target_key: { type: 'string' },
|
|
736
|
-
path: { type: 'string' },
|
|
737
|
-
query: { type: 'object' },
|
|
738
|
-
capture: { type: 'string', enum: ['title', 'text', 'screenshot'] },
|
|
739
|
-
wait_until: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle'] },
|
|
740
|
-
max_chars: { type: 'number' },
|
|
741
|
-
full_page: { type: 'boolean' },
|
|
742
|
-
request_id: { type: 'string' },
|
|
743
|
-
timeout_ms: { type: 'number' },
|
|
744
|
-
}, ['target_key', 'request_id'], false),
|
|
745
|
-
definition('web_domain_access_preview', 'Preview a browser domain access request without writing plugin config or accepting arbitrary URLs.', {
|
|
746
|
-
repo_id: repoId,
|
|
747
|
-
domain: { type: 'string' },
|
|
748
|
-
reason: { type: 'string' },
|
|
749
|
-
}, ['domain']),
|
|
750
|
-
definition('web_domain_access_apply', 'Apply a previously previewed browser domain access request through browser configuration with explicit authorization.', {
|
|
751
|
-
repo_id: repoId,
|
|
752
|
-
domain: { type: 'string' },
|
|
753
|
-
reason: { type: 'string' },
|
|
754
|
-
preview_ticket_id: { type: 'string' },
|
|
755
|
-
request_id: { type: 'string' },
|
|
756
|
-
confirm_authorization: { type: 'boolean' },
|
|
757
|
-
}, ['domain', 'request_id', 'confirm_authorization'], false),
|
|
758
750
|
definition('work_result_summary', 'Return a redacted job result summary with failure class and suggested next actions. Does not return raw stdout or stderr.', {
|
|
759
751
|
repo_id: repoId,
|
|
760
752
|
job_id: { type: 'string' },
|
|
@@ -1123,6 +1115,77 @@ function result(value: Record<string, unknown>, isError = false): CallToolResult
|
|
|
1123
1115
|
return { content: [{ type: 'text', text: JSON.stringify(value) }], structuredContent: value, ...(isError ? { isError: true } : {}) };
|
|
1124
1116
|
}
|
|
1125
1117
|
|
|
1118
|
+
const MAX_INLINE_PLUGIN_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
1119
|
+
const MAX_INLINE_PLUGIN_IMAGES = 4;
|
|
1120
|
+
const INLINE_PLUGIN_IMAGE_MEDIA_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
|
|
1121
|
+
|
|
1122
|
+
type McpImageContent = Extract<SdkCallToolResult['content'][number], { type: 'image' }>;
|
|
1123
|
+
|
|
1124
|
+
export function boundedPluginArtifactImageContent(
|
|
1125
|
+
controllerHome: string,
|
|
1126
|
+
repoId: string,
|
|
1127
|
+
pluginResult: Record<string, unknown> | undefined,
|
|
1128
|
+
): McpImageContent[] {
|
|
1129
|
+
if (!pluginResult) return [];
|
|
1130
|
+
const nestedResult = pluginResult.result && typeof pluginResult.result === 'object' && !Array.isArray(pluginResult.result)
|
|
1131
|
+
? pluginResult.result as Record<string, unknown>
|
|
1132
|
+
: undefined;
|
|
1133
|
+
const candidates = [
|
|
1134
|
+
...(Array.isArray(pluginResult.artifactCandidates) ? pluginResult.artifactCandidates : []),
|
|
1135
|
+
...(nestedResult && Array.isArray(nestedResult.artifactCandidates) ? nestedResult.artifactCandidates : []),
|
|
1136
|
+
];
|
|
1137
|
+
if (candidates.length === 0) return [];
|
|
1138
|
+
const allowedRoot = resolve(repositoryControllerRoot(controllerHome, repoId));
|
|
1139
|
+
const images: McpImageContent[] = [];
|
|
1140
|
+
const seenPaths = new Set<string>();
|
|
1141
|
+
|
|
1142
|
+
for (const candidate of candidates) {
|
|
1143
|
+
if (images.length >= MAX_INLINE_PLUGIN_IMAGES) break;
|
|
1144
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) continue;
|
|
1145
|
+
const record = candidate as Record<string, unknown>;
|
|
1146
|
+
const mediaType = typeof record.mediaType === 'string' ? record.mediaType : '';
|
|
1147
|
+
const path = typeof record.path === 'string' ? record.path : '';
|
|
1148
|
+
if (!INLINE_PLUGIN_IMAGE_MEDIA_TYPES.has(mediaType) || !path) continue;
|
|
1149
|
+
|
|
1150
|
+
const resolvedPath = resolve(path);
|
|
1151
|
+
if (seenPaths.has(resolvedPath)) continue;
|
|
1152
|
+
const rel = relative(allowedRoot, resolvedPath);
|
|
1153
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) continue;
|
|
1154
|
+
if (!existsSync(resolvedPath)) continue;
|
|
1155
|
+
|
|
1156
|
+
try {
|
|
1157
|
+
const stat = statSync(resolvedPath);
|
|
1158
|
+
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_INLINE_PLUGIN_IMAGE_BYTES) continue;
|
|
1159
|
+
images.push({
|
|
1160
|
+
type: 'image',
|
|
1161
|
+
data: readFileSync(resolvedPath).toString('base64'),
|
|
1162
|
+
mimeType: mediaType,
|
|
1163
|
+
});
|
|
1164
|
+
seenPaths.add(resolvedPath);
|
|
1165
|
+
} catch {
|
|
1166
|
+
// The structured plugin result remains authoritative when an artifact cannot be inlined.
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
return images;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function resultWithPluginArtifactImages(
|
|
1174
|
+
value: Record<string, unknown>,
|
|
1175
|
+
controllerHome: string,
|
|
1176
|
+
repoId: string,
|
|
1177
|
+
pluginResult: Record<string, unknown> | undefined,
|
|
1178
|
+
): CallToolResult {
|
|
1179
|
+
const images = boundedPluginArtifactImageContent(controllerHome, repoId, pluginResult);
|
|
1180
|
+
const richResult: SdkCallToolResult = {
|
|
1181
|
+
content: [{ type: 'text', text: JSON.stringify(value) }, ...images],
|
|
1182
|
+
structuredContent: value,
|
|
1183
|
+
};
|
|
1184
|
+
// The legacy internal facade intentionally types content as text-first for existing callers.
|
|
1185
|
+
// The MCP wire contract supports additional SDK image blocks after that stable first text block.
|
|
1186
|
+
return richResult as unknown as CallToolResult;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1126
1189
|
function repositoryRootForRepoId(controllerHome: string, repoId: string): string | undefined {
|
|
1127
1190
|
return listRepositories(controllerHome).find((repository) => repository.repoId === repoId)?.canonicalRoot;
|
|
1128
1191
|
}
|
|
@@ -1543,6 +1606,64 @@ function authenticatedFacadeControllerIdentity(
|
|
|
1543
1606
|
};
|
|
1544
1607
|
}
|
|
1545
1608
|
|
|
1609
|
+
function ensureFacadeWorkHandle(
|
|
1610
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
1611
|
+
repository: ReturnType<typeof selected>,
|
|
1612
|
+
workId: string,
|
|
1613
|
+
args: Record<string, unknown>,
|
|
1614
|
+
): WorkHandleState | undefined {
|
|
1615
|
+
const existing = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
|
|
1616
|
+
if (existing) return existing;
|
|
1617
|
+
const contract = getWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, workId);
|
|
1618
|
+
if (!contract || contract.workKind !== 'repository_change' || contract.mode !== 'goal_workloop' || !contract.checkoutId) return undefined;
|
|
1619
|
+
const executionRepository = resolveRepositorySelection({ repoId: repository.repoId, checkoutId: contract.checkoutId, controllerHome: ctx.controllerHome, allowSoleRepository: false });
|
|
1620
|
+
const checkout = selectRepositoryCheckout(executionRepository, contract.checkoutId, { allowArchived: true });
|
|
1621
|
+
const status = repositoryGitStatus(checkout);
|
|
1622
|
+
if (!status.branch) throw new Error(`WORKTREE_DETACHED: ${contract.checkoutId} has no branch`);
|
|
1623
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
1624
|
+
const at = new Date().toISOString();
|
|
1625
|
+
const managedWorktree = Boolean(contract.worktreeRef) && resolve(contract.worktreeRef!) === resolve(checkout.canonicalRoot) && resolve(checkout.canonicalRoot) !== resolve(repository.canonicalRoot);
|
|
1626
|
+
return writeWorkHandle(ctx.controllerHome, {
|
|
1627
|
+
schemaVersion: 1,
|
|
1628
|
+
workId,
|
|
1629
|
+
sessionId: identity.sessionId,
|
|
1630
|
+
principalId: identity.principalId,
|
|
1631
|
+
repositoryId: repository.repoId,
|
|
1632
|
+
checkoutId: contract.checkoutId,
|
|
1633
|
+
worktreePath: checkout.canonicalRoot,
|
|
1634
|
+
branch: status.branch,
|
|
1635
|
+
sourceCheckoutId: repository.activeCheckoutId,
|
|
1636
|
+
managedWorktree,
|
|
1637
|
+
workContractId: contract.workId,
|
|
1638
|
+
baseCommit: contract.baseRevision ?? status.head ?? undefined,
|
|
1639
|
+
expectedHead: status.head ?? contract.baseRevision,
|
|
1640
|
+
permissionSnapshotVersion: currentPermissionSnapshotVersion(ctx.controllerHome, repository.repoId),
|
|
1641
|
+
state: 'prepared',
|
|
1642
|
+
createdAt: at,
|
|
1643
|
+
updatedAt: at,
|
|
1644
|
+
finalization: { validation: 'pending', commit: 'pending', merge: 'pending', branchCleanup: 'pending', worktreeCleanup: 'pending' },
|
|
1645
|
+
cleanupResponsibility: { owner: 'work_finalizer', registeredAt: at },
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function claimNewFacadeWork(
|
|
1650
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
1651
|
+
repository: ReturnType<typeof selected>,
|
|
1652
|
+
workId: string,
|
|
1653
|
+
args: Record<string, unknown>,
|
|
1654
|
+
) {
|
|
1655
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
1656
|
+
return resumeControllerSession({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, {
|
|
1657
|
+
workId,
|
|
1658
|
+
controllerId: identity.controllerId,
|
|
1659
|
+
controllerType: identity.controllerType,
|
|
1660
|
+
sessionId: identity.sessionId,
|
|
1661
|
+
principalId: identity.principalId,
|
|
1662
|
+
controllerInstanceId: identity.controllerInstanceId,
|
|
1663
|
+
leaseMs: typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1546
1667
|
function bindFacadeExecutionSession(
|
|
1547
1668
|
ctx: MultiRepositoryMcpToolContext,
|
|
1548
1669
|
repository: ReturnType<typeof selected>,
|
|
@@ -1623,14 +1744,7 @@ async function finalizeFacadeWorkHandle(
|
|
|
1623
1744
|
...(noChangeEvidence ? { no_change_evidence: noChangeEvidence } : {}),
|
|
1624
1745
|
};
|
|
1625
1746
|
|
|
1626
|
-
|
|
1627
|
-
let payload = contextRecord(physical?.structuredContent);
|
|
1628
|
-
if (
|
|
1629
|
-
physical
|
|
1630
|
-
&& physical.isError !== true
|
|
1631
|
-
&& typeof payload.continuation === 'string'
|
|
1632
|
-
&& payload.continuation.startsWith('WORK_COMMITTED_REVALIDATION_REQUIRED')
|
|
1633
|
-
) {
|
|
1747
|
+
const validateExactWorkspace = async (): Promise<CallToolResult | undefined> => {
|
|
1634
1748
|
const contract = getWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, workId);
|
|
1635
1749
|
const validation = await callExecutionTool(ctx, 'work_validate', {
|
|
1636
1750
|
session_id: session.sessionId,
|
|
@@ -1640,13 +1754,38 @@ async function finalizeFacadeWorkHandle(
|
|
|
1640
1754
|
});
|
|
1641
1755
|
if (!validation || validation.isError === true) return validation;
|
|
1642
1756
|
const validationPayload = contextRecord(validation.structuredContent);
|
|
1643
|
-
|
|
1757
|
+
return contextRecord(validationPayload.validation).passed === true ? undefined : validation;
|
|
1758
|
+
};
|
|
1759
|
+
|
|
1760
|
+
let physical = await callExecutionTool(ctx, 'work_finalize', finalizeArgs);
|
|
1761
|
+
let payload = contextRecord(physical?.structuredContent);
|
|
1762
|
+
if (physical?.isError === true && contextRecord(payload.error).code === 'WORK_VALIDATION_REQUIRED') {
|
|
1763
|
+
const validationFailure = await validateExactWorkspace();
|
|
1764
|
+
if (validationFailure) return validationFailure;
|
|
1644
1765
|
physical = await callExecutionTool(ctx, 'work_finalize', finalizeArgs);
|
|
1645
1766
|
payload = contextRecord(physical?.structuredContent);
|
|
1646
1767
|
}
|
|
1768
|
+
if (
|
|
1769
|
+
physical
|
|
1770
|
+
&& physical.isError !== true
|
|
1771
|
+
&& typeof payload.continuation === 'string'
|
|
1772
|
+
&& payload.continuation.startsWith('WORK_COMMITTED_REVALIDATION_REQUIRED')
|
|
1773
|
+
) {
|
|
1774
|
+
const validationFailure = await validateExactWorkspace();
|
|
1775
|
+
if (validationFailure) return validationFailure;
|
|
1776
|
+
physical = await callExecutionTool(ctx, 'work_finalize', finalizeArgs);
|
|
1777
|
+
}
|
|
1647
1778
|
return physical;
|
|
1648
1779
|
}
|
|
1649
1780
|
|
|
1781
|
+
function structuralIndexRoot(repository: ReturnType<typeof resolveRepositorySelection>): string | undefined {
|
|
1782
|
+
if (existsSync(join(repository.canonicalRoot, '.codegraph', 'codegraph.db'))) return repository.canonicalRoot;
|
|
1783
|
+
return repository.checkouts
|
|
1784
|
+
.filter((checkout) => checkout.checkoutId !== repository.activeCheckoutId && checkout.worktree !== true)
|
|
1785
|
+
.map((checkout) => checkout.canonicalRoot)
|
|
1786
|
+
.find((root) => existsSync(join(root, '.codegraph', 'codegraph.db')));
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1650
1789
|
function selected(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>) {
|
|
1651
1790
|
return resolveRepositorySelection({
|
|
1652
1791
|
repoId: typeof args.repo_id === 'string' ? args.repo_id : undefined,
|
|
@@ -2569,15 +2708,18 @@ async function runFacadeVerify(
|
|
|
2569
2708
|
const requestId = typeof args.request_id === 'string' && args.request_id.trim()
|
|
2570
2709
|
? args.request_id.trim()
|
|
2571
2710
|
: undefined;
|
|
2572
|
-
const observedGitHead = repositoryGitStatus(repository).head;
|
|
2573
2711
|
const workContract = workId ? getWorkContract(store, workId) : undefined;
|
|
2574
2712
|
if (workId && !workContract) throw new Error(`WORK_NOT_FOUND: ${workId}`);
|
|
2713
|
+
const verificationRepository = workContract?.checkoutId
|
|
2714
|
+
? selectRepositoryCheckout(repository, workContract.checkoutId, { allowArchived: true })
|
|
2715
|
+
: repository;
|
|
2716
|
+
const observedGitHead = repositoryGitStatus(verificationRepository).head;
|
|
2575
2717
|
const executed = await runPersistedCheckViaProcessRuntime({
|
|
2576
2718
|
controllerHome: ctx.controllerHome,
|
|
2577
|
-
repoId:
|
|
2578
|
-
checkoutId:
|
|
2579
|
-
repoRoot:
|
|
2580
|
-
executionIdentity: executionIdentityForRepository(
|
|
2719
|
+
repoId: verificationRepository.repoId,
|
|
2720
|
+
checkoutId: verificationRepository.activeCheckoutId,
|
|
2721
|
+
repoRoot: verificationRepository.canonicalRoot,
|
|
2722
|
+
executionIdentity: executionIdentityForRepository(verificationRepository, workId ? { workId } : {}),
|
|
2581
2723
|
checkId: normalizedCheckId,
|
|
2582
2724
|
timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
2583
2725
|
// rh_work.verify is a control-plane continuation primitive: callers should
|
|
@@ -2620,7 +2762,7 @@ async function runFacadeVerify(
|
|
|
2620
2762
|
|
|
2621
2763
|
const handle = executed.process;
|
|
2622
2764
|
if (!handle) throw new Error(`PROCESS_CHECK_HANDLE_MISSING: ${normalizedCheckId}`);
|
|
2623
|
-
const record = getProcessRecord(ctx.controllerHome,
|
|
2765
|
+
const record = getProcessRecord(ctx.controllerHome, verificationRepository.repoId, handle.processId);
|
|
2624
2766
|
const checkContentRevision = record?.checkExecution?.revision;
|
|
2625
2767
|
|
|
2626
2768
|
if (!handle.completed) {
|
|
@@ -2651,11 +2793,11 @@ async function runFacadeVerify(
|
|
|
2651
2793
|
|
|
2652
2794
|
if (!record) throw new Error(`PROCESS_CHECK_RECORD_MISSING: ${handle.processId}`);
|
|
2653
2795
|
const receipt = processCheckCompletionReceipt(record, {
|
|
2654
|
-
repoId:
|
|
2796
|
+
repoId: verificationRepository.repoId,
|
|
2655
2797
|
checkId: normalizedCheckId,
|
|
2656
2798
|
processId: handle.processId,
|
|
2657
2799
|
...(record.checkExecution ? {
|
|
2658
|
-
checkoutId:
|
|
2800
|
+
checkoutId: verificationRepository.activeCheckoutId,
|
|
2659
2801
|
workId: workId || undefined,
|
|
2660
2802
|
requestId,
|
|
2661
2803
|
checkExecution: {
|
|
@@ -2677,7 +2819,7 @@ async function runFacadeVerify(
|
|
|
2677
2819
|
);
|
|
2678
2820
|
const legacyEvidence = record.origin?.checkResultReceiptPath
|
|
2679
2821
|
? undefined
|
|
2680
|
-
: readLatestControllerCheckEvidence(
|
|
2822
|
+
: readLatestControllerCheckEvidence(verificationRepository.canonicalRoot, normalizedCheckId);
|
|
2681
2823
|
const legacyEvidenceMatchesProcess = Boolean(
|
|
2682
2824
|
legacyEvidence?.cacheKey
|
|
2683
2825
|
&& record.checkExecution?.cacheKey
|
|
@@ -2922,7 +3064,14 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
2922
3064
|
});
|
|
2923
3065
|
const capabilities = listCapabilityDescriptors(manifests);
|
|
2924
3066
|
const pendingHandoffs = listHandoffItems({ ...store, status: 'pending', limit: 20 });
|
|
2925
|
-
const
|
|
3067
|
+
const activeContracts = listWorkContracts({ ...store, status: 'active', limit: 200 });
|
|
3068
|
+
const activePrimaryWork = activeContracts.filter((contract) => (contract.lifecycleRole ?? 'primary') === 'primary');
|
|
3069
|
+
const activeExecutionChildren = activeContracts.filter((contract) => contract.lifecycleRole === 'execution_child');
|
|
3070
|
+
const activeRuntimeInstanceId = currentControllerInstanceId();
|
|
3071
|
+
const activeProcessRecords = listProcessRecords(ctx.controllerHome, repository.repoId, 500).filter((process) => isManagedProcessActive(process) && (!process.runtimeInstanceId || process.runtimeInstanceId === activeRuntimeInstanceId));
|
|
3072
|
+
const activeProcessWorkIds = new Set(activeProcessRecords.map((process) => process.workId).filter((workId): workId is string => Boolean(workId)));
|
|
3073
|
+
const activeControllerWorkIds = new Set(activePrimaryWork.filter((contract) => Boolean(getControllerSession({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, contract.workId))).map((contract) => contract.workId));
|
|
3074
|
+
const executingPrimaryWorkIds = new Set([...activeProcessWorkIds, ...activeControllerWorkIds].filter((workId) => activePrimaryWork.some((contract) => contract.workId === workId)));
|
|
2926
3075
|
const preferredFacadeTools = ['rh_access', 'rh_status', 'rh_inbox', 'rh_context', 'rh_work'] as const;
|
|
2927
3076
|
const facade = buildFacadeResult({
|
|
2928
3077
|
status: effectiveReady ? 'ok' : 'blocked',
|
|
@@ -2951,7 +3100,15 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
2951
3100
|
dynamicDomainSchemaLoadingSupported: false,
|
|
2952
3101
|
},
|
|
2953
3102
|
pendingHandoffCount: pendingHandoffs.length,
|
|
2954
|
-
|
|
3103
|
+
// User-facing Work count means objective-level primary lanes. Low-level
|
|
3104
|
+
// resumable operation handles are reported separately.
|
|
3105
|
+
activeWorkCount: activePrimaryWork.length,
|
|
3106
|
+
activePrimaryWorkCount: activePrimaryWork.length,
|
|
3107
|
+
activeExecutionChildCount: activeExecutionChildren.length,
|
|
3108
|
+
activeProcessCount: activeProcessRecords.length,
|
|
3109
|
+
executingPrimaryWorkCount: executingPrimaryWorkIds.size,
|
|
3110
|
+
waitingPrimaryWorkCount: Math.max(0, activePrimaryWork.length - executingPrimaryWorkIds.size),
|
|
3111
|
+
activeContractCount: activeContracts.length,
|
|
2955
3112
|
// Summary keeps the stable facade surface only; detail expands to the full registered schema.
|
|
2956
3113
|
toolSurface: detailLevel === 'detail'
|
|
2957
3114
|
? exposure.actualToolNames
|
|
@@ -3099,6 +3256,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3099
3256
|
maxFiles: typeof args.max_files === 'number' ? args.max_files : undefined,
|
|
3100
3257
|
maxSnippets: typeof args.max_snippets === 'number' ? args.max_snippets : undefined,
|
|
3101
3258
|
structuralContext,
|
|
3259
|
+
structuralIndexRoot: structuralContext === 'off' ? undefined : structuralIndexRoot(repository),
|
|
3102
3260
|
retrievalMode,
|
|
3103
3261
|
impactDomains,
|
|
3104
3262
|
});
|
|
@@ -3116,6 +3274,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3116
3274
|
goal: pack.goal,
|
|
3117
3275
|
search: pack.search,
|
|
3118
3276
|
structuralContext: pack.structuralContext,
|
|
3277
|
+
impactContext: pack.impactContext,
|
|
3119
3278
|
files: pack.files,
|
|
3120
3279
|
deniedPaths: pack.deniedPaths,
|
|
3121
3280
|
omitted: pack.omitted,
|
|
@@ -3167,6 +3326,11 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3167
3326
|
.filter((job) => timestampIsCurrent(job.updatedAt, currentCutoffMs))
|
|
3168
3327
|
.slice(0, 5)
|
|
3169
3328
|
: [];
|
|
3329
|
+
const processScan = listProcessRecords(ctx.controllerHome, repository.repoId, workId ? 100 : 50);
|
|
3330
|
+
const relevantProcesses = processScan.filter((process) => workId ? process.workId === workId : timestampIsCurrent(process.updatedAt, currentCutoffMs));
|
|
3331
|
+
const activeRuntimeInstanceId = currentControllerInstanceId();
|
|
3332
|
+
const activeProcesses = relevantProcesses.filter((process) => isManagedProcessActive(process) && (!process.runtimeInstanceId || process.runtimeInstanceId === activeRuntimeInstanceId));
|
|
3333
|
+
const workController = work ? getControllerSession(store, work.workId) : undefined;
|
|
3170
3334
|
const repositoryManifests = listAssistantPluginManifests(ctx.controllerHome, repository, {
|
|
3171
3335
|
preferStored: true,
|
|
3172
3336
|
});
|
|
@@ -3204,6 +3368,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3204
3368
|
|| timestampIsCurrent(item.updatedAt, currentCutoffMs)
|
|
3205
3369
|
))
|
|
3206
3370
|
: pendingAttention;
|
|
3371
|
+
const workAttention = work ? currentAttentionScan.find((item) => item.workId === work.workId) : undefined;
|
|
3207
3372
|
const currentAttentionItems = isSummary
|
|
3208
3373
|
? currentAttentionScan.slice(0, 3)
|
|
3209
3374
|
: pendingAttention;
|
|
@@ -3259,6 +3424,10 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3259
3424
|
}
|
|
3260
3425
|
: undefined,
|
|
3261
3426
|
executionJob: executionJob ? summarizeWorkListItem(executionJob) : undefined,
|
|
3427
|
+
executionState: work ? (workAttention ? 'blocked' : activeProcesses.length > 0 ? 'executing' : workController ? 'controller_active' : 'waiting_trigger') : undefined,
|
|
3428
|
+
activeController: workController ? { controllerType: workController.controllerType, sessionId: workController.sessionId, leaseExpiresAt: workController.leaseExpiresAt } : undefined,
|
|
3429
|
+
activeProcesses: activeProcesses.slice(0, 3).map((process) => ({ processId: process.processId, workId: process.workId, status: process.status, route: process.route, startedAt: process.startedAt, updatedAt: process.updatedAt })),
|
|
3430
|
+
recentProcesses: relevantProcesses.slice(0, 5).map((process) => ({ processId: process.processId, workId: process.workId, status: process.status, route: process.route, startedAt: process.startedAt, updatedAt: process.updatedAt })),
|
|
3262
3431
|
activeWork: activeContracts.map((entry) => ({
|
|
3263
3432
|
workId: entry.workId,
|
|
3264
3433
|
status: entry.status,
|
|
@@ -3281,6 +3450,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3281
3450
|
historicalNonTerminalWork: Math.max(0, activeContractScan.length - currentContractScan.length),
|
|
3282
3451
|
currentAttention: currentAttentionScan.length,
|
|
3283
3452
|
currentAttentionShown: attention.length,
|
|
3453
|
+
activeProcesses: activeProcesses.length,
|
|
3454
|
+
recentProcesses: relevantProcesses.length,
|
|
3284
3455
|
pendingAttentionScanned: pendingAttention.length,
|
|
3285
3456
|
historicalPendingAttention: Math.max(0, pendingAttention.length - currentAttentionScan.length),
|
|
3286
3457
|
omittedCurrentAttention: Math.max(0, currentAttentionScan.length - attention.length),
|
|
@@ -3318,6 +3489,10 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3318
3489
|
},
|
|
3319
3490
|
work: work ? { ...work, continuation: buildWorkContinuationSnapshot(work) } : undefined,
|
|
3320
3491
|
executionJob: executionJob ? summarizeWorkListItem(executionJob) : undefined,
|
|
3492
|
+
executionState: work ? (workAttention ? 'blocked' : activeProcesses.length > 0 ? 'executing' : workController ? 'controller_active' : 'waiting_trigger') : undefined,
|
|
3493
|
+
activeController: workController,
|
|
3494
|
+
activeProcesses: activeProcesses.slice(0, 10),
|
|
3495
|
+
recentProcesses: relevantProcesses.slice(0, 20),
|
|
3321
3496
|
activeWork: activeContracts.map((entry) => ({
|
|
3322
3497
|
workId: entry.workId,
|
|
3323
3498
|
status: entry.status,
|
|
@@ -3333,6 +3508,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3333
3508
|
capabilities: capabilities.length,
|
|
3334
3509
|
activeWork: activeContracts.length,
|
|
3335
3510
|
recentExecutionJobs: recentJobs.length,
|
|
3511
|
+
activeProcesses: activeProcesses.length,
|
|
3512
|
+
recentProcesses: relevantProcesses.length,
|
|
3336
3513
|
activeAttention: attention.length,
|
|
3337
3514
|
},
|
|
3338
3515
|
bounded: true,
|
|
@@ -3385,8 +3562,59 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3385
3562
|
const triggerType = ['interval', 'cron', 'calendar', 'condition', 'repository-event', 'dependency-checkpoint', 'manual'].includes(triggerTypeRaw)
|
|
3386
3563
|
? triggerTypeRaw as 'interval' | 'cron' | 'calendar' | 'condition' | 'repository-event' | 'dependency-checkpoint' | 'manual'
|
|
3387
3564
|
: undefined;
|
|
3565
|
+
const scheduleModeRaw = String(args.schedule_mode ?? (workId ? 'continuation' : 'workflow')).trim();
|
|
3566
|
+
if (!['workflow', 'continuation', 'browser_watch', 'browser_keepalive'].includes(scheduleModeRaw)) throw new Error('SCHEDULE_MODE_INVALID');
|
|
3567
|
+
if (scheduleModeRaw === 'workflow') {
|
|
3568
|
+
if (controllerType !== 'chatgpt') throw new Error('WORKFLOW_SCHEDULE_CONTROLLER_TYPE_UNSUPPORTED');
|
|
3569
|
+
const prompt = (typeof args.objective === 'string' ? args.objective : typeof args.continuation_prompt === 'string' ? args.continuation_prompt : '').trim();
|
|
3570
|
+
if (!prompt) throw new Error('WORKFLOW_SCHEDULE_PROMPT_REQUIRED');
|
|
3571
|
+
const trigger = {
|
|
3572
|
+
type: triggerType ?? (typeof args.every_minutes === 'number' ? 'interval' : 'manual'),
|
|
3573
|
+
everyMinutes: typeof args.every_minutes === 'number' ? Math.max(1, Math.trunc(args.every_minutes)) : undefined,
|
|
3574
|
+
cronExpression: typeof args.cron_expression === 'string' ? args.cron_expression : undefined,
|
|
3575
|
+
timezone: typeof args.schedule_timezone === 'string' ? args.schedule_timezone : undefined,
|
|
3576
|
+
catchUpMinutes: typeof args.catch_up_minutes === 'number' ? Math.max(0, Math.trunc(args.catch_up_minutes)) : undefined,
|
|
3577
|
+
calendarAt: typeof args.calendar_at === 'string' ? args.calendar_at : undefined,
|
|
3578
|
+
condition: args.condition && typeof args.condition === 'object' && !Array.isArray(args.condition) ? args.condition as never : undefined,
|
|
3579
|
+
eventName: typeof args.event_name === 'string' ? args.event_name : undefined,
|
|
3580
|
+
dependencyJobIds: Array.isArray(args.dependency_job_ids) ? args.dependency_job_ids.map(String) : undefined,
|
|
3581
|
+
};
|
|
3582
|
+
const policy = {
|
|
3583
|
+
maxActiveOccurrences: 1,
|
|
3584
|
+
maxFailures: typeof args.max_failures === 'number' ? Math.max(1, Math.trunc(args.max_failures)) : 3,
|
|
3585
|
+
cooldownMinutes: typeof args.cooldown_minutes === 'number' ? Math.max(0, Math.trunc(args.cooldown_minutes)) : 120,
|
|
3586
|
+
dailyBudgetMinutes: typeof args.daily_budget_minutes === 'number' ? Math.max(1, Math.trunc(args.daily_budget_minutes)) : 180,
|
|
3587
|
+
shadowMode: args.shadow_mode !== false,
|
|
3588
|
+
backoffBaseMinutes: typeof args.backoff_base_minutes === 'number' ? Math.max(1, Math.trunc(args.backoff_base_minutes)) : 5,
|
|
3589
|
+
backoffMaxMinutes: typeof args.backoff_max_minutes === 'number' ? Math.max(1, Math.trunc(args.backoff_max_minutes)) : 24 * 60,
|
|
3590
|
+
};
|
|
3591
|
+
const actionArguments = {
|
|
3592
|
+
prompt,
|
|
3593
|
+
...(typeof args.browser_session_id === 'string' && args.browser_session_id.trim() ? { browser_session_id: args.browser_session_id.trim() } : {}),
|
|
3594
|
+
...(typeof args.conversation_url === 'string' && args.conversation_url.trim() ? { conversation_url: args.conversation_url.trim() } : {}),
|
|
3595
|
+
};
|
|
3596
|
+
assertAutomatedOperationAllowed('chatgpt_browser_prompt', actionArguments);
|
|
3597
|
+
const name = typeof args.schedule_name === 'string' && args.schedule_name.trim() ? args.schedule_name.trim() : `Workflow: ${prompt.slice(0, 80)}`;
|
|
3598
|
+
const stopConditions = Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : [];
|
|
3599
|
+
const semantic = JSON.stringify({ repoId: repository.repoId, name, trigger, policy, actionArguments, stopConditions });
|
|
3600
|
+
const requestId = typeof args.schedule_request_id === 'string' && args.schedule_request_id.trim()
|
|
3601
|
+
? args.schedule_request_id.trim()
|
|
3602
|
+
: `workflow-schedule:${repository.repoId}:${createHash('sha256').update(semantic).digest('hex').slice(0, 20)}`;
|
|
3603
|
+
const schedule = createSchedule(ctx.controllerHome, {
|
|
3604
|
+
requestId,
|
|
3605
|
+
repoId: repository.repoId,
|
|
3606
|
+
name,
|
|
3607
|
+
enabled: true,
|
|
3608
|
+
trigger,
|
|
3609
|
+
policy,
|
|
3610
|
+
action: { operation: 'chatgpt_browser_prompt', target: 'runtime', arguments: actionArguments, resourceClaims: [] },
|
|
3611
|
+
stopConditions,
|
|
3612
|
+
});
|
|
3613
|
+
return result(buildFacadeResult({ summary: `Workflow schedule ${schedule.scheduleId} is configured without a durable Work.`, data: { schedule } }) as unknown as Record<string, unknown>);
|
|
3614
|
+
}
|
|
3388
3615
|
const created = createWorkContinuationSchedule(ctx.controllerHome, repository.repoId, {
|
|
3389
3616
|
workId,
|
|
3617
|
+
scheduleMode: scheduleModeRaw as 'continuation' | 'browser_watch' | 'browser_keepalive',
|
|
3390
3618
|
controllerType: controllerType as ContinuationControllerType,
|
|
3391
3619
|
executable: typeof args.executable === 'string' ? args.executable : undefined,
|
|
3392
3620
|
launchArgs: Array.isArray(args.launch_args) ? args.launch_args.map(String) : undefined,
|
|
@@ -3395,6 +3623,18 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3395
3623
|
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
|
|
3396
3624
|
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
|
|
3397
3625
|
continuationPrompt: typeof args.continuation_prompt === 'string' ? args.continuation_prompt : undefined,
|
|
3626
|
+
probeUrl: typeof args.probe_url === 'string' ? args.probe_url : undefined,
|
|
3627
|
+
probeBrowserSessionId: typeof args.probe_browser_session_id === 'string' ? args.probe_browser_session_id : undefined,
|
|
3628
|
+
probeSelector: typeof args.probe_selector === 'string' ? args.probe_selector : undefined,
|
|
3629
|
+
probeMaxChars: typeof args.probe_max_chars === 'number' ? args.probe_max_chars : undefined,
|
|
3630
|
+
probeTimeoutMs: typeof args.probe_timeout_ms === 'number' ? args.probe_timeout_ms : undefined,
|
|
3631
|
+
includeTerms: Array.isArray(args.include_terms) ? args.include_terms.map(String) : undefined,
|
|
3632
|
+
ignorePatterns: Array.isArray(args.ignore_patterns) ? args.ignore_patterns.map(String) : undefined,
|
|
3633
|
+
loginUrlTerms: Array.isArray(args.login_url_terms) ? args.login_url_terms.map(String) : undefined,
|
|
3634
|
+
loginTextTerms: Array.isArray(args.login_text_terms) ? args.login_text_terms.map(String) : undefined,
|
|
3635
|
+
wakeOnFirstObservation: args.wake_on_first_observation === true,
|
|
3636
|
+
wakeOnAuthRequired: args.wake_on_auth_required !== false,
|
|
3637
|
+
authRequiredPrompt: typeof args.auth_required_prompt === 'string' ? args.auth_required_prompt : undefined,
|
|
3398
3638
|
scheduleName: typeof args.schedule_name === 'string' ? args.schedule_name : undefined,
|
|
3399
3639
|
requestId: typeof args.schedule_request_id === 'string' ? args.schedule_request_id : undefined,
|
|
3400
3640
|
triggerType,
|
|
@@ -3414,33 +3654,41 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3414
3654
|
backoffMaxMinutes: typeof args.backoff_max_minutes === 'number' ? args.backoff_max_minutes : undefined,
|
|
3415
3655
|
stopConditions: Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : undefined,
|
|
3416
3656
|
});
|
|
3417
|
-
return result(buildFacadeResult({ summary: `
|
|
3657
|
+
return result(buildFacadeResult({ summary: `Work schedule ${created.schedule.scheduleId} is configured for Work ${workId}.`, data: { schedule: created.schedule, work: buildWorkContinuationSnapshot(created.work) } }) as unknown as Record<string, unknown>);
|
|
3418
3658
|
}
|
|
3419
3659
|
if (operation === 'schedule_list') {
|
|
3420
|
-
const
|
|
3421
|
-
|
|
3660
|
+
const schedules = listSchedules(ctx.controllerHome, repository.repoId)
|
|
3661
|
+
.filter((entry) => !workId || entry.action.arguments?.work_id === workId);
|
|
3662
|
+
const data = args.include_occurrences === true
|
|
3663
|
+
? { schedules, occurrences: listOccurrences(ctx.controllerHome, repository.repoId, undefined, 100).filter((entry) => schedules.some((schedule) => schedule.scheduleId === entry.scheduleId)) }
|
|
3664
|
+
: { schedules };
|
|
3665
|
+
return result(buildFacadeResult({ summary: `Found ${schedules.length} schedule(s).`, data }) as unknown as Record<string, unknown>);
|
|
3422
3666
|
}
|
|
3423
3667
|
if (!scheduleId) throw new Error('SCHEDULE_ID_REQUIRED');
|
|
3424
3668
|
if (operation === 'schedule_get') {
|
|
3425
|
-
const
|
|
3426
|
-
|
|
3669
|
+
const schedule = getSchedule(ctx.controllerHome, repository.repoId, scheduleId);
|
|
3670
|
+
const data = args.include_occurrences === true ? { schedule, occurrences: listOccurrences(ctx.controllerHome, repository.repoId, scheduleId, 50) } : { schedule };
|
|
3671
|
+
return result(buildFacadeResult({ summary: `Schedule ${scheduleId}.`, data }) as unknown as Record<string, unknown>);
|
|
3427
3672
|
}
|
|
3428
3673
|
if (operation === 'schedule_pause') {
|
|
3429
|
-
const schedule =
|
|
3430
|
-
|
|
3674
|
+
const schedule = getSchedule(ctx.controllerHome, repository.repoId, scheduleId);
|
|
3675
|
+
const saved = saveSchedule(ctx.controllerHome, { ...schedule, enabled: false, pausedReason: typeof args.reason === 'string' && args.reason.trim() ? args.reason.trim() : 'Paused by ChatGPT.' });
|
|
3676
|
+
return result(buildFacadeResult({ summary: `Schedule ${scheduleId} is paused.`, data: { schedule: saved } }) as unknown as Record<string, unknown>);
|
|
3431
3677
|
}
|
|
3432
3678
|
if (operation === 'schedule_resume') {
|
|
3433
|
-
const schedule =
|
|
3434
|
-
|
|
3679
|
+
const schedule = getSchedule(ctx.controllerHome, repository.repoId, scheduleId);
|
|
3680
|
+
const saved = saveSchedule(ctx.controllerHome, { ...schedule, enabled: true, pausedReason: undefined, consecutiveFailures: 0, nextEligibleAt: undefined });
|
|
3681
|
+
return result(buildFacadeResult({ summary: `Schedule ${scheduleId} is resumed.`, data: { schedule: saved } }) as unknown as Record<string, unknown>);
|
|
3435
3682
|
}
|
|
3436
3683
|
if (operation === 'schedule_trigger') {
|
|
3437
|
-
const
|
|
3684
|
+
const schedule = getSchedule(ctx.controllerHome, repository.repoId, scheduleId);
|
|
3685
|
+
const occurrence = await evaluateSchedule(ctx.controllerHome, schedule, true, {
|
|
3438
3686
|
source: typeof args.event_name === 'string' && args.event_name.trim() ? 'repository-event' : 'manual',
|
|
3439
3687
|
eventName: typeof args.event_name === 'string' ? args.event_name : undefined,
|
|
3440
3688
|
eventId: typeof args.event_id === 'string' ? args.event_id : undefined,
|
|
3441
3689
|
data: args.event_data && typeof args.event_data === 'object' && !Array.isArray(args.event_data) ? args.event_data as Record<string, unknown> : undefined,
|
|
3442
3690
|
});
|
|
3443
|
-
return result(buildFacadeResult({ summary: occurrence ? `
|
|
3691
|
+
return result(buildFacadeResult({ summary: occurrence ? `Schedule ${scheduleId} produced ${occurrence.decision}.` : `Schedule ${scheduleId} produced no occurrence.`, data: { occurrence } }) as unknown as Record<string, unknown>);
|
|
3444
3692
|
}
|
|
3445
3693
|
} catch (error) {
|
|
3446
3694
|
return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Schedule operation failed.', data: {} }) as unknown as Record<string, unknown>, true);
|
|
@@ -3453,7 +3701,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3453
3701
|
if (operation === 'controller_claim') {
|
|
3454
3702
|
try {
|
|
3455
3703
|
const workId = String(args.work_id ?? '').trim();
|
|
3456
|
-
|
|
3704
|
+
const work = getWorkContract(store, workId);
|
|
3705
|
+
if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
|
|
3457
3706
|
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
3458
3707
|
const session = resumeControllerSession(store, {
|
|
3459
3708
|
workId,
|
|
@@ -3464,6 +3713,24 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3464
3713
|
controllerInstanceId: identity.controllerInstanceId,
|
|
3465
3714
|
leaseMs: typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
|
|
3466
3715
|
});
|
|
3716
|
+
const permissionSnapshotVersion = currentPermissionSnapshotVersion(ctx.controllerHome, repository.repoId);
|
|
3717
|
+
const executionSession = startExecutionSession(ctx.controllerHome, {
|
|
3718
|
+
sessionId: identity.sessionId,
|
|
3719
|
+
principalId: identity.principalId,
|
|
3720
|
+
controllerInstanceId: identity.controllerInstanceId,
|
|
3721
|
+
permissionSnapshotVersion,
|
|
3722
|
+
});
|
|
3723
|
+
updateExecutionSession(ctx.controllerHome, {
|
|
3724
|
+
sessionId: executionSession.sessionId,
|
|
3725
|
+
principalId: executionSession.principalId,
|
|
3726
|
+
controllerInstanceId: executionSession.controllerInstanceId,
|
|
3727
|
+
}, {
|
|
3728
|
+
activeRepositoryId: repository.repoId,
|
|
3729
|
+
activeCheckoutId: work.checkoutId || repository.activeCheckoutId,
|
|
3730
|
+
activeWorkId: work.workId,
|
|
3731
|
+
permissionSnapshotVersion,
|
|
3732
|
+
lastValidatedAt: new Date().toISOString(),
|
|
3733
|
+
});
|
|
3467
3734
|
return result(buildFacadeResult({ summary: `Controller ${session.controllerId} claimed ${session.workId}.`, data: { session } }) as unknown as Record<string, unknown>);
|
|
3468
3735
|
} catch (error) {
|
|
3469
3736
|
return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller claim failed.', data: {} }) as unknown as Record<string, unknown>, true);
|
|
@@ -3478,6 +3745,10 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3478
3745
|
throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId} is owned by ${owner.controllerId}`);
|
|
3479
3746
|
}
|
|
3480
3747
|
releaseControllerSession(store, workId, identity.controllerId);
|
|
3748
|
+
const executionSession = readExecutionSession(ctx.controllerHome, identity);
|
|
3749
|
+
if (executionSession?.activeWorkId === workId) {
|
|
3750
|
+
updateExecutionSession(ctx.controllerHome, identity, { activeWorkId: undefined, lastValidatedAt: new Date().toISOString() });
|
|
3751
|
+
}
|
|
3481
3752
|
return result(buildFacadeResult({ summary: 'Controller lease released.', data: {} }) as unknown as Record<string, unknown>);
|
|
3482
3753
|
} catch (error) {
|
|
3483
3754
|
return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller release failed.', data: {} }) as unknown as Record<string, unknown>, true);
|
|
@@ -3515,15 +3786,81 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3515
3786
|
controllerInstanceId: ctx.controllerInstanceId,
|
|
3516
3787
|
availableChecks: checks,
|
|
3517
3788
|
sourceRevision: gitSnapshot(repository.canonicalRoot).head ?? undefined,
|
|
3789
|
+
materializeIsolatedWorkspace: ({ workId, title, baseRef }: { workId: string; title: string; baseRef?: string }) => {
|
|
3790
|
+
const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, { requestId: workId, title, baseRef });
|
|
3791
|
+
if (!workspace.managed || !workspace.checkoutId || !workspace.root) throw new Error('MANAGED_WORKSPACE_NOT_MATERIALIZED');
|
|
3792
|
+
return { checkoutId: workspace.checkoutId, root: workspace.root, baseRevision: workspace.baseRevision, managed: true as const };
|
|
3793
|
+
},
|
|
3518
3794
|
};
|
|
3519
3795
|
|
|
3520
3796
|
if (operation.startsWith('plan_')) {
|
|
3521
3797
|
try {
|
|
3522
3798
|
if (operation === 'plan_create') {
|
|
3523
3799
|
const rawSteps = Array.isArray(args.plan_steps) ? args.plan_steps : [];
|
|
3800
|
+
const requestedRequirementId = typeof args.requirement_id === 'string' && args.requirement_id.trim() ? args.requirement_id.trim() : undefined;
|
|
3801
|
+
const requestedScopeKey = String(args.scope_key ?? '').trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 160);
|
|
3802
|
+
const activePlans = listPlanContracts({ ...store, status: 'active', limit: 100 });
|
|
3803
|
+
const exactScopeAuthority = requestedScopeKey
|
|
3804
|
+
? activePlans.find((candidate) => candidate.scopeKey === requestedScopeKey)
|
|
3805
|
+
: undefined;
|
|
3806
|
+
if (exactScopeAuthority) {
|
|
3807
|
+
const facade = buildFacadeResult({
|
|
3808
|
+
summary: `PLAN_AUTHORITY_REUSED: active Plan ${exactScopeAuthority.planId} already owns scope ${requestedScopeKey}; no duplicate draft was created.`,
|
|
3809
|
+
data: {
|
|
3810
|
+
plan: summarizePlanContract(exactScopeAuthority),
|
|
3811
|
+
executionStarted: false,
|
|
3812
|
+
planContractCreated: false,
|
|
3813
|
+
admissionDecision: 'reuse_existing',
|
|
3814
|
+
resolutionRequired: false,
|
|
3815
|
+
},
|
|
3816
|
+
suggestedNextActions: [{ label: 'Read active Plan', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: exactScopeAuthority.planId }, risk: 'readonly', confidence: 'high' }],
|
|
3817
|
+
});
|
|
3818
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
3819
|
+
}
|
|
3820
|
+
const requirementPlans = requestedRequirementId
|
|
3821
|
+
? activePlans.filter((candidate) => candidate.requirementId === requestedRequirementId)
|
|
3822
|
+
: [];
|
|
3823
|
+
const requestedPlanRelation = args.plan_relation === 'extend' || args.plan_relation === 'parallel'
|
|
3824
|
+
? args.plan_relation
|
|
3825
|
+
: undefined;
|
|
3826
|
+
const relatedPlanId = typeof args.related_plan_id === 'string' && args.related_plan_id.trim() ? args.related_plan_id.trim() : undefined;
|
|
3827
|
+
const relatedPlan = relatedPlanId
|
|
3828
|
+
? requirementPlans.find((candidate) => candidate.planId === relatedPlanId)
|
|
3829
|
+
: requirementPlans.length === 1 ? requirementPlans[0] : undefined;
|
|
3830
|
+
if (requirementPlans.length > 0 && !requestedPlanRelation) {
|
|
3831
|
+
const facade = buildFacadeResult({
|
|
3832
|
+
summary: `PLAN_RELATION_RESOLUTION_REQUIRED: Requirement ${requestedRequirementId} already has ${requirementPlans.length} active Plan slice(s). Decide whether this scope extends one of them or is an intentional parallel slice before creating a draft.`,
|
|
3833
|
+
data: {
|
|
3834
|
+
executionStarted: false,
|
|
3835
|
+
planContractCreated: false,
|
|
3836
|
+
admissionDecision: 'resolution_required',
|
|
3837
|
+
resolutionRequired: true,
|
|
3838
|
+
candidates: requirementPlans.slice(0, 8).map(summarizePlanContract),
|
|
3839
|
+
allowedPlanRelations: ['extend', 'parallel'],
|
|
3840
|
+
},
|
|
3841
|
+
suggestedNextActions: requirementPlans.slice(0, 3).map((candidate) => ({ label: `Read ${candidate.planId}`, tool: 'rh_work', operation: 'plan_get', payload: { plan_id: candidate.planId }, risk: 'readonly' as const, confidence: 'high' as const })),
|
|
3842
|
+
});
|
|
3843
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
3844
|
+
}
|
|
3845
|
+
if (requirementPlans.length > 0 && requestedPlanRelation === 'extend') {
|
|
3846
|
+
if (!relatedPlan) {
|
|
3847
|
+
const facade = buildFacadeResult({
|
|
3848
|
+
summary: `PLAN_EXTENSION_TARGET_REQUIRED: select related_plan_id from the active Plan slices for Requirement ${requestedRequirementId}.`,
|
|
3849
|
+
data: { executionStarted: false, planContractCreated: false, admissionDecision: 'resolution_required', resolutionRequired: true, candidates: requirementPlans.slice(0, 8).map(summarizePlanContract) },
|
|
3850
|
+
});
|
|
3851
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
3852
|
+
}
|
|
3853
|
+
const facade = buildFacadeResult({
|
|
3854
|
+
summary: `PLAN_EXTENSION_REQUIRES_REPLAN: ${relatedPlan.planId} remains the serial authority. Extend/replan and supersede that authority rather than creating a second active Plan draft.`,
|
|
3855
|
+
data: { plan: summarizePlanContract(relatedPlan), executionStarted: false, planContractCreated: false, admissionDecision: 'extend_existing', resolutionRequired: false },
|
|
3856
|
+
suggestedNextActions: [{ label: 'Read active Plan', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: relatedPlan.planId }, risk: 'readonly', confidence: 'high' }],
|
|
3857
|
+
});
|
|
3858
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
3859
|
+
}
|
|
3524
3860
|
const plan = createPlanContract(store, {
|
|
3525
3861
|
planId: String(args.plan_id ?? ''),
|
|
3526
3862
|
repoId: repository.repoId,
|
|
3863
|
+
requirementId: requestedRequirementId,
|
|
3527
3864
|
scopeKey: String(args.scope_key ?? ''),
|
|
3528
3865
|
sourceRevision: String(args.source_revision ?? ''),
|
|
3529
3866
|
goal: String(args.objective ?? ''),
|
|
@@ -3545,8 +3882,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3545
3882
|
})),
|
|
3546
3883
|
});
|
|
3547
3884
|
const facade = buildFacadeResult({
|
|
3548
|
-
summary: `PlanContract ${plan.planId} created as draft; no execution was started.`,
|
|
3549
|
-
data: { plan: summarizePlanContract(plan), executionStarted: false },
|
|
3885
|
+
summary: `PlanContract ${plan.planId} created as draft after authority preflight; no execution was started.`,
|
|
3886
|
+
data: { plan: summarizePlanContract(plan), executionStarted: false, planContractCreated: true, admissionDecision: 'create_new' },
|
|
3550
3887
|
suggestedNextActions: [{ label: 'Approve reviewed plan', tool: 'rh_work', operation: 'plan_approve', payload: { plan_id: plan.planId }, risk: 'workspace_write', confidence: 'medium' }],
|
|
3551
3888
|
});
|
|
3552
3889
|
return result(facade as unknown as Record<string, unknown>);
|
|
@@ -3652,7 +3989,43 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3652
3989
|
|
|
3653
3990
|
if (operation === 'finalize') {
|
|
3654
3991
|
const workId = String(args.work_id ?? '').trim();
|
|
3655
|
-
|
|
3992
|
+
let before = workId ? getWorkContract(store, workId) : undefined;
|
|
3993
|
+
if (before && !before.completionReceipt && args.reconcile_historical_delivery === true) {
|
|
3994
|
+
try {
|
|
3995
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
3996
|
+
const owner = getControllerSession(store, workId);
|
|
3997
|
+
if (!owner || (owner.principalId?.trim() || owner.controllerId) !== identity.principalId) {
|
|
3998
|
+
throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_CONTROLLER_CLAIM_REQUIRED: ${workId}`);
|
|
3999
|
+
}
|
|
4000
|
+
const historicalHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
|
|
4001
|
+
if (historicalHandle?.managedWorktree) throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_MANAGED_CLEANUP_REQUIRED: ${workId}`);
|
|
4002
|
+
const reconciliation = acceptReviewedDirectEditWorkReconciliation({
|
|
4003
|
+
controllerHome: ctx.controllerHome,
|
|
4004
|
+
repoId: repository.repoId,
|
|
4005
|
+
checkoutId: before.checkoutId ?? repository.activeCheckoutId,
|
|
4006
|
+
repoRoot: repository.canonicalRoot,
|
|
4007
|
+
workId,
|
|
4008
|
+
targetBranch: typeof args.target_branch === 'string' && args.target_branch.trim() ? args.target_branch.trim() : repository.defaultBranch || 'main',
|
|
4009
|
+
targetRevision: String(args.reconcile_target_revision ?? ''),
|
|
4010
|
+
comparedPaths: Array.isArray(args.reconcile_compared_paths) ? args.reconcile_compared_paths.map(String) : [],
|
|
4011
|
+
reviewer: identity.principalId,
|
|
4012
|
+
rationale: String(args.reconcile_rationale ?? ''),
|
|
4013
|
+
cleanupOwnershipProof: String(args.reconcile_cleanup_proof ?? ''),
|
|
4014
|
+
});
|
|
4015
|
+
if (historicalHandle && historicalHandle.state !== 'cleaned') {
|
|
4016
|
+
const finalization: WorkHandleState['finalization'] = { validation: 'done', commit: 'done', merge: 'skipped', branchCleanup: 'skipped', worktreeCleanup: 'skipped' };
|
|
4017
|
+
const delivered = historicalHandle.state === 'committed' || historicalHandle.state === 'merged' || historicalHandle.state === 'failed_terminal_cleanup'
|
|
4018
|
+
? historicalHandle
|
|
4019
|
+
: transitionWorkHandle(ctx.controllerHome, historicalHandle, 'committed', { expectedHead: reconciliation.receipt.targetRevision, finalization, failureReason: undefined });
|
|
4020
|
+
transitionWorkHandle(ctx.controllerHome, delivered, 'cleaned', { expectedHead: reconciliation.receipt.targetRevision, finalization, failureReason: undefined });
|
|
4021
|
+
}
|
|
4022
|
+
releaseControllerSession(store, workId, owner.controllerId);
|
|
4023
|
+
before = getWorkContract(store, workId);
|
|
4024
|
+
} catch (error) {
|
|
4025
|
+
const blocked = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Historical Work delivery reconciliation failed.', data: { workId, lifecycleClosed: false } });
|
|
4026
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
3656
4029
|
if (before && !before.completionReceipt) {
|
|
3657
4030
|
try {
|
|
3658
4031
|
const physical = await finalizeFacadeWorkHandle(ctx, repository, args, 'finalize');
|
|
@@ -3708,12 +4081,28 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3708
4081
|
}
|
|
3709
4082
|
|
|
3710
4083
|
const facade = runGoalWorkloop({ ...workloopCtx, sourceRevision: workloopCtx.sourceRevision ?? undefined }, operation as 'start' | 'continue', args);
|
|
4084
|
+
const facadeData = facade.data && typeof facade.data === 'object' ? facade.data as Record<string, unknown> : {};
|
|
4085
|
+
const facadeWorkId = contextText(contextRecord(facadeData.work).workId, 200);
|
|
4086
|
+
if (facade.status === 'ok' && facadeWorkId) {
|
|
4087
|
+
try {
|
|
4088
|
+
const handle = ensureFacadeWorkHandle(ctx, repository, facadeWorkId, args);
|
|
4089
|
+
if (handle) facadeData.executionHandle = { workId: handle.workId, checkoutId: handle.checkoutId, managedWorktree: handle.managedWorktree, state: handle.state };
|
|
4090
|
+
if (facadeData.workContractCreated === true) {
|
|
4091
|
+
const owner = claimNewFacadeWork(ctx, repository, facadeWorkId, args);
|
|
4092
|
+
facadeData.controllerSession = owner;
|
|
4093
|
+
facadeData.ownershipClaimed = true;
|
|
4094
|
+
}
|
|
4095
|
+
} catch (error) {
|
|
4096
|
+
const blocked = buildFacadeResult({ status: 'blocked', summary: `WORK_HANDLE_MATERIALIZATION_FAILED: ${error instanceof Error ? error.message : String(error)}`, data: { ...facadeData, workId: facadeWorkId, executionStarted: false } });
|
|
4097
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
3711
4100
|
const response = resumedControllerSession
|
|
3712
4101
|
? {
|
|
3713
4102
|
...facade,
|
|
3714
4103
|
summary: `Controller ownership resumed for ${resumedControllerSession.workId}. ${facade.summary}`,
|
|
3715
4104
|
data: {
|
|
3716
|
-
...
|
|
4105
|
+
...facadeData,
|
|
3717
4106
|
ownershipResumed: true,
|
|
3718
4107
|
controllerSession: resumedControllerSession,
|
|
3719
4108
|
},
|
|
@@ -3781,6 +4170,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3781
4170
|
const accepted = acceptSubmittedWorkContract(ctx.controllerHome, {
|
|
3782
4171
|
requestId,
|
|
3783
4172
|
repoId: repository.repoId,
|
|
4173
|
+
parentWorkId: typeof args.parent_work_id === 'string' && args.parent_work_id.trim() ? args.parent_work_id.trim() : undefined,
|
|
3784
4174
|
semanticKey,
|
|
3785
4175
|
operation: {
|
|
3786
4176
|
name: operation,
|
|
@@ -5254,9 +5644,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5254
5644
|
case 'recovery.workspace_auth_login_prepare':
|
|
5255
5645
|
payload = { skipped: true, nextTool: 'workspace_auth_login_prepare', reason: 'Auth login is a non-secret handoff and should be prepared through the dedicated typed tool.' };
|
|
5256
5646
|
break;
|
|
5257
|
-
case 'recovery.browser_domain_access_preview':
|
|
5258
|
-
payload = { skipped: true, nextTool: 'web_domain_access_preview', reason: 'Browser access must be granted by domain key before snapshot or interaction.' };
|
|
5259
|
-
break;
|
|
5260
5647
|
case 'recovery.external_filesystem_grant_preview':
|
|
5261
5648
|
payload = { skipped: true, nextTool: 'external_filesystem_grant_preview', reason: 'External filesystem access must be converted into a named read-only target first.' };
|
|
5262
5649
|
break;
|
|
@@ -5489,6 +5876,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5489
5876
|
pluginId,
|
|
5490
5877
|
actionId,
|
|
5491
5878
|
requestId,
|
|
5879
|
+
workId: typeof args.work_id === 'string' && args.work_id.trim() ? args.work_id.trim() : undefined,
|
|
5492
5880
|
args: actionArguments,
|
|
5493
5881
|
timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
5494
5882
|
signal: ctx.signal,
|
|
@@ -5500,7 +5888,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5500
5888
|
const action = manifest.actions.find((entry) => entry.actionId === actionId);
|
|
5501
5889
|
if (action && isDirectPluginReadAction(action)) {
|
|
5502
5890
|
const direct = await executeAssistantPluginReadDirect(ctx.controllerHome, repository, request);
|
|
5503
|
-
|
|
5891
|
+
const value = {
|
|
5504
5892
|
accepted: true,
|
|
5505
5893
|
direct: true,
|
|
5506
5894
|
durable: false,
|
|
@@ -5521,10 +5909,11 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5521
5909
|
},
|
|
5522
5910
|
},
|
|
5523
5911
|
next: 'Continue with the returned bounded result; use rh_context capability detail only when the typed action schema/policy is needed.',
|
|
5524
|
-
}
|
|
5912
|
+
};
|
|
5913
|
+
return resultWithPluginArtifactImages(value, ctx.controllerHome, repository.repoId, direct.result);
|
|
5525
5914
|
}
|
|
5526
5915
|
const submitted = await submitAssistantPluginAction(ctx.controllerHome, repository, request);
|
|
5527
|
-
|
|
5916
|
+
const value = {
|
|
5528
5917
|
accepted: true,
|
|
5529
5918
|
deduplicated: submitted.deduplicated,
|
|
5530
5919
|
direct: true,
|
|
@@ -5539,6 +5928,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5539
5928
|
scope: repository.repoId === '__controller__' ? 'controller' : 'repository',
|
|
5540
5929
|
receiptId: submitted.receipt.receiptId,
|
|
5541
5930
|
requestId: submitted.receipt.requestId,
|
|
5931
|
+
...(submitted.receipt.workId ? { workId: submitted.receipt.workId } : {}),
|
|
5542
5932
|
result: submitted.result,
|
|
5543
5933
|
detail: {
|
|
5544
5934
|
tool: 'rh_context',
|
|
@@ -5549,7 +5939,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5549
5939
|
},
|
|
5550
5940
|
},
|
|
5551
5941
|
next: 'Continue with the returned bounded plugin result; use rh_context capability detail only when the typed action schema/policy is needed.',
|
|
5552
|
-
}
|
|
5942
|
+
};
|
|
5943
|
+
return resultWithPluginArtifactImages(value, ctx.controllerHome, repository.repoId, submitted.result);
|
|
5553
5944
|
}
|
|
5554
5945
|
case 'toolchain_plugin_summary': {
|
|
5555
5946
|
const pluginId = String(args.plugin_id ?? '').trim();
|
|
@@ -5559,97 +5950,10 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5559
5950
|
plugin: summarizePluginForLowInterception(manifest),
|
|
5560
5951
|
nonOpaque: true,
|
|
5561
5952
|
next: manifest.pluginId === 'browser'
|
|
5562
|
-
? 'Use
|
|
5953
|
+
? 'Use rh_context for browser capability schemas and plugin_action_execute for typed HTTP(S) browser actions.'
|
|
5563
5954
|
: undefined,
|
|
5564
5955
|
});
|
|
5565
5956
|
}
|
|
5566
|
-
case 'web_targets_list': {
|
|
5567
|
-
const repository = selected(ctx, args);
|
|
5568
|
-
const manifest = getAssistantPluginManifest(ctx.controllerHome, repository, 'browser');
|
|
5569
|
-
return result({
|
|
5570
|
-
pluginId: 'browser',
|
|
5571
|
-
enabled: manifest.enabled,
|
|
5572
|
-
healthState: manifest.health.state,
|
|
5573
|
-
ready: manifest.health.ready,
|
|
5574
|
-
targets: listWebTargets(repository.canonicalRoot, manifest),
|
|
5575
|
-
safety: {
|
|
5576
|
-
arbitraryUrlAccepted: false,
|
|
5577
|
-
returnsRawConfig: false,
|
|
5578
|
-
nextTool: 'web_target_snapshot',
|
|
5579
|
-
},
|
|
5580
|
-
});
|
|
5581
|
-
}
|
|
5582
|
-
case 'web_target_snapshot': {
|
|
5583
|
-
const repository = selected(ctx, args);
|
|
5584
|
-
const manifest = getAssistantPluginManifest(ctx.controllerHome, repository, 'browser');
|
|
5585
|
-
const url = resolveWebTargetUrl(repository.canonicalRoot, String(args.target_key ?? '').trim(), args.path, args.query, manifest);
|
|
5586
|
-
const capture = args.capture === 'screenshot' ? 'screenshot' : args.capture === 'text' ? 'text' : 'title';
|
|
5587
|
-
const actionId = capture === 'screenshot' ? 'screenshot' : 'open_page';
|
|
5588
|
-
const actionArguments: Record<string, unknown> = {
|
|
5589
|
-
url,
|
|
5590
|
-
wait_until: typeof args.wait_until === 'string' ? args.wait_until : 'domcontentloaded',
|
|
5591
|
-
timeout_ms: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
5592
|
-
...(capture === 'text' ? { extract_text: true, max_chars: typeof args.max_chars === 'number' ? args.max_chars : 4000 } : {}),
|
|
5593
|
-
...(capture === 'screenshot' ? { full_page: args.full_page === true } : {}),
|
|
5594
|
-
};
|
|
5595
|
-
const requestId = String(args.request_id ?? '').trim();
|
|
5596
|
-
if (!requestId) throw new Error('REQUEST_ID_REQUIRED: web_target_snapshot requires request_id');
|
|
5597
|
-
const submitted = await submitAssistantPluginAction(ctx.controllerHome, repository, {
|
|
5598
|
-
pluginId: 'browser',
|
|
5599
|
-
actionId,
|
|
5600
|
-
requestId,
|
|
5601
|
-
args: actionArguments,
|
|
5602
|
-
timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
5603
|
-
origin: { surface: 'mcp', actor: 'web_target_snapshot', correlationId: requestId },
|
|
5604
|
-
});
|
|
5605
|
-
return result({
|
|
5606
|
-
accepted: true,
|
|
5607
|
-
deduplicated: submitted.deduplicated,
|
|
5608
|
-
webTarget: {
|
|
5609
|
-
targetKey: String(args.target_key ?? '').trim(),
|
|
5610
|
-
capture,
|
|
5611
|
-
path: typeof args.path === 'string' ? args.path : '/',
|
|
5612
|
-
arbitraryUrlAccepted: false,
|
|
5613
|
-
},
|
|
5614
|
-
job: summarizeWork(submitted.job, repository.canonicalRoot),
|
|
5615
|
-
next: `Call work_status_digest with work_ref ${submitted.job.jobId}.`,
|
|
5616
|
-
});
|
|
5617
|
-
}
|
|
5618
|
-
case 'web_domain_access_preview': {
|
|
5619
|
-
const repository = selected(ctx, args);
|
|
5620
|
-
const manifest = getAssistantPluginManifest(ctx.controllerHome, repository, 'browser');
|
|
5621
|
-
return result({
|
|
5622
|
-
preview: previewBrowserDomainAccess(repository.canonicalRoot, args.domain, args.reason, manifest),
|
|
5623
|
-
next: 'After human review, call web_domain_access_apply with confirm_authorization=true.',
|
|
5624
|
-
});
|
|
5625
|
-
}
|
|
5626
|
-
case 'web_domain_access_apply': {
|
|
5627
|
-
const repository = selected(ctx, args);
|
|
5628
|
-
if (args.confirm_authorization !== true) throw new Error('CONFIRM_AUTHORIZATION_REQUIRED: web_domain_access_apply requires confirm_authorization=true');
|
|
5629
|
-
const manifest = getAssistantPluginManifest(ctx.controllerHome, repository, 'browser');
|
|
5630
|
-
const preview = previewBrowserDomainAccess(repository.canonicalRoot, args.domain, args.reason, manifest);
|
|
5631
|
-
const providedTicket = typeof args.preview_ticket_id === 'string' ? args.preview_ticket_id.trim() : '';
|
|
5632
|
-
if (providedTicket && providedTicket !== preview.ticketId) throw new Error('DOMAIN_ACCESS_TICKET_MISMATCH');
|
|
5633
|
-
const requestId = String(args.request_id ?? '').trim();
|
|
5634
|
-
if (!requestId) throw new Error('REQUEST_ID_REQUIRED: web_domain_access_apply requires request_id');
|
|
5635
|
-
const allowedDomains = mergeAllowedDomains(repository.canonicalRoot, args.domain, manifest);
|
|
5636
|
-
const submitted = await submitAssistantPluginAction(ctx.controllerHome, repository, {
|
|
5637
|
-
pluginId: 'browser',
|
|
5638
|
-
actionId: 'configure',
|
|
5639
|
-
requestId,
|
|
5640
|
-
args: { enabled: true, allowed_domains: allowedDomains },
|
|
5641
|
-
confirmAuthorization: true,
|
|
5642
|
-
origin: { surface: 'mcp', actor: 'web_domain_access_apply', correlationId: requestId },
|
|
5643
|
-
});
|
|
5644
|
-
return result({
|
|
5645
|
-
accepted: true,
|
|
5646
|
-
deduplicated: submitted.deduplicated,
|
|
5647
|
-
preview,
|
|
5648
|
-
allowedDomainCount: allowedDomains.length,
|
|
5649
|
-
job: summarizeWork(submitted.job, repository.canonicalRoot),
|
|
5650
|
-
next: `Call work_status_digest with work_ref ${submitted.job.jobId}.`,
|
|
5651
|
-
});
|
|
5652
|
-
}
|
|
5653
5957
|
case 'work_result_summary': {
|
|
5654
5958
|
const repository = selected(ctx, args);
|
|
5655
5959
|
const jobId = String(args.job_id ?? '').trim();
|