@moretea-labs/forge 1.8.1 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -2
- package/CHANGELOG.md +20 -0
- package/README.en.md +24 -3
- package/README.md +10 -6
- package/README.zh-CN.md +5 -3
- package/THIRD_PARTY_NOTICES.md +1 -2
- package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
- package/adapters/chatgpt/provider-delivery.ts +1 -1
- package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
- package/adapters/computer/chatgpt-extension-target.ts +295 -50
- package/adapters/computer/chrome-extension/background.js +227 -71
- package/adapters/computer/chrome-extension/content.js +181 -157
- package/adapters/computer/chrome-extension/manifest.json +3 -1
- package/adapters/computer/desktop-operator-provider.ts +18 -6
- package/adapters/computer/interaction-target-authority.ts +39 -9
- package/adapters/mcp/access-mode.ts +18 -39
- package/adapters/mcp/auth.ts +2 -6
- package/adapters/mcp/instructions.ts +2 -2
- package/adapters/mcp/multi-repository.ts +7 -28
- package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
- package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
- package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
- package/adapters/mcp/runtime-gateway/router.ts +3 -5
- package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
- package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
- package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
- package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
- package/adapters/mcp/server.ts +24 -3
- package/adapters/mcp/setup.ts +4 -10
- package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
- package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
- package/adapters/mcp/toolset-names.ts +11 -42
- package/adapters/mcp/toolset.ts +16 -90
- package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
- package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
- package/adapters/mcp/transports/http-observation.ts +18 -6
- package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
- package/adapters/mcp/transports/http.ts +54 -14
- package/adapters/mcp/types.ts +0 -6
- package/assets/hooks/lib/session-state.sh +0 -38
- package/assets/hooks/post-bash.sh +1 -1
- package/assets/hooks/post-tool-observer.sh +0 -5
- package/assets/hooks/prompt-guard.sh +0 -55
- package/assets/partials/04-project-structure.partial.md +1 -1
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -1
- package/assets/partials-agents/08-deep-docs.partial.md +1 -1
- package/assets/reference-configs/agentic-development-flow.md +1 -1
- package/assets/reference-configs/external-tooling.md +0 -76
- package/assets/reference-configs/release-deploy.md +1 -1
- package/assets/templates/helpers/architecture-queue.sh +1 -1
- package/assets/templates/helpers/check-agent-tooling.sh +1 -327
- package/assets/templates/helpers/check-task-workflow.sh +1 -1
- package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
- package/assets/templates/helpers/migrate-project-template.sh +2 -2
- package/assets/workflow-contract.v1.json +0 -2
- package/docs/README.md +5 -2
- package/docs/architecture/CURRENT.md +114 -30
- package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
- package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
- package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
- package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
- package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
- package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
- package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
- package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
- package/docs/architecture/domains/public-surface.md +3 -5
- package/docs/architecture/domains/verification.md +5 -5
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
- package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
- package/docs/forge-plugin-management.md +14 -2
- package/docs/operations/controller-browser-plugin.md +23 -0
- package/docs/operations/features.md +6 -7
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/github-repository.md +4 -4
- package/docs/operations/github-semantic-release-projection.md +20 -0
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +13 -54
- package/docs/operations/runtime-performance-diagnostics.md +8 -0
- package/docs/operations/standalone-disaster-recovery.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/03-first-repository-task.md +2 -0
- package/docs/wiki/Computer-and-Plugins.md +78 -0
- package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
- package/docs/wiki/Home.md +16 -2
- package/docs/wiki/Implementation.md +24 -12
- package/docs/wiki/Installation.md +28 -32
- package/docs/wiki/Integrations.md +4 -0
- package/docs/wiki/Quick-Start.md +35 -22
- package/docs/wiki/Releases-and-Upgrades.md +15 -25
- package/docs/wiki/_Sidebar.md +5 -1
- package/package.json +1 -12
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
- package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
- package/packages/kernel/scheduler/application/eligibility.ts +4 -1
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
- package/packages/kernel/work/application/work-service.ts +1 -0
- package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
- package/packages/kernel/work/domain/state-machine.ts +9 -2
- package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
- package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
- package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
- package/scripts/assemble-template.ts +11 -72
- package/scripts/check-mcp-compatibility.ts +55 -74
- package/scripts/check-package-identity.mjs +0 -5
- package/scripts/check-runtime-architecture.mjs +305 -28
- package/scripts/init-project.sh +0 -46
- package/scripts/install-ios-hid-toolchain.sh +60 -0
- package/scripts/install-standalone-recovery.ts +1 -1
- package/scripts/lib/project-init-lib.sh +3 -3
- package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
- package/scripts/open-source-audit-allowlist.txt +0 -1
- package/scripts/run-governed-gate.ts +2 -0
- package/scripts/smoke-ios-physical-device.ts +38 -10
- package/scripts/smoke-mcp-http-runtime.ts +7 -8
- package/scripts/smoke-mcp-tool-surface.ts +27 -41
- package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
- package/scripts/sync-generated-authority.mjs +0 -1
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/agent-jobs/integration.ts +2 -2
- package/src/cli/chatgpt-browser/file-policy.ts +2 -2
- package/src/cli/commands/doctor.ts +1 -143
- package/src/cli/commands/global-runtime.ts +3 -196
- package/src/cli/commands/init-hook.ts +6 -23
- package/src/cli/commands/init.ts +3 -252
- package/src/cli/commands/mcp.ts +0 -5
- package/src/cli/commands/prompt-guard-decision.ts +0 -2
- package/src/cli/commands/recovery.ts +2 -14
- package/src/cli/commands/repository.ts +17 -1
- package/src/cli/commands/setup-execution.ts +41 -0
- package/src/cli/controller/completion-orchestrator.ts +1 -1
- package/src/cli/controller/composite-operations.ts +1 -1
- package/src/cli/controller/context/instruction-context.ts +1 -1
- package/src/cli/controller/context/known-paths.ts +2 -2
- package/src/cli/controller/context/query-planning.ts +11 -4
- package/src/cli/controller/context/rule-resolution.ts +2 -2
- package/src/cli/controller/context/source-materializer.ts +2 -2
- package/src/cli/controller/context/structural-provider.ts +84 -0
- package/src/cli/controller/context/structural.ts +6 -6
- package/src/cli/controller/context/types.ts +8 -9
- package/src/cli/controller/context-pack.ts +97 -76
- package/src/cli/controller/operational-plan.ts +1 -1
- package/src/cli/controller/systemd-user.ts +4 -10
- package/src/cli/editing/edit-session.ts +17 -5
- package/src/cli/github/github.ts +4 -4
- package/src/cli/github/semantic-projection.ts +174 -0
- package/src/cli/hook/prompt-intents.ts +0 -9
- package/src/cli/index.ts +1 -43
- package/src/cli/local-bridge/connector-freshness.ts +1 -12
- package/src/cli/local-bridge/console-automations.ts +1 -1
- package/src/cli/local-bridge/console-view-models.ts +1 -1
- package/src/cli/local-bridge/facade-api.ts +0 -2
- package/src/cli/repositories/command-classifier.ts +0 -9
- package/src/cli/repositories/command-scope.ts +2 -2
- package/src/cli/repositories/locks.ts +20 -0
- package/src/cli/repositories/registry.ts +2 -1
- package/src/cli/repositories/safe-patch.ts +2 -12
- package/src/cli/repositories/structured-git.ts +39 -3
- package/src/cli/repository/inspector.ts +185 -343
- package/src/effects/process-runner.ts +17 -1
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +7 -0
- package/src/runtime/context/semantic-navigation.ts +1 -1
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
- package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
- package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
- package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
- package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
- package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
- package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
- package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
- package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
- package/src/runtime/control-plane/governance/external-effects.ts +1 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
- package/src/runtime/evidence/sensitive-output.ts +22 -2
- package/src/runtime/execution/jobs/child-reference.ts +11 -0
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
- package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
- package/src/runtime/execution/process-runtime/runtime.ts +20 -16
- package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
- package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
- package/src/runtime/execution/workers/executor.ts +1 -2
- package/src/runtime/maintenance/cleanup.ts +41 -5
- package/src/runtime/plugins/action-application.ts +12 -1
- package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
- package/src/runtime/plugins/browser-adapter.ts +381 -313
- package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
- package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
- package/src/runtime/plugins/browser-registration.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +26 -35
- package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
- package/src/runtime/plugins/computer-registration.ts +76 -9
- package/src/runtime/plugins/github-adapter.ts +30 -3
- package/src/runtime/plugins/gmail-adapter.ts +166 -1
- package/src/runtime/plugins/google-shared.ts +10 -5
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
- package/src/runtime/plugins/ios-adapter.ts +45 -40
- package/src/runtime/plugins/ios-agent-device.ts +54 -4
- package/src/runtime/plugins/ios-device-ownership.ts +103 -0
- package/src/runtime/plugins/ios-physical-device.ts +497 -79
- package/src/runtime/plugins/local-system-adapter.ts +1 -99
- package/src/runtime/plugins/store.ts +80 -7
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/projections/git-status-sampler.ts +2 -19
- package/src/runtime/recovery/actions.ts +1 -1
- package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
- package/src/runtime/recovery/probe.ts +2 -2
- package/src/runtime/release/release-coordinator.ts +20 -4
- package/src/runtime/root/controller-progression-composition.ts +67 -5
- package/src/runtime/root/package-connector-service.ts +123 -11
- package/src/runtime/root/package-runtime-release.ts +6 -18
- package/src/runtime/root/package-runtime-service.ts +7 -13
- package/src/runtime/root/release-execution-canary.ts +18 -5
- package/src/runtime/root/release-manifest.ts +0 -34
- package/src/runtime/root/release-materialize.ts +0 -78
- package/src/runtime/root/runtime.ts +1 -2
- package/src/runtime/root/scheduled-controller-composition.ts +2 -1
- package/src/runtime/root/service.ts +90 -11
- package/src/runtime/root/types.ts +0 -6
- package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
- package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
- package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
- package/src/runtime/safe-tooling/ios-development.ts +22 -8
- package/src/runtime/safe-tooling/job-summary.ts +1 -1
- package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
- package/src/runtime/shared/atomic-text-file.ts +21 -0
- package/src/runtime/shared/json-files.ts +8 -3
- package/src/runtime/standalone-recovery/core.ts +189 -174
- package/src/runtime/standalone-recovery/entry.ts +62 -30
- package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
- package/src/runtime/workflow/schedules/engine.ts +36 -14
- package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
- package/supervisor/client.ts +31 -14
- package/supervisor/control-plane.ts +210 -115
- package/supervisor/forge-validators.ts +88 -21
- package/supervisor/native-browser-adapter.ts +393 -28
- package/supervisor/native-messaging/host.ts +21 -2
- package/supervisor/protocol.ts +74 -26
- package/supervisor/server.ts +16 -8
- package/supervisor/store.ts +476 -57
- package/supervisor/types.ts +19 -13
- package/SKILL.md +0 -427
- package/adapters/computer/chatgpt-target-router.ts +0 -103
- package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
- package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
- package/adapters/mcp/toolset-selection.ts +0 -41
- package/agents/openai.yaml +0 -4
- package/assets/skill-hooks.json +0 -41
- package/assets/skill-version.json +0 -189
- package/assets/skills/claude-review/SKILL.md +0 -231
- package/assets/skills/codex-review/SKILL.md +0 -103
- package/assets/templates/helpers/check-skill-version.ts +0 -193
- package/scripts/check-skill-version.ts +0 -5
- package/scripts/ensure-codegraph.sh +0 -16
- package/scripts/run-skill-hook.ts +0 -244
- package/scripts/sync-codex-installed-copies.sh +0 -182
- package/src/cli/commands/tools.ts +0 -167
- package/src/cli/mcp/access-mode.ts +0 -2
- package/src/cli/mcp/access-tools.ts +0 -2
- package/src/cli/mcp/audit.ts +0 -2
- package/src/cli/mcp/auth.ts +0 -2
- package/src/cli/mcp/instructions.ts +0 -2
- package/src/cli/mcp/legacy-context.ts +0 -2
- package/src/cli/mcp/legacy-tool-service.ts +0 -2
- package/src/cli/mcp/multi-repository.ts +0 -2
- package/src/cli/mcp/oauth.ts +0 -2
- package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
- package/src/cli/mcp/paths.ts +0 -2
- package/src/cli/mcp/policy.ts +0 -2
- package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
- package/src/cli/mcp/proxy-env.ts +0 -2
- package/src/cli/mcp/readiness-projection.ts +0 -2
- package/src/cli/mcp/redaction.ts +0 -2
- package/src/cli/mcp/repo.ts +0 -2
- package/src/cli/mcp/repository-tools.ts +0 -2
- package/src/cli/mcp/server.ts +0 -2
- package/src/cli/mcp/setup.ts +0 -2
- package/src/cli/mcp/tool-capability-cache.ts +0 -2
- package/src/cli/mcp/tool-contract.ts +0 -2
- package/src/cli/mcp/tools.ts +0 -2
- package/src/cli/mcp/toolset-names.ts +0 -2
- package/src/cli/mcp/toolset-selection.ts +0 -2
- package/src/cli/mcp/toolset.ts +0 -2
- package/src/cli/mcp/transports/http.ts +0 -2
- package/src/cli/mcp/transports/session-registry.ts +0 -2
- package/src/cli/mcp/transports/stdio.ts +0 -2
- package/src/cli/mcp/types.ts +0 -2
- package/src/cli/mcp/umbrella-tools.ts +0 -2
- package/src/cli/tools/codegraph.ts +0 -695
- package/src/core/adoption/helper-wrapper-plan.ts +0 -159
- package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
- package/src/runtime/context/codegraph-read-provider.ts +0 -611
- package/src/runtime/context/codegraph-sidecar.cjs +0 -248
- package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
- package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
- package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
- package/src/runtime/gateway/mcp/process-tools.ts +0 -2
- package/src/runtime/gateway/mcp/router.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
- package/src/runtime/plugins/browser-provider-registry.ts +0 -2
- package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from 'child_process';
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { basename, extname } from 'path';
|
|
5
5
|
import {
|
|
@@ -16,6 +16,7 @@ import type { BrowserNativeAttachAttempt, BrowserNativeProduct } from '../../../
|
|
|
16
16
|
const RECORD_SEPARATOR = String.fromCharCode(30);
|
|
17
17
|
const DEFAULT_NATIVE_TIMEOUT_MS = 5_000;
|
|
18
18
|
const MAX_NATIVE_OUTPUT_BYTES = 1_048_576;
|
|
19
|
+
const MAX_NATIVE_BROWSER_FILE_ATTACH_BYTES = 16 * 1024 * 1024;
|
|
19
20
|
|
|
20
21
|
export type MacOsBrowserProduct = BrowserNativeProduct;
|
|
21
22
|
|
|
@@ -136,6 +137,8 @@ const defaultRuntimeHooks: MacOsBrowserRuntimeHooks = {
|
|
|
136
137
|
appExists: (path) => existsSync(path),
|
|
137
138
|
processRunning: async (processName, timeoutMs) => {
|
|
138
139
|
try {
|
|
140
|
+
// Process existence only: the canonical Computer provider owns exact
|
|
141
|
+
// GUI PID selection and rejects ambiguous or unverified targets.
|
|
139
142
|
await execFileText('/usr/bin/pgrep', ['-x', processName], timeoutMs);
|
|
140
143
|
return true;
|
|
141
144
|
} catch (error) {
|
|
@@ -222,6 +225,7 @@ async function runCreateTabAutomation(
|
|
|
222
225
|
testArgs: string[],
|
|
223
226
|
timeoutMs: number,
|
|
224
227
|
): Promise<MacOsBrowserCreateTabEvidence> {
|
|
228
|
+
await assertMacOsBrowserApplicationRunning(request.product, timeoutMs);
|
|
225
229
|
if (runtimeHooks.runAppleScript) {
|
|
226
230
|
const value = await runtimeHooks.runAppleScript(testScript, testArgs, timeoutMs);
|
|
227
231
|
return parseMacOsBrowserCreateTabBrokerResult({ value });
|
|
@@ -338,6 +342,7 @@ async function sendBrowserTrustedInput(
|
|
|
338
342
|
request: BrowserAutomationTrustedInputRequest,
|
|
339
343
|
timeoutMs: number,
|
|
340
344
|
): Promise<void> {
|
|
345
|
+
await assertMacOsBrowserApplicationRunning(request.product, timeoutMs);
|
|
341
346
|
if (runtimeHooks.trustedInput) {
|
|
342
347
|
await runtimeHooks.trustedInput(request, timeoutMs);
|
|
343
348
|
return;
|
|
@@ -647,13 +652,15 @@ if preferredWindowId is not "" then
|
|
|
647
652
|
end if
|
|
648
653
|
end repeat
|
|
649
654
|
end if
|
|
655
|
+
if preferredWindowId is not "" and targetWindow is missing value then error "FORGE_BROWSER_PREFERRED_WINDOW_MISSING:" & preferredWindowId
|
|
650
656
|
if targetWindow is missing value and (count of windows) is greater than 0 then set targetWindow to front window
|
|
651
657
|
if targetWindow is missing value then
|
|
652
658
|
set targetWindow to make new window
|
|
653
659
|
set targetTab to active tab of targetWindow
|
|
654
660
|
set URL of targetTab to targetUrl
|
|
655
661
|
else
|
|
656
|
-
|
|
662
|
+
-- Preserve the selected background window; front-window reassignment here
|
|
663
|
+
-- previously discarded the caller's stable target and opened the wrong tab.
|
|
657
664
|
set originalActiveTabId to ((id of active tab of targetWindow) as text)
|
|
658
665
|
set targetTab to make new tab at end of tabs of targetWindow with properties {URL:targetUrl}
|
|
659
666
|
-- Restore the previously active tab by identity, never by a stale position:
|
|
@@ -946,12 +953,25 @@ export class MacOsAppleEventsPage {
|
|
|
946
953
|
return await runBrowserAutomationText(effectiveRequest, effectiveScript, args, timeoutMs);
|
|
947
954
|
} catch (error) {
|
|
948
955
|
const message = error instanceof Error ? error.message : String(error);
|
|
949
|
-
//
|
|
950
|
-
//
|
|
951
|
-
//
|
|
952
|
-
const
|
|
956
|
+
// Preserve the provider's *typed* process-identity refusal. In particular,
|
|
957
|
+
// do not turn ambiguous Chrome app roots into a retryable generic error:
|
|
958
|
+
// retries must never guess another user's running browser instance.
|
|
959
|
+
const nativeCode = error instanceof AssistantPluginError ? error.code : undefined;
|
|
960
|
+
const nativeFailure = nativeCode === 'BROWSER_AUTOMATION_PROCESS_TARGET_AMBIGUOUS'
|
|
961
|
+
|| nativeCode === 'BROWSER_AUTOMATION_PROCESS_IDENTITY_LOST'
|
|
962
|
+
|| nativeCode === 'BROWSER_AUTOMATION_PROCESS_PROBE_UNAVAILABLE'
|
|
963
|
+
|| nativeCode === 'BROWSER_AUTOMATION_NATIVE_EVENT_FAILED'
|
|
964
|
+
|| nativeCode === 'BROWSER_AUTOMATION_MUTATION_OUTCOME_UNKNOWN'
|
|
965
|
+
? nativeCode : undefined;
|
|
966
|
+
const nonRetryable = nativeFailure && nativeFailure !== 'BROWSER_AUTOMATION_PROCESS_PROBE_UNAVAILABLE';
|
|
967
|
+
// An Apple Event can mutate the page even if its callback fails.
|
|
968
|
+
// Preserve uncertainty and never retry such a mutation blindly.
|
|
969
|
+
const outcomeUnknown = nativeFailure === 'BROWSER_AUTOMATION_MUTATION_OUTCOME_UNKNOWN'
|
|
970
|
+
|| (error instanceof AssistantPluginError && error.effectOutcome === 'outcome_unknown');
|
|
971
|
+
const brokerFailure = nativeFailure ?? /\b(BROWSER_AUTOMATION_TIMEOUT|BROWSER_AUTOMATION_SERIALIZATION_BUSY)\b/.exec(message)?.[1];
|
|
953
972
|
throw new AssistantPluginError(brokerFailure ?? 'PLUGIN_BROWSER_NATIVE_OPERATION_FAILED', `${this.browser.appName} Apple Events operation failed: ${message}`, {
|
|
954
|
-
retryable:
|
|
973
|
+
retryable: !nonRetryable && !outcomeUnknown,
|
|
974
|
+
effectOutcome: outcomeUnknown ? 'outcome_unknown' : 'failed',
|
|
955
975
|
details: { browserProduct: this.browser.product, bundleId: this.browser.bundleId },
|
|
956
976
|
});
|
|
957
977
|
}
|
|
@@ -980,8 +1000,12 @@ export class MacOsAppleEventsPage {
|
|
|
980
1000
|
if (macOsBrowserJavaScriptAutomationDisabled(error)) {
|
|
981
1001
|
throw new AssistantPluginError(
|
|
982
1002
|
'PLUGIN_BROWSER_JAVASCRIPT_PERMISSION_REQUIRED',
|
|
983
|
-
`${this.browser.appName}
|
|
984
|
-
{ retryable: false, details: {
|
|
1003
|
+
`${this.browser.appName} rejected JavaScript from Apple Events for the browser instance addressed by macOS automation. This does not prove that the user's visible Chrome instance has this setting disabled. Verify the exact Chrome process/profile and View > Developer > Allow JavaScript from Apple Events before changing settings.`,
|
|
1004
|
+
{ retryable: false, details: {
|
|
1005
|
+
browserProduct: this.browser.product, bundleId: this.browser.bundleId,
|
|
1006
|
+
processIdentity: 'unverified',
|
|
1007
|
+
...(this.targetRef ? { windowId: this.targetRef.windowId, tabId: this.targetRef.tabId } : {}),
|
|
1008
|
+
} },
|
|
985
1009
|
);
|
|
986
1010
|
}
|
|
987
1011
|
throw error;
|
|
@@ -1119,15 +1143,16 @@ export class MacOsAppleEventsPage {
|
|
|
1119
1143
|
if (filePaths.length === 0 || filePaths.length > 32) {
|
|
1120
1144
|
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Native browser file input accepts 1-32 local files.', { retryable: false });
|
|
1121
1145
|
}
|
|
1122
|
-
|
|
1146
|
+
|
|
1147
|
+
// Validate the entire batch before sending bytes or changing input.files.
|
|
1148
|
+
const attachments = filePaths.map((filePath) => {
|
|
1123
1149
|
if (!filePath || !existsSync(filePath)) {
|
|
1124
1150
|
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Local file does not exist.', { retryable: false });
|
|
1125
1151
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
const bytes = readFileSync(filePath);
|
|
1152
|
+
const metadata = statSync(filePath);
|
|
1153
|
+
if (!metadata.isFile()) {
|
|
1154
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Native browser can attach only regular files.', { retryable: false });
|
|
1155
|
+
}
|
|
1131
1156
|
const fileName = basename(filePath);
|
|
1132
1157
|
const extension = extname(fileName).toLowerCase();
|
|
1133
1158
|
const mimeType = extension === '.pdf' ? 'application/pdf'
|
|
@@ -1135,123 +1160,98 @@ export class MacOsAppleEventsPage {
|
|
|
1135
1160
|
: extension === '.png' ? 'image/png'
|
|
1136
1161
|
: extension === '.jpg' || extension === '.jpeg' ? 'image/jpeg'
|
|
1137
1162
|
: 'application/octet-stream';
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1163
|
+
return { filePath, fileName, size: metadata.size, mimeType };
|
|
1164
|
+
});
|
|
1165
|
+
const totalBytes = attachments.reduce((sum, attachment) => sum + attachment.size, 0);
|
|
1166
|
+
if (totalBytes > MAX_NATIVE_BROWSER_FILE_ATTACH_BYTES) {
|
|
1167
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_TOO_LARGE',
|
|
1168
|
+
'Native browser upload is bounded to 16 MiB per batch; select a smaller batch instead of queuing unbounded Apple Events.',
|
|
1169
|
+
{ retryable: false, details: { totalBytes, limitBytes: MAX_NATIVE_BROWSER_FILE_ATTACH_BYTES } });
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// A read-only DOM preflight must pass before staging starts. In particular,
|
|
1173
|
+
// multi-file uploads cannot reset a previous file selection on preflight failure.
|
|
1174
|
+
await this.evaluate(`(() => { // __forgeFileInputPreflight
|
|
1175
|
+
const matches = document.querySelectorAll(${JSON.stringify(selector)});
|
|
1176
|
+
if (matches.length !== 1) throw new Error('File input selector must resolve to exactly one element.');
|
|
1177
|
+
const element = matches[0];
|
|
1178
|
+
if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
|
|
1179
|
+
if (element.disabled) throw new Error('Target file input is disabled.');
|
|
1180
|
+
if (${attachments.length > 1} && !element.multiple) throw new Error('Target input does not allow multiple files.');
|
|
1181
|
+
return true;
|
|
1182
|
+
})()`);
|
|
1183
|
+
|
|
1184
|
+
const batchKey = `__forgeStagedFiles_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
1185
|
+
const bufferKey = `__forgeEncodedFile_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
1186
|
+
const expected = attachments.map(({ fileName, size }) => ({ name: fileName, size }));
|
|
1187
|
+
const chunkChars = 192 * 1024;
|
|
1188
|
+
try {
|
|
1189
|
+
for (const attachment of attachments) {
|
|
1190
|
+
// Stage one file at a time. No DOM selection or change event occurs
|
|
1191
|
+
// until the complete batch has arrived and its sizes have been checked.
|
|
1192
|
+
const encoded = readFileSync(attachment.filePath).toString('base64');
|
|
1142
1193
|
await this.evaluate(`(() => { globalThis[${JSON.stringify(bufferKey)}] = ''; return true; })()`);
|
|
1143
1194
|
for (let offset = 0; offset < encoded.length; offset += chunkChars) {
|
|
1144
1195
|
const chunk = encoded.slice(offset, offset + chunkChars);
|
|
1145
1196
|
await this.evaluate(`(() => { globalThis[${JSON.stringify(bufferKey)}] += ${JSON.stringify(chunk)}; return true; })()`);
|
|
1146
1197
|
}
|
|
1147
|
-
const
|
|
1148
|
-
const element = document.querySelector(${JSON.stringify(selector)});
|
|
1149
|
-
if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
|
|
1150
|
-
const expectedName = ${JSON.stringify(fileName)};
|
|
1151
|
-
const expectedSize = ${bytes.length};
|
|
1198
|
+
const staged = await this.evaluate<{ name?: string; size?: number }>(`(() => { // __forgeFileInputStaged
|
|
1152
1199
|
const encoded = String(globalThis[${JSON.stringify(bufferKey)}] || '');
|
|
1153
1200
|
const binary = atob(encoded);
|
|
1154
1201
|
const data = new Uint8Array(binary.length);
|
|
1155
1202
|
for (let i = 0; i < binary.length; i += 1) data[i] = binary.charCodeAt(i);
|
|
1156
|
-
const file = new File([data],
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
const selected = element.files && element.files[0];
|
|
1163
|
-
return selected ? { name: selected.name, size: selected.size, expectedName, expectedSize } : null;
|
|
1203
|
+
const file = new File([data], ${JSON.stringify(attachment.fileName)}, { type: ${JSON.stringify(attachment.mimeType)} });
|
|
1204
|
+
const staged = globalThis[${JSON.stringify(batchKey)}] || [];
|
|
1205
|
+
staged.push(file);
|
|
1206
|
+
globalThis[${JSON.stringify(batchKey)}] = staged;
|
|
1207
|
+
delete globalThis[${JSON.stringify(bufferKey)}];
|
|
1208
|
+
return { name: file.name, size: file.size };
|
|
1164
1209
|
})()`);
|
|
1165
|
-
if (
|
|
1166
|
-
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_ATTACH_FAILED',
|
|
1167
|
-
retryable:
|
|
1168
|
-
details: { expectedName: fileName, expectedSize: bytes.length, actualName: attached?.name, actualSize: attached?.size },
|
|
1169
|
-
});
|
|
1210
|
+
if (staged?.name !== attachment.fileName || staged?.size !== attachment.size) {
|
|
1211
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_ATTACH_FAILED',
|
|
1212
|
+
'Browser did not stage the expected local file.', { retryable: false });
|
|
1170
1213
|
}
|
|
1171
|
-
} finally {
|
|
1172
|
-
await this.evaluate(`(() => { try { delete globalThis[${JSON.stringify(bufferKey)}]; } catch {} return true; })()`).catch(() => undefined);
|
|
1173
1214
|
}
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
1215
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
if (attachments.length > 1) {
|
|
1195
|
-
await this.evaluate(`(() => {
|
|
1196
|
-
const element = document.querySelector(${JSON.stringify(selector)});
|
|
1197
|
-
if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
|
|
1198
|
-
if (!element.multiple) throw new Error('Target input does not allow multiple files.');
|
|
1216
|
+
// One commit to input.files and one event pair, after all File objects
|
|
1217
|
+
// are staged and verified. Failed transfer never partially selects files.
|
|
1218
|
+
const selected = await this.evaluate<Array<{ name?: string; size?: number }>>(`(() => { // __forgeFileInputCommit
|
|
1219
|
+
const matches = document.querySelectorAll(${JSON.stringify(selector)});
|
|
1220
|
+
if (matches.length !== 1) throw new Error('File input selector changed during transfer.');
|
|
1221
|
+
const element = matches[0];
|
|
1222
|
+
if (!(element instanceof HTMLInputElement) || element.type !== 'file' || element.disabled) {
|
|
1223
|
+
throw new Error('File input became unavailable during transfer.');
|
|
1224
|
+
}
|
|
1225
|
+
if (${attachments.length > 1} && !element.multiple) throw new Error('File input no longer supports multiple files.');
|
|
1226
|
+
const staged = globalThis[${JSON.stringify(batchKey)}];
|
|
1227
|
+
const expected = ${JSON.stringify(expected)};
|
|
1228
|
+
if (!Array.isArray(staged) || staged.length !== expected.length ||
|
|
1229
|
+
!staged.every((file, i) => file instanceof File && file.name === expected[i].name && file.size === expected[i].size)) {
|
|
1230
|
+
throw new Error('Staged file batch does not match the requested selection.');
|
|
1231
|
+
}
|
|
1199
1232
|
const transfer = new DataTransfer();
|
|
1233
|
+
staged.forEach((file) => transfer.items.add(file));
|
|
1200
1234
|
element.files = transfer.files;
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
const chunkChars = 192 * 1024;
|
|
1206
|
-
try {
|
|
1207
|
-
for (let index = 0; index < attachments.length; index += 1) {
|
|
1208
|
-
const attachment = attachments[index];
|
|
1209
|
-
const encoded = attachment.bytes.toString('base64');
|
|
1210
|
-
await this.evaluate(`(() => { globalThis[${JSON.stringify(attachment.bufferKey)}] = ''; return true; })()`);
|
|
1211
|
-
for (let offset = 0; offset < encoded.length; offset += chunkChars) {
|
|
1212
|
-
const chunk = encoded.slice(offset, offset + chunkChars);
|
|
1213
|
-
await this.evaluate(`(() => { globalThis[${JSON.stringify(attachment.bufferKey)}] += ${JSON.stringify(chunk)}; return true; })()`);
|
|
1235
|
+
const selected = Array.from(element.files || []).map((file) => ({ name: file.name, size: file.size }));
|
|
1236
|
+
if (selected.length !== expected.length ||
|
|
1237
|
+
!selected.every((file, i) => file.name === expected[i].name && file.size === expected[i].size)) {
|
|
1238
|
+
throw new Error('Browser did not retain the requested file selection.');
|
|
1214
1239
|
}
|
|
1215
|
-
const append = attachments.length > 1 && index > 0;
|
|
1216
|
-
await this.evaluate(`(() => {
|
|
1217
|
-
const element = document.querySelector(${JSON.stringify(selector)});
|
|
1218
|
-
if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
|
|
1219
|
-
const expectedName = ${JSON.stringify(attachment.fileName)};
|
|
1220
|
-
const expectedSize = ${attachment.bytes.length};
|
|
1221
|
-
const encoded = String(globalThis[${JSON.stringify(attachment.bufferKey)}] || '');
|
|
1222
|
-
const binary = atob(encoded);
|
|
1223
|
-
const data = new Uint8Array(binary.length);
|
|
1224
|
-
for (let i = 0; i < binary.length; i += 1) data[i] = binary.charCodeAt(i);
|
|
1225
|
-
const file = new File([data], expectedName, { type: ${JSON.stringify(attachment.mimeType)} });
|
|
1226
|
-
const transfer = new DataTransfer();
|
|
1227
|
-
if (${append ? 'true' : 'false'}) {
|
|
1228
|
-
for (const existing of Array.from(element.files || [])) transfer.items.add(existing);
|
|
1229
|
-
}
|
|
1230
|
-
transfer.items.add(file);
|
|
1231
|
-
element.files = transfer.files;
|
|
1232
|
-
return Array.from(element.files || []).map((selected) => ({ name: selected.name, size: selected.size }));
|
|
1233
|
-
})()`);
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
const selected = await this.evaluate<Array<{ name?: string; size?: number }>>(`(() => {
|
|
1237
|
-
const element = document.querySelector(${JSON.stringify(selector)});
|
|
1238
|
-
if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
|
|
1239
1240
|
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1240
1241
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1241
|
-
return
|
|
1242
|
+
return selected;
|
|
1242
1243
|
})()`);
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
retryable: true,
|
|
1248
|
-
details: { expected, actual: selected },
|
|
1249
|
-
});
|
|
1244
|
+
if (selected.length !== expected.length ||
|
|
1245
|
+
!expected.every((item, index) => selected[index]?.name === item.name && selected[index]?.size === item.size)) {
|
|
1246
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_ATTACH_FAILED',
|
|
1247
|
+
'Native browser did not retain the requested local files on the target input.', { retryable: false });
|
|
1250
1248
|
}
|
|
1251
1249
|
} finally {
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1250
|
+
// Unknown effect outcomes must be reconciled by observation, never by replay.
|
|
1251
|
+
await this.evaluate(`(() => {
|
|
1252
|
+
try { delete globalThis[${JSON.stringify(bufferKey)}]; delete globalThis[${JSON.stringify(batchKey)}]; } catch {}
|
|
1253
|
+
return true;
|
|
1254
|
+
})()`).catch(() => undefined);
|
|
1255
1255
|
}
|
|
1256
1256
|
}
|
|
1257
1257
|
|
|
@@ -1581,7 +1581,9 @@ async function resolveCurrentMacOsBrowserTabRef(
|
|
|
1581
1581
|
? await runtimeHooks.tabInventory(product, timeoutMs)
|
|
1582
1582
|
: runtimeHooks.runAppleScript
|
|
1583
1583
|
? undefined
|
|
1584
|
-
|
|
1584
|
+
// Stable identity needs the complete inventory, not UI-current flags.
|
|
1585
|
+
// Input/activation checks read their exact target's foreground state.
|
|
1586
|
+
: await listMacOsBrowserTabs(product, timeoutMs, { correctFrontmost: false });
|
|
1585
1587
|
if (!inventory) return ref;
|
|
1586
1588
|
|
|
1587
1589
|
const matches = inventory.tabs.filter((candidate) => candidate.tabId === ref.tabId);
|
|
@@ -616,13 +616,14 @@ export function browserActions(): AssistantPluginActionDescriptor[] {
|
|
|
616
616
|
{
|
|
617
617
|
actionId: 'attach_local_file',
|
|
618
618
|
title: 'Attach local file(s)',
|
|
619
|
-
description: '
|
|
619
|
+
description: 'Attach allowed local files to input[type=file]. Paths are repository-relative by default; optional target_key makes them relative to an existing caller-owned local-system read grant (e.g. Downloads). Does not drive a macOS Open Panel or submit the upload form.',
|
|
620
620
|
readOnly: false, risk: 'remote_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
621
621
|
scopes: ['browser.interact', 'browser.profile'], resourceClaims: writeRemote,
|
|
622
622
|
argumentsSchema: interactSchema({
|
|
623
623
|
selector: { type: 'string' },
|
|
624
624
|
file_path: { type: 'string' },
|
|
625
625
|
file_paths: { type: 'array', minItems: 1, maxItems: 32, items: { type: 'string' } },
|
|
626
|
+
target_key: { type: 'string' },
|
|
626
627
|
}, ['selector']),
|
|
627
628
|
},
|
|
628
629
|
{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildBrowserPluginManifest,
|
|
3
|
+
normalizeLegacyBrowserConfig,
|
|
3
4
|
executeBrowserPluginAction,
|
|
4
5
|
resolveBrowserPluginAuthorizationContext,
|
|
5
6
|
} from './browser-adapter';
|
|
@@ -24,6 +25,7 @@ export const browserPluginAdapter: AssistantPluginAdapter = {
|
|
|
24
25
|
scope: 'controller_with_repository_overlay',
|
|
25
26
|
exposure: 'internal',
|
|
26
27
|
buildManifest: buildBrowserPluginManifest,
|
|
28
|
+
normalizeLegacyConfig: normalizeLegacyBrowserConfig,
|
|
27
29
|
executeAction: executeBrowserPluginAction,
|
|
28
30
|
resolveAuthorizationContext: resolveBrowserPluginAuthorizationContext,
|
|
29
31
|
shouldRefreshManifestAfterAction(actionId) {
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
BrowserProviderRegistry,
|
|
13
13
|
BrowserProviderSelectionError,
|
|
14
|
-
BrowserProviderUnavailableBeforeActionError,
|
|
15
14
|
type BrowserRuntimeProvider,
|
|
16
15
|
} from '../../../packages/plugin-runtime/browser/provider-registry';
|
|
17
16
|
|
|
@@ -197,42 +196,34 @@ export async function executeBrowserRuntimeAction(options: {
|
|
|
197
196
|
input: AssistantPluginActionExecutionInput;
|
|
198
197
|
providers: readonly BrowserRuntimeProvider<AssistantPluginActionExecutionInput, Record<string, unknown>>[];
|
|
199
198
|
}): Promise<Record<string, unknown>> {
|
|
199
|
+
if (options.providers.length !== 1) {
|
|
200
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_PROVIDER_CARDINALITY_INVALID',
|
|
201
|
+
'One Browser action must bind exactly one provider before dispatch.', { retryable: false });
|
|
202
|
+
}
|
|
200
203
|
const registry = registryFor(options.runtimeKey);
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
registry.register(options.providers[0]!);
|
|
203
205
|
const policy = browserRuntimeActionPolicy(options.input.actionId);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
});
|
|
214
|
-
} catch (error) {
|
|
215
|
-
if (error instanceof BrowserProviderSelectionError) {
|
|
216
|
-
throw new AssistantPluginError(error.code, error.message, { retryable: error.retryable });
|
|
217
|
-
}
|
|
218
|
-
throw error;
|
|
219
|
-
}
|
|
220
|
-
const transaction = transactionFor(options.input, selection.provider.providerId, policy);
|
|
221
|
-
const validationErrors = validateBrowserTransaction(transaction);
|
|
222
|
-
if (validationErrors.length > 0) {
|
|
223
|
-
throw new AssistantPluginError('PLUGIN_BROWSER_TRANSACTION_INVALID', validationErrors[0]!, {
|
|
224
|
-
retryable: false,
|
|
225
|
-
details: { transactionId: transaction.transactionId, errors: validationErrors },
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
return await selection.provider.execute(options.input);
|
|
231
|
-
} catch (error) {
|
|
232
|
-
if (!(error instanceof BrowserProviderUnavailableBeforeActionError)) throw error;
|
|
233
|
-
excluded.add(selection.provider.providerId);
|
|
234
|
-
// This is provider reselection, not action replay: the typed error proves the rejected provider
|
|
235
|
-
// never attempted the action. Unknown outcomes from non-idempotent actions always propagate.
|
|
206
|
+
let selection;
|
|
207
|
+
try {
|
|
208
|
+
selection = await registry.select({
|
|
209
|
+
input: options.input,
|
|
210
|
+
requiredCapabilities: policy.requiredCapabilities,
|
|
211
|
+
foreground: policy.foreground,
|
|
212
|
+
});
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (error instanceof BrowserProviderSelectionError) {
|
|
215
|
+
throw new AssistantPluginError(error.code, error.message, { retryable: error.retryable });
|
|
236
216
|
}
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
const transaction = transactionFor(options.input, selection.provider.providerId, policy);
|
|
220
|
+
const validationErrors = validateBrowserTransaction(transaction);
|
|
221
|
+
if (validationErrors.length > 0) {
|
|
222
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_TRANSACTION_INVALID', validationErrors[0]!, {
|
|
223
|
+
retryable: false,
|
|
224
|
+
details: { transactionId: transaction.transactionId, errors: validationErrors },
|
|
225
|
+
});
|
|
237
226
|
}
|
|
227
|
+
// Provider failures are returned to the caller. Never silently switch target/provider.
|
|
228
|
+
return await selection.provider.execute(options.input);
|
|
238
229
|
}
|
|
@@ -112,8 +112,17 @@ function projectExtension(source: string, destination: string): void {
|
|
|
112
112
|
assertRegularFile(sourcePath, 'WORKFLOW_SUPERVISOR_EXTENSION_SOURCE_INVALID');
|
|
113
113
|
writeFileSync(join(staging, file), readFileSync(sourcePath), { mode: 0o600 });
|
|
114
114
|
}
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
// Preserve the previous projection until the staged replacement is ready.
|
|
116
|
+
const backup = destination + '.backup-' + randomUUID().slice(0, 12);
|
|
117
|
+
const hadDestination = existsSync(destination);
|
|
118
|
+
if (hadDestination) renameSync(destination, backup);
|
|
119
|
+
try {
|
|
120
|
+
renameSync(staging, destination);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (hadDestination) renameSync(backup, destination);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
if (hadDestination) rmSync(backup, { recursive: true, force: true });
|
|
117
126
|
} catch (error) {
|
|
118
127
|
rmSync(staging, { recursive: true, force: true });
|
|
119
128
|
throw error;
|
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
import { desktopOperatorActions } from './desktop-operator-registration';
|
|
32
32
|
import { getExternalPluginAdapter } from './external-adapter';
|
|
33
33
|
import { AssistantPluginError } from './errors';
|
|
34
|
+
import { executeIosPhysicalDeviceAction, iosPhysicalDeviceActions, iosPhysicalDeviceCapabilities, iosPhysicalDeviceStatus, isIosPhysicalDeviceAction, resolveIosPhysicalDeviceAuthorizationContext } from './ios-physical-device';
|
|
35
|
+
import { remoteXpcHidCachedStatus } from './ios/remote-xpc-hid';
|
|
34
36
|
import type {
|
|
35
37
|
AssistantPluginActionDescriptor,
|
|
36
38
|
AssistantPluginActionExecutionInput,
|
|
@@ -445,14 +447,37 @@ function buildComputerManifest(
|
|
|
445
447
|
repoRoot?: string,
|
|
446
448
|
context?: AssistantPluginBuildContext,
|
|
447
449
|
): AssistantPluginManifest {
|
|
448
|
-
const browser = buildBrowserPluginManifest(previousRevision, previousUpdatedAt, repoRoot, context);
|
|
450
|
+
const browser = buildBrowserPluginManifest(previousRevision, previousUpdatedAt, repoRoot, context, true);
|
|
451
|
+
const scriptedBrowserActions = new Set([
|
|
452
|
+
'configure', 'install_unpacked_extension', 'list_unpacked_extensions', 'clear_session', 'reconcile_sessions',
|
|
453
|
+
]);
|
|
454
|
+
const browserActions = browser.actions.filter((action) => !scriptedBrowserActions.has(action.actionId));
|
|
455
|
+
const browserCapabilities = browser.capabilities.map((capability) => ({
|
|
456
|
+
...capability,
|
|
457
|
+
actions: capability.actions.filter((action) => !scriptedBrowserActions.has(action)),
|
|
458
|
+
...(capability.capabilityId === 'browser-interaction'
|
|
459
|
+
? { description: 'Interact with existing Chrome or Vivaldi through native Computer; scripted Playwright is a separate explicit capability.' }
|
|
460
|
+
: {}),
|
|
461
|
+
...(capability.capabilityId === 'browser-session'
|
|
462
|
+
? { description: 'Discover, bind and reuse tabs of the user’s existing browser.' }
|
|
463
|
+
: {}),
|
|
464
|
+
...(capability.capabilityId === 'browser-human-handoff'
|
|
465
|
+
? { description: 'Present the exact existing native browser tab for manual interaction.' }
|
|
466
|
+
: {}),
|
|
467
|
+
})).filter((capability) => capability.actions.length > 0);
|
|
449
468
|
const desktop = desktopProviderManifest(context);
|
|
450
469
|
const platform = currentComputerPlatform();
|
|
451
470
|
const desktopSupported = platform === 'darwin';
|
|
452
471
|
const browserReady = browser.enabled && browser.health.ready;
|
|
453
472
|
const desktopReady = desktopSupported && desktop?.enabled === true && desktop.health.ready;
|
|
473
|
+
const physicalRepository = desktopSupported && context?.controllerScoped === false;
|
|
474
|
+
const physicalReady = physicalRepository && iosPhysicalDeviceStatus().coreDeviceReady;
|
|
475
|
+
// Passive manifest reads must never launch Python: input readiness is
|
|
476
|
+
// published only after an explicit HID action verified the exact toolchain.
|
|
477
|
+
const hidStatus = physicalReady && context ? remoteXpcHidCachedStatus(context.controllerHome) : undefined;
|
|
478
|
+
const physicalInputReady = physicalReady && hidStatus?.available === true;
|
|
454
479
|
const ready = browserReady && desktopReady;
|
|
455
|
-
const partial = browserReady || desktopReady;
|
|
480
|
+
const partial = browserReady || desktopReady || physicalReady;
|
|
456
481
|
const checkedAt = new Date().toISOString();
|
|
457
482
|
return {
|
|
458
483
|
...browser,
|
|
@@ -464,11 +489,14 @@ function buildComputerManifest(
|
|
|
464
489
|
strategy: 'derived',
|
|
465
490
|
duplicateStateAllowed: false,
|
|
466
491
|
sourceOfTruth: [
|
|
467
|
-
|
|
492
|
+
// Computer browser is built in and never reads a project's independent
|
|
493
|
+
// Browser-script configuration to decide activation or routing.
|
|
468
494
|
'controllerHome:sqlite/computer_interaction_target',
|
|
495
|
+
'controller-home:repository browser artifacts',
|
|
496
|
+
'controller-home:repository ios-device interactions and Controller-global UDID lock',
|
|
469
497
|
],
|
|
470
498
|
},
|
|
471
|
-
enabled: browser.enabled || desktop?.enabled === true,
|
|
499
|
+
enabled: browser.enabled || desktop?.enabled === true || physicalReady,
|
|
472
500
|
lifecycle: {
|
|
473
501
|
state: ready ? 'enabled' : partial ? 'degraded' : 'error',
|
|
474
502
|
reason: ready
|
|
@@ -488,6 +516,7 @@ function buildComputerManifest(
|
|
|
488
516
|
],
|
|
489
517
|
warnings: [
|
|
490
518
|
...browser.health.warnings,
|
|
519
|
+
...(physicalReady && !physicalInputReady ? [`Physical iPhone observation is ready, but RemoteXPC HID input requires Controller-owned pymobiledevice3 ${String(hidStatus?.toolchainVersion ?? 'toolchain')}.`] : []),
|
|
491
520
|
...(desktopSupported
|
|
492
521
|
? (desktopReady ? [] : ['Native Computer capability diagnostics are available through the internal platform provider.'])
|
|
493
522
|
: [`Native Desktop Computer capabilities are unsupported on ${platform}.`]),
|
|
@@ -496,11 +525,24 @@ function buildComputerManifest(
|
|
|
496
525
|
partial: !ready && partial,
|
|
497
526
|
browser: { ready: browserReady, state: browser.health.state },
|
|
498
527
|
desktop: { supported: desktopSupported, ready: desktopReady, state: desktop?.health.state ?? (desktopSupported ? 'not_installed' : 'unsupported') },
|
|
528
|
+
iphone: {
|
|
529
|
+
supported: desktopSupported, ready: physicalInputReady,
|
|
530
|
+
observationReady: physicalReady, inputReady: physicalInputReady,
|
|
531
|
+
toolchainVersion: hidStatus?.toolchainVersion,
|
|
532
|
+
provider: 'coredevice',
|
|
533
|
+
},
|
|
499
534
|
},
|
|
500
535
|
},
|
|
501
|
-
permissions: [...browser.permissions
|
|
502
|
-
|
|
503
|
-
|
|
536
|
+
permissions: [...browser.permissions.filter((permission) => permission.scope !== 'browser.extensions').map((permission) =>
|
|
537
|
+
permission.scope === 'browser.profile'
|
|
538
|
+
? { ...permission, description: 'Bind and persist exact existing native browser tab identities.' }
|
|
539
|
+
: permission), ...desktopProductPermissions(desktopReady),
|
|
540
|
+
...(physicalRepository ? [
|
|
541
|
+
{ scope: 'ios.discover' as const, mode: 'read' as const, description: 'Discover paired physical iPhones through CoreDevice.', granted: physicalReady, required: false },
|
|
542
|
+
{ scope: 'ios.device' as const, mode: 'write' as const, description: 'Use one exact iPhone through the existing CoreDevice/RemoteXPC owner.', granted: physicalReady, required: false },
|
|
543
|
+
] : [])],
|
|
544
|
+
capabilities: [...browserCapabilities, ...desktopProductCapabilities(), ...(physicalRepository ? iosPhysicalDeviceCapabilities() : [])],
|
|
545
|
+
actions: [...browserActions, ...desktopProductActions(), ...(physicalRepository ? iosPhysicalDeviceActions() : [])],
|
|
504
546
|
updatedAt: previousUpdatedAt ?? checkedAt,
|
|
505
547
|
};
|
|
506
548
|
}
|
|
@@ -1003,19 +1045,44 @@ async function resolveDesktopAuthorizationContext(input: AssistantPluginActionEx
|
|
|
1003
1045
|
return provider.resolveAuthorizationContext?.(providerInput(input, input.actionId, input.args));
|
|
1004
1046
|
}
|
|
1005
1047
|
|
|
1048
|
+
// Computer's browser subdomain always targets the user's real browser.
|
|
1049
|
+
// The independent Browser plugin retains explicit Playwright scripting modes,
|
|
1050
|
+
// but Computer must neither select nor silently fall back to those providers.
|
|
1051
|
+
function computerNativeBrowserInput(input: AssistantPluginActionExecutionInput): AssistantPluginActionExecutionInput {
|
|
1052
|
+
if (['configure', 'install_unpacked_extension', 'list_unpacked_extensions', 'clear_session', 'reconcile_sessions'].includes(input.actionId)) {
|
|
1053
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_SCRIPT_ACTION_SEPARATE',
|
|
1054
|
+
'Browser profile configuration and script-managed extensions belong to the independent Browser plugin.', { retryable: false });
|
|
1055
|
+
}
|
|
1056
|
+
if ((input.args.browser_mode !== undefined && input.args.browser_mode !== 'attach_preferred')
|
|
1057
|
+
|| (input.args.cdp_attach_fallback !== undefined && input.args.cdp_attach_fallback !== 'fail_closed')
|
|
1058
|
+
|| (input.args.native_attach_mode !== undefined && input.args.native_attach_mode !== 'auto')) {
|
|
1059
|
+
throw new AssistantPluginError('PLUGIN_COMPUTER_BROWSER_SCRIPT_MODE_SEPARATE',
|
|
1060
|
+
'Computer only operates the native browser; use the independent Browser plugin for scripted or isolated profiles.',
|
|
1061
|
+
{ retryable: false });
|
|
1062
|
+
}
|
|
1063
|
+
return { ...input, args: { ...input.args,
|
|
1064
|
+
browser_mode: 'attach_preferred',
|
|
1065
|
+
cdp_attach_fallback: 'fail_closed',
|
|
1066
|
+
native_attach_mode: 'auto',
|
|
1067
|
+
__forge_computer_native_only: true,
|
|
1068
|
+
} };
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1006
1071
|
export const computerPluginAdapter: AssistantPluginAdapter = {
|
|
1007
1072
|
pluginId: 'computer',
|
|
1008
1073
|
scope: 'controller_with_repository_overlay',
|
|
1009
1074
|
buildManifest: buildComputerManifest,
|
|
1010
1075
|
async resolveAuthorizationContext(input) {
|
|
1076
|
+
if (isIosPhysicalDeviceAction(input.actionId)) return resolveIosPhysicalDeviceAuthorizationContext(input);
|
|
1011
1077
|
if (!isDesktopProductAction(input.actionId)) {
|
|
1012
|
-
return resolveBrowserPluginAuthorizationContext(
|
|
1078
|
+
return resolveBrowserPluginAuthorizationContext(computerNativeBrowserInput(input));
|
|
1013
1079
|
}
|
|
1014
1080
|
return resolveDesktopAuthorizationContext(input);
|
|
1015
1081
|
},
|
|
1016
1082
|
async executeAction(input) {
|
|
1083
|
+
if (isIosPhysicalDeviceAction(input.actionId)) return executeIosPhysicalDeviceAction(input);
|
|
1017
1084
|
if (!isDesktopProductAction(input.actionId)) {
|
|
1018
|
-
return executeBrowserPluginAction(
|
|
1085
|
+
return executeBrowserPluginAction(computerNativeBrowserInput(input));
|
|
1019
1086
|
}
|
|
1020
1087
|
if (input.actionId === COMPUTER_CONSOLE_UNLOCK_PREPARE_ACTION) {
|
|
1021
1088
|
return executeProtectedConsoleUnlockPreparation({ confirmAuthorization: input.confirmAuthorization === true, timeoutMs: input.timeoutMs }, input.controllerHome);
|