@moretea-labs/forge 1.5.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +120 -0
- package/CHANGELOG.md +36 -0
- package/CLAUDE.md +120 -0
- package/CODE_OF_CONDUCT.md +23 -0
- package/CONTRIBUTING.md +77 -0
- package/LICENSE +22 -0
- package/NOTICE +9 -0
- package/README.en.md +5 -0
- package/README.es.md +12 -0
- package/README.fr.md +12 -0
- package/README.ja.md +12 -0
- package/README.md +132 -0
- package/README.zh-CN.md +84 -0
- package/SECURITY.md +50 -0
- package/SKILL.md +431 -0
- package/SUPPORT.md +31 -0
- package/THIRD_PARTY_NOTICES.md +18 -0
- package/agents/openai.yaml +4 -0
- package/assets/AGENTS.md +68 -0
- package/assets/CLAUDE.md +68 -0
- package/assets/branding/README.md +10 -0
- package/assets/branding/forge-controller-flow.mmd +8 -0
- package/assets/hooks/AGENTS.md +75 -0
- package/assets/hooks/CLAUDE.md +75 -0
- package/assets/hooks/anti-simplification.sh +12 -0
- package/assets/hooks/changelog-guard.sh +80 -0
- package/assets/hooks/codex.hooks.template.json +60 -0
- package/assets/hooks/first-principles-guard.sh +72 -0
- package/assets/hooks/hook-input.sh +618 -0
- package/assets/hooks/lib/session-state.sh +106 -0
- package/assets/hooks/lib/workflow-state.sh +1730 -0
- package/assets/hooks/post-bash.sh +215 -0
- package/assets/hooks/post-edit-guard.sh +256 -0
- package/assets/hooks/post-tool-observer.sh +99 -0
- package/assets/hooks/pre-edit-guard.sh +240 -0
- package/assets/hooks/prompt-guard.sh +1077 -0
- package/assets/hooks/run-hook.sh +67 -0
- package/assets/hooks/security-sentinel.sh +115 -0
- package/assets/hooks/session-start-context.sh +593 -0
- package/assets/hooks/settings.template.json +60 -0
- package/assets/hooks/stop-orchestrator.sh +171 -0
- package/assets/hooks/subagent-return-channel-guard.sh +107 -0
- package/assets/hooks/worktree-guard.sh +39 -0
- package/assets/initializer-question-pack.v1.json +132 -0
- package/assets/initializer-question-pack.v1.schema.json +46 -0
- package/assets/initializer-question-pack.v2.json +174 -0
- package/assets/initializer-question-pack.v3.json +206 -0
- package/assets/initializer-question-pack.v3.schema.json +92 -0
- package/assets/initializer-question-pack.v4.json +557 -0
- package/assets/initializer-question-pack.v4.schema.json +169 -0
- package/assets/partials/01-header.partial.md +10 -0
- package/assets/partials/02-iron-rules.partial.md +30 -0
- package/assets/partials/03-philosophy.partial.md +32 -0
- package/assets/partials/04-project-structure.partial.md +56 -0
- package/assets/partials/05-workflow.partial.md +79 -0
- package/assets/partials/06-cloudflare.partial.md +20 -0
- package/assets/partials/07-footer.partial.md +38 -0
- package/assets/partials/08-orchestration.partial.md +49 -0
- package/assets/partials/09-compact-instructions.partial.md +11 -0
- package/assets/partials/_assembly-order.md +40 -0
- package/assets/partials-agents/01-header.partial.md +8 -0
- package/assets/partials-agents/02-operating-mode.partial.md +44 -0
- package/assets/partials-agents/03-orchestration.partial.md +45 -0
- package/assets/partials-agents/04-task-protocol.partial.md +69 -0
- package/assets/partials-agents/05-coding-constraints.partial.md +20 -0
- package/assets/partials-agents/06-quality-safety.partial.md +23 -0
- package/assets/partials-agents/07-cloudflare.partial.md +9 -0
- package/assets/partials-agents/08-deep-docs.partial.md +32 -0
- package/assets/plan-map.json +301 -0
- package/assets/plugin-registry.v1.json +8 -0
- package/assets/project-structures/ai-native-collaborative-editor.txt +25 -0
- package/assets/project-structures/ai-native-product-copilot.txt +21 -0
- package/assets/project-structures/ai-native-runtime-console.txt +25 -0
- package/assets/project-structures/ai-native-sidecar-kernel.txt +19 -0
- package/assets/project-structures/astro-ssr.txt +25 -0
- package/assets/project-structures/expo-nativewind.txt +52 -0
- package/assets/project-structures/monorepo.txt +63 -0
- package/assets/project-structures/remix.txt +50 -0
- package/assets/project-structures/tanstack-start-workers.txt +51 -0
- package/assets/project-structures/vite-tanstack.txt +58 -0
- package/assets/reference-configs/AGENTS.md +13 -0
- package/assets/reference-configs/CLAUDE.md +13 -0
- package/assets/reference-configs/agentic-development-flow.md +94 -0
- package/assets/reference-configs/ai-workflows.md +14 -0
- package/assets/reference-configs/changelog-versioning.md +13 -0
- package/assets/reference-configs/coding-standards.md +14 -0
- package/assets/reference-configs/development-protocol.md +21 -0
- package/assets/reference-configs/document-generation.md +38 -0
- package/assets/reference-configs/evaluator-rubric.md +19 -0
- package/assets/reference-configs/external-tooling.md +374 -0
- package/assets/reference-configs/git-strategy.md +14 -0
- package/assets/reference-configs/global-working-rules.md +64 -0
- package/assets/reference-configs/handoff-protocol.md +38 -0
- package/assets/reference-configs/harness-overview.md +124 -0
- package/assets/reference-configs/heartbeat-triage.md +57 -0
- package/assets/reference-configs/hook-operations.md +96 -0
- package/assets/reference-configs/release-deploy.md +40 -0
- package/assets/reference-configs/spa-day-protocol.md +14 -0
- package/assets/reference-configs/sprint-contracts.md +89 -0
- package/assets/reference-configs/workflow-orchestration.md +14 -0
- package/assets/skill-hooks.json +41 -0
- package/assets/skill-version.json +189 -0
- package/assets/skills/claude-review/SKILL.md +231 -0
- package/assets/skills/codex-review/SKILL.md +103 -0
- package/assets/templates/AGENTS.md +13 -0
- package/assets/templates/CLAUDE.md +13 -0
- package/assets/templates/architecture.template.md +80 -0
- package/assets/templates/brief.template.md +38 -0
- package/assets/templates/contract.template.md +104 -0
- package/assets/templates/decisions.template.md +85 -0
- package/assets/templates/factor-factory/factor-backtest-report.template.md +36 -0
- package/assets/templates/factor-factory/factor-hypothesis.template.md +30 -0
- package/assets/templates/factor-factory/factor-registry.template.json +5 -0
- package/assets/templates/gitignore.template +53 -0
- package/assets/templates/guides/metro-esm-gotchas.template.md +46 -0
- package/assets/templates/helpers/architecture-event.ts +822 -0
- package/assets/templates/helpers/architecture-queue.sh +647 -0
- package/assets/templates/helpers/archive-architecture-request.sh +244 -0
- package/assets/templates/helpers/archive-workflow.sh +295 -0
- package/assets/templates/helpers/capability-config.ts +394 -0
- package/assets/templates/helpers/capability-resolver.ts +505 -0
- package/assets/templates/helpers/capture-plan.sh +335 -0
- package/assets/templates/helpers/check-agent-tooling.sh +1533 -0
- package/assets/templates/helpers/check-architecture-sync.sh +287 -0
- package/assets/templates/helpers/check-brain-manifest.sh +255 -0
- package/assets/templates/helpers/check-context-files.sh +49 -0
- package/assets/templates/helpers/check-deploy-sql-order.sh +117 -0
- package/assets/templates/helpers/check-skill-version.ts +193 -0
- package/assets/templates/helpers/check-task-sync.sh +65 -0
- package/assets/templates/helpers/check-task-workflow.sh +1002 -0
- package/assets/templates/helpers/codex-handoff-resume.sh +269 -0
- package/assets/templates/helpers/context-contract-sync.sh +516 -0
- package/assets/templates/helpers/contract-run.ts +465 -0
- package/assets/templates/helpers/contract-worktree.sh +725 -0
- package/assets/templates/helpers/ensure-task-workflow.sh +1218 -0
- package/assets/templates/helpers/harness-trace-grade.sh +150 -0
- package/assets/templates/helpers/heartbeat-triage.sh +361 -0
- package/assets/templates/helpers/inspect-project-state.ts +280 -0
- package/assets/templates/helpers/maintenance-triage.sh +167 -0
- package/assets/templates/helpers/migrate-project-template.sh +54 -0
- package/assets/templates/helpers/migrate-workflow-docs.ts +413 -0
- package/assets/templates/helpers/new-plan.sh +183 -0
- package/assets/templates/helpers/new-spec.sh +50 -0
- package/assets/templates/helpers/new-sprint.sh +49 -0
- package/assets/templates/helpers/plan-to-todo.sh +799 -0
- package/assets/templates/helpers/prepare-codex-handoff.sh +203 -0
- package/assets/templates/helpers/prepare-handoff.sh +81 -0
- package/assets/templates/helpers/refresh-current-status.sh +469 -0
- package/assets/templates/helpers/select-agent-context-blocks.sh +66 -0
- package/assets/templates/helpers/ship-worktrees.sh +561 -0
- package/assets/templates/helpers/sprint-backlog.sh +837 -0
- package/assets/templates/helpers/summarize-failures.sh +125 -0
- package/assets/templates/helpers/switch-plan.sh +153 -0
- package/assets/templates/helpers/sync-brain-docs.sh +381 -0
- package/assets/templates/helpers/verify-contract.sh +705 -0
- package/assets/templates/helpers/verify-sprint.sh +680 -0
- package/assets/templates/helpers/workflow-contract.ts +202 -0
- package/assets/templates/helpers/workstream-sync.sh +263 -0
- package/assets/templates/implementation-notes.template.md +37 -0
- package/assets/templates/packages.template.md +45 -0
- package/assets/templates/plan.template.md +77 -0
- package/assets/templates/prd.template.md +165 -0
- package/assets/templates/research.template.md +25 -0
- package/assets/templates/review.template.md +78 -0
- package/assets/templates/spec.template.md +31 -0
- package/assets/templates/sprint.template.md +69 -0
- package/assets/templates/tech-stack.template.md +57 -0
- package/assets/versions.json +77 -0
- package/assets/workflow-contract.v1.json +671 -0
- package/bin/forge-desktop-helper.mjs +204 -0
- package/bin/forge-hook.mjs +32 -0
- package/bin/forge-runtime-service.mjs +34 -0
- package/bin/forge-runtime.mjs +30 -0
- package/bin/forge.mjs +32 -0
- package/docs/CHANGELOG.md +978 -0
- package/docs/README.md +72 -0
- package/docs/README.zh-CN.md +58 -0
- package/docs/architecture/chatgpt-handoff-facade.md +393 -0
- package/docs/architecture/current/README.md +158 -0
- package/docs/architecture/current/approved-target-architecture.zh-CN.md +1501 -0
- package/docs/architecture/current/architecture-invariants.md +331 -0
- package/docs/architecture/current/automation-and-schedule-engine.md +453 -0
- package/docs/architecture/current/codegraph-context-provider.md +226 -0
- package/docs/architecture/current/control-plane-authority-inventory.md +82 -0
- package/docs/architecture/current/controller-repository-decoupling.md +148 -0
- package/docs/architecture/current/dispatch-and-agent-strategy.md +354 -0
- package/docs/architecture/current/entity-model.md +619 -0
- package/docs/architecture/current/external-plugin-governance.md +396 -0
- package/docs/architecture/current/failure-recovery.md +471 -0
- package/docs/architecture/current/governance.md +213 -0
- package/docs/architecture/current/human-interaction-plane.md +87 -0
- package/docs/architecture/current/implementation-status.md +128 -0
- package/docs/architecture/current/job-and-run-lifecycle.md +413 -0
- package/docs/architecture/current/local-workspace-target-grants.md +70 -0
- package/docs/architecture/current/migration-roadmap.md +105 -0
- package/docs/architecture/current/multi-repository-execution.md +371 -0
- package/docs/architecture/current/operations-runbook.md +45 -0
- package/docs/architecture/current/peer-supercontroller-runtime.md +32 -0
- package/docs/architecture/current/personal-assistant-plugin-baseline.md +207 -0
- package/docs/architecture/current/repository-access-modes.md +102 -0
- package/docs/architecture/current/route-policy.md +15 -0
- package/docs/architecture/current/route-session-concurrency.md +85 -0
- package/docs/architecture/current/runtime-architecture-simplification.md +214 -0
- package/docs/architecture/current/runtime-directory-map.md +49 -0
- package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +85 -0
- package/docs/architecture/current/scheduler-and-resource-claims.md +432 -0
- package/docs/architecture/current/session-aware-execution-and-authorization.md +77 -0
- package/docs/architecture/current/system-overview.md +144 -0
- package/docs/architecture/current/target-requirements-traceability.md +202 -0
- package/docs/architecture/current/thin-harness-governance-policy.md +32 -0
- package/docs/architecture/current/thin-harness-v1.md +313 -0
- package/docs/architecture/current/verification-and-release-gates.md +391 -0
- package/docs/architecture/decisions/20260801-controller-home-sqlite-state.md +100 -0
- package/docs/architecture/decisions/20260803-canonical-skill-surface.md +40 -0
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +53 -0
- package/docs/architecture/diagrams/.gitkeep +1 -0
- package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +186 -0
- package/docs/architecture/domains/.gitkeep +0 -0
- package/docs/architecture/domains/public-surface.md +30 -0
- package/docs/architecture/domains/runtime-harness.md +33 -0
- package/docs/architecture/domains/verification.md +33 -0
- package/docs/architecture/domains/workflow-engine.md +30 -0
- package/docs/architecture/global-hook-runtime.md +162 -0
- package/docs/architecture/history.md +38 -0
- package/docs/architecture/index.md +114 -0
- package/docs/architecture/ios-semantic-automation-provider-v2.md +166 -0
- package/docs/architecture/modules/.gitkeep +0 -0
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +24 -0
- package/docs/architecture/requests/.gitkeep +1 -0
- package/docs/architecture/requests/archive/2026/20260528-154740-ai-hooks-ai-hooks-hook-input-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-154816-assets-hooks-assets-hooks-hook-input-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-161851-ai-harness-workflow-contract-json-ai-harness-workflow-contract-json.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-161853-scripts-check-task-workflow-sh-scripts-check-task-workflow-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-161911-assets-workflow-contract-v1-json-assets-workflow-contract-v1-json.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222416-ai-hooks-ai-hooks-hook-input-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222444-ai-hooks-ai-hooks-pre-edit-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222457-ai-hooks-ai-hooks-pre-edit-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222648-ai-hooks-ai-hooks-pre-edit-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222705-ai-hooks-ai-hooks-worktree-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222726-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-222727-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-224736-ai-hooks-ai-hooks-hook-input-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233259-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233335-assets-hooks-assets-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233456-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233457-assets-hooks-assets-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233637-ai-hooks-ai-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260528-233638-assets-hooks-assets-hooks-prompt-guard-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260529-000322-root-package-json.md +59 -0
- package/docs/architecture/requests/archive/2026/20260529-004238-ai-hooks-ai-hooks-lib-workflow-state-sh.md +59 -0
- package/docs/architecture/requests/archive/2026/20260529-020654-ai-harness-policy-json-ai-harness-policy-json.md +63 -0
- package/docs/architecture/requests/archive/2026/20260529-094236-ai-hooks-ai-hooks-prompt-guard-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260529-094323-assets-hooks-assets-hooks-prompt-guard-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260529-094331-ai-hooks-ai-hooks-prompt-guard-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260529-094339-assets-hooks-assets-hooks-prompt-guard-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260529-094430-assets-hooks-assets-hooks-session-start-context-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260529-094446-ai-hooks-ai-hooks-session-start-context-sh.md +60 -0
- package/docs/architecture/requests/archive/2026/20260613-002040-runtime-harness-hook-adapters.md +68 -0
- package/docs/architecture/requests/archive/2026/root.md +68 -0
- package/docs/architecture/requests/archive/2026/runtime-harness-hook-adapters.md +76 -0
- package/docs/architecture/requests/archive/2026/verification-evals-checks.md +68 -0
- package/docs/architecture/requests/archive/2026/workflow-engine-contract-assets.md +69 -0
- package/docs/architecture/reviews/2026-07-11-access-surface-review.md +99 -0
- package/docs/architecture/snapshots/.gitkeep +1 -0
- package/docs/forge-autonomous-goal-loop.md +7 -0
- package/docs/forge-chatgpt-browser-engine.md +11 -0
- package/docs/forge-chatgpt-mcp-setup.md +239 -0
- package/docs/forge-chatgpt-supervised-automation.md +11 -0
- package/docs/forge-github-issue-launcher.md +7 -0
- package/docs/forge-plugin-management.md +95 -0
- package/docs/forge-provider-configuration.md +7 -0
- package/docs/forge-runtime-self-healing-loop.md +7 -0
- package/docs/images/forge-banner-cn.svg +1 -0
- package/docs/images/forge-banner.svg +1 -0
- package/docs/images/forge-controller-flow.svg +1 -0
- package/docs/images/image.png +0 -0
- package/docs/images/repo-harness-gptpro-cn.png +0 -0
- package/docs/images/repo-harness-gptpro.png +0 -0
- package/docs/images/repo-harness-hook-carrot.png +0 -0
- package/docs/operations/chatgpt-personal-assistant-runtime.md +83 -0
- package/docs/operations/checkout-route-integrity.md +22 -0
- package/docs/operations/controller-browser-plugin.md +299 -0
- package/docs/operations/controller-desktop-plugin.md +112 -0
- package/docs/operations/controller-performance-and-502.md +126 -0
- package/docs/operations/controller-reliability-runbook.md +134 -0
- package/docs/operations/environment-hardening.md +50 -0
- package/docs/operations/features.md +45 -0
- package/docs/operations/features.zh-CN.md +45 -0
- package/docs/operations/github-repository.md +60 -0
- package/docs/operations/gmail-assistant-routines.md +186 -0
- package/docs/operations/homebrew.md +26 -0
- package/docs/operations/local-system-assistant.md +46 -0
- package/docs/operations/local-system-assistant.zh-CN.md +46 -0
- package/docs/operations/mcp-tool-exposure.md +58 -0
- package/docs/operations/mobile-intents-shortcuts.md +117 -0
- package/docs/operations/open-source-release-hygiene.md +31 -0
- package/docs/operations/personal-assistant-operator-guide.md +61 -0
- package/docs/operations/platform-support.md +62 -0
- package/docs/operations/platform-support.zh-CN.md +61 -0
- package/docs/operations/provider-configuration.md +31 -0
- package/docs/operations/releasing.md +86 -0
- package/docs/operations/releasing.zh-CN.md +86 -0
- package/docs/operations/route-session-concurrency-baseline.json +893 -0
- package/docs/operations/route-session-concurrency-baseline.md +94 -0
- package/docs/operations/runtime-performance-diagnostics.md +14 -0
- package/docs/operations/sqlite-control-plane-recovery.md +65 -0
- package/docs/operations/standalone-disaster-recovery.md +136 -0
- package/docs/operations/troubleshooting.md +72 -0
- package/docs/operations/troubleshooting.zh-CN.md +70 -0
- package/docs/public-usage-guide.md +33 -0
- package/docs/public-usage-guide.zh-CN.md +33 -0
- package/docs/tutorials/01-install-and-start.md +93 -0
- package/docs/tutorials/01-install-and-start.zh-CN.md +93 -0
- package/docs/tutorials/02-connect-chatgpt.md +82 -0
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +80 -0
- package/docs/tutorials/03-first-repository-task.md +57 -0
- package/docs/tutorials/03-first-repository-task.zh-CN.md +57 -0
- package/docs/tutorials/README.md +13 -0
- package/docs/tutorials/README.zh-CN.md +13 -0
- package/docs/wiki/Architecture.md +85 -0
- package/docs/wiki/Core-Concepts.md +27 -0
- package/docs/wiki/Home.md +36 -0
- package/docs/wiki/Implementation.md +172 -0
- package/docs/wiki/Installation.md +50 -0
- package/docs/wiki/Integrations.md +29 -0
- package/docs/wiki/Operations.md +26 -0
- package/docs/wiki/Quick-Start.md +46 -0
- package/docs/wiki/Releases-and-Upgrades.md +34 -0
- package/docs/wiki/Runtime-Architecture.md +50 -0
- package/docs/wiki/Security-Model.md +31 -0
- package/docs/wiki/Troubleshooting.md +38 -0
- package/docs/wiki/Work-Lifecycle.md +31 -0
- package/docs/wiki/_Sidebar.md +21 -0
- package/install.ps1 +48 -0
- package/install.sh +66 -0
- package/package.json +156 -0
- package/scripts/activate-source-baseline.ts +335 -0
- package/scripts/architecture-event.ts +822 -0
- package/scripts/architecture-queue.sh +647 -0
- package/scripts/archive-architecture-request.sh +244 -0
- package/scripts/archive-workflow.sh +295 -0
- package/scripts/assemble-template.ts +1172 -0
- package/scripts/benchmark-controller-hotpath.ts +212 -0
- package/scripts/benchmark-controller-round2.ts +139 -0
- package/scripts/benchmark-ios-agent-device.ts +514 -0
- package/scripts/benchmark-route-session-concurrency.ts +714 -0
- package/scripts/benchmark-thin-harness-gateway-ab.ts +632 -0
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +479 -0
- package/scripts/benchmark-thin-harness.ts +251 -0
- package/scripts/bootstrap-runtime-maintenance-recovery.sh +183 -0
- package/scripts/canary-global-hook.sh +263 -0
- package/scripts/capability-config.ts +394 -0
- package/scripts/capability-resolver.ts +505 -0
- package/scripts/capture-plan.sh +335 -0
- package/scripts/check-agent-tooling.sh +1549 -0
- package/scripts/check-architecture-sync.sh +373 -0
- package/scripts/check-brain-manifest.sh +255 -0
- package/scripts/check-ci.sh +16 -0
- package/scripts/check-context-files.sh +49 -0
- package/scripts/check-deploy-sql-order.sh +117 -0
- package/scripts/check-mcp-compatibility.ts +93 -0
- package/scripts/check-npm-release.sh +32 -0
- package/scripts/check-open-source-tracked-surface.sh +146 -0
- package/scripts/check-package-identity.mjs +117 -0
- package/scripts/check-platform-support.mjs +79 -0
- package/scripts/check-public-docs.sh +105 -0
- package/scripts/check-public-release-surface.sh +19 -0
- package/scripts/check-release-published.sh +57 -0
- package/scripts/check-release-readiness.sh +43 -0
- package/scripts/check-release-version.mjs +74 -0
- package/scripts/check-runtime-architecture.mjs +593 -0
- package/scripts/check-runtime-env.mjs +89 -0
- package/scripts/check-skill-version.ts +193 -0
- package/scripts/check-tarball-install-smoke.sh +66 -0
- package/scripts/check-task-sync.sh +65 -0
- package/scripts/check-task-workflow.sh +1002 -0
- package/scripts/check-third-party-notices.mjs +36 -0
- package/scripts/codex-handoff-resume.sh +269 -0
- package/scripts/complete-control-plane-cutover.ts +126 -0
- package/scripts/context-contract-sync.sh +516 -0
- package/scripts/contract-run.ts +465 -0
- package/scripts/contract-worktree.sh +725 -0
- package/scripts/controller-ngrok-rotation.sh +678 -0
- package/scripts/create-project-dirs.sh +282 -0
- package/scripts/ensure-codegraph.sh +16 -0
- package/scripts/ensure-task-workflow.sh +1218 -0
- package/scripts/export-requirement-portfolio.ts +28 -0
- package/scripts/factor-lab-check.sh +73 -0
- package/scripts/factor-lab-new.sh +96 -0
- package/scripts/factor-lab-promote.sh +86 -0
- package/scripts/factor-lab-reject.sh +68 -0
- package/scripts/harness-trace-grade.sh +150 -0
- package/scripts/heartbeat-triage.sh +361 -0
- package/scripts/hook-dispatch-diet-report.ts +234 -0
- package/scripts/hook-shim.sh +93 -0
- package/scripts/init-project.sh +505 -0
- package/scripts/initializer-question-pack.ts +200 -0
- package/scripts/inspect-project-state.ts +280 -0
- package/scripts/install-desktop-operator-registration.ts +45 -0
- package/scripts/install-standalone-recovery.ts +74 -0
- package/scripts/lib/AGENTS.md +13 -0
- package/scripts/lib/CLAUDE.md +13 -0
- package/scripts/lib/controller-home-env.sh +69 -0
- package/scripts/lib/project-init-lib.sh +2759 -0
- package/scripts/load-standalone-recovery.sh +17 -0
- package/scripts/loop-engine-cutover-gate.ts +324 -0
- package/scripts/maintenance-triage.sh +167 -0
- package/scripts/migrate-project-template.sh +1189 -0
- package/scripts/migrate-requirement-portfolio.ts +86 -0
- package/scripts/migrate-workflow-docs.ts +413 -0
- package/scripts/new-plan.sh +183 -0
- package/scripts/new-spec.sh +50 -0
- package/scripts/new-sprint.sh +49 -0
- package/scripts/open-source-audit-allowlist.txt +46 -0
- package/scripts/package-source-archive.sh +147 -0
- package/scripts/plan-to-todo.sh +799 -0
- package/scripts/prepare-codex-handoff.sh +203 -0
- package/scripts/prepare-handoff.sh +81 -0
- package/scripts/public-release-files.txt +39 -0
- package/scripts/public-release.ts +174 -0
- package/scripts/publish-release-tarball.sh +27 -0
- package/scripts/refresh-current-status.sh +469 -0
- package/scripts/refresh-ios-physical-runtime.ts +196 -0
- package/scripts/repair-executable-modes.ts +8 -0
- package/scripts/rollout-all-registered-repos.sh +7 -0
- package/scripts/route-nl-vs-ts-eval.ts +679 -0
- package/scripts/run-bun-test-file.ts +182 -0
- package/scripts/run-governed-gate.ts +140 -0
- package/scripts/run-mobile-monitor-bridge.ts +71 -0
- package/scripts/run-skill-evals.ts +1022 -0
- package/scripts/run-skill-hook.ts +244 -0
- package/scripts/run-supervised-command.ts +24 -0
- package/scripts/run-tests-portable.sh +20 -0
- package/scripts/select-agent-context-blocks.sh +66 -0
- package/scripts/setup-plugins.sh +43 -0
- package/scripts/ship-worktrees.sh +561 -0
- package/scripts/smoke-browser-cdp-bridge.ts +93 -0
- package/scripts/smoke-chatgpt-supervised-automation.ts +208 -0
- package/scripts/smoke-ios-physical-device.ts +114 -0
- package/scripts/smoke-mcp-http-runtime.ts +216 -0
- package/scripts/smoke-mcp-tool-surface.ts +117 -0
- package/scripts/smoke-runtime-recovery.ts +220 -0
- package/scripts/smoke-schedule-engine.ts +283 -0
- package/scripts/sprint-backlog.sh +837 -0
- package/scripts/stage-runtime-release.ts +38 -0
- package/scripts/summarize-failures.sh +125 -0
- package/scripts/switch-plan.sh +153 -0
- package/scripts/sync-brain-docs.sh +381 -0
- package/scripts/sync-codex-installed-copies.sh +183 -0
- package/scripts/test-governance.ts +75 -0
- package/scripts/verify-contract.sh +705 -0
- package/scripts/verify-forge-runtime.sh +31 -0
- package/scripts/verify-sprint.sh +680 -0
- package/scripts/windows-paths.d.mts +7 -0
- package/scripts/windows-paths.mjs +28 -0
- package/scripts/workflow-contract.ts +206 -0
- package/scripts/workstream-sync.sh +263 -0
- package/src/cli/agent-jobs/executable-resolver.ts +289 -0
- package/src/cli/agent-jobs/executor-health.ts +252 -0
- package/src/cli/agent-jobs/integration.ts +963 -0
- package/src/cli/agent-jobs/job-manager.ts +2030 -0
- package/src/cli/agent-jobs/types.ts +219 -0
- package/src/cli/agent-jobs/worker-lifecycle.ts +288 -0
- package/src/cli/chatgpt-browser/bind-server.ts +504 -0
- package/src/cli/chatgpt-browser/binding.ts +140 -0
- package/src/cli/chatgpt-browser/bridge-extension.ts +221 -0
- package/src/cli/chatgpt-browser/bridge-provider.ts +268 -0
- package/src/cli/chatgpt-browser/engine.ts +569 -0
- package/src/cli/chatgpt-browser/file-policy.ts +158 -0
- package/src/cli/chatgpt-browser/native-provider.ts +553 -0
- package/src/cli/chatgpt-browser/oracle-provider.ts +437 -0
- package/src/cli/chatgpt-browser/prompt-assembler.ts +29 -0
- package/src/cli/chatgpt-browser/session-store.ts +301 -0
- package/src/cli/chatgpt-browser/types.ts +172 -0
- package/src/cli/commands/adopt-plan.ts +186 -0
- package/src/cli/commands/brain-root.ts +132 -0
- package/src/cli/commands/brain.ts +609 -0
- package/src/cli/commands/capability-context.ts +613 -0
- package/src/cli/commands/chatgpt.ts +424 -0
- package/src/cli/commands/controller.ts +680 -0
- package/src/cli/commands/docs.ts +103 -0
- package/src/cli/commands/doctor.ts +476 -0
- package/src/cli/commands/global-runtime.ts +323 -0
- package/src/cli/commands/hook.ts +34 -0
- package/src/cli/commands/init-hook.ts +870 -0
- package/src/cli/commands/init.ts +829 -0
- package/src/cli/commands/install.ts +86 -0
- package/src/cli/commands/mcp.ts +315 -0
- package/src/cli/commands/migrate.ts +154 -0
- package/src/cli/commands/plugin.ts +74 -0
- package/src/cli/commands/prompt-guard-decision.ts +238 -0
- package/src/cli/commands/recovery.ts +667 -0
- package/src/cli/commands/repository.ts +224 -0
- package/src/cli/commands/run.ts +23 -0
- package/src/cli/commands/runtime.ts +191 -0
- package/src/cli/commands/security.ts +440 -0
- package/src/cli/commands/status.ts +137 -0
- package/src/cli/commands/tools.ts +167 -0
- package/src/cli/controller/check-runner.ts +1022 -0
- package/src/cli/controller/codex-continuation.ts +153 -0
- package/src/cli/controller/completion-backlog.ts +380 -0
- package/src/cli/controller/completion-orchestrator.ts +1807 -0
- package/src/cli/controller/completion-target.ts +96 -0
- package/src/cli/controller/composite-operations.ts +196 -0
- package/src/cli/controller/composite-result.ts +130 -0
- package/src/cli/controller/context-pack.ts +652 -0
- package/src/cli/controller/execution-completion.ts +175 -0
- package/src/cli/controller/execution-policy.ts +392 -0
- package/src/cli/controller/governance.ts +594 -0
- package/src/cli/controller/issue-store.ts +1421 -0
- package/src/cli/controller/launch-agents.ts +410 -0
- package/src/cli/controller/legacy-issue-cutover.ts +415 -0
- package/src/cli/controller/lifecycle-authority.ts +16 -0
- package/src/cli/controller/migrated-issue-decision.ts +237 -0
- package/src/cli/controller/operational-plan.ts +226 -0
- package/src/cli/controller/postcondition.ts +134 -0
- package/src/cli/controller/progress.ts +656 -0
- package/src/cli/controller/project-state.ts +147 -0
- package/src/cli/controller/requirement-portfolio-export.ts +146 -0
- package/src/cli/controller/requirement-portfolio-migration.ts +613 -0
- package/src/cli/controller/run-evidence.ts +43 -0
- package/src/cli/controller/runtime-config.ts +92 -0
- package/src/cli/controller/stuck-state-migration.ts +396 -0
- package/src/cli/controller/task-ledger.ts +632 -0
- package/src/cli/controller/task-status-resolver.ts +403 -0
- package/src/cli/controller/types.ts +273 -0
- package/src/cli/controller/work-mode.ts +186 -0
- package/src/cli/controller/worklog.ts +201 -0
- package/src/cli/diagnostic-entry.ts +39 -0
- package/src/cli/editing/edit-session.ts +1606 -0
- package/src/cli/editing/executable-modes.ts +70 -0
- package/src/cli/github/github.ts +274 -0
- package/src/cli/github/plugin.ts +345 -0
- package/src/cli/github/session.ts +104 -0
- package/src/cli/hook/prompt-guard-decision.ts +238 -0
- package/src/cli/hook/prompt-intents.ts +534 -0
- package/src/cli/hook/route-registry.ts +107 -0
- package/src/cli/hook/runtime.ts +303 -0
- package/src/cli/hook/state-snapshot.ts +388 -0
- package/src/cli/hook-entry.ts +51 -0
- package/src/cli/index.ts +574 -0
- package/src/cli/installer/managed-entries.ts +78 -0
- package/src/cli/installer/shared.ts +65 -0
- package/src/cli/installer/targets/claude.ts +136 -0
- package/src/cli/installer/targets/codex.ts +174 -0
- package/src/cli/installer/targets/registry.ts +28 -0
- package/src/cli/installer/types.ts +72 -0
- package/src/cli/local-bridge/connector-freshness.ts +529 -0
- package/src/cli/local-bridge/console-view-models.ts +324 -0
- package/src/cli/local-bridge/dashboard.ts +1312 -0
- package/src/cli/local-bridge/facade-api.ts +1877 -0
- package/src/cli/local-bridge/job-store.ts +1451 -0
- package/src/cli/local-bridge/mobile-intents.ts +344 -0
- package/src/cli/local-bridge/mobile-monitor.ts +209 -0
- package/src/cli/local-bridge/server.ts +3183 -0
- package/src/cli/local-bridge/types.ts +176 -0
- package/src/cli/mcp/access-mode.ts +132 -0
- package/src/cli/mcp/access-tools.ts +387 -0
- package/src/cli/mcp/audit.ts +32 -0
- package/src/cli/mcp/auth.ts +402 -0
- package/src/cli/mcp/instructions.ts +33 -0
- package/src/cli/mcp/legacy-context.ts +16 -0
- package/src/cli/mcp/legacy-tool-service.ts +5516 -0
- package/src/cli/mcp/multi-repository.ts +423 -0
- package/src/cli/mcp/oauth.ts +226 -0
- package/src/cli/mcp/paths.ts +134 -0
- package/src/cli/mcp/policy.ts +230 -0
- package/src/cli/mcp/proxy-env.ts +62 -0
- package/src/cli/mcp/readiness-projection.ts +24 -0
- package/src/cli/mcp/redaction.ts +21 -0
- package/src/cli/mcp/repo.ts +24 -0
- package/src/cli/mcp/repository-tools.ts +1295 -0
- package/src/cli/mcp/server.ts +341 -0
- package/src/cli/mcp/setup.ts +793 -0
- package/src/cli/mcp/tool-capability-cache.ts +87 -0
- package/src/cli/mcp/tools.ts +9 -0
- package/src/cli/mcp/toolset-names.ts +70 -0
- package/src/cli/mcp/toolset-selection.ts +41 -0
- package/src/cli/mcp/toolset.ts +300 -0
- package/src/cli/mcp/transports/AGENTS.md +39 -0
- package/src/cli/mcp/transports/CLAUDE.md +39 -0
- package/src/cli/mcp/transports/http.ts +1234 -0
- package/src/cli/mcp/transports/session-registry.ts +428 -0
- package/src/cli/mcp/transports/stdio.ts +8 -0
- package/src/cli/mcp/types.ts +41 -0
- package/src/cli/mcp/umbrella-tools.ts +117 -0
- package/src/cli/repo-adoption/reclaim-runtime.ts +681 -0
- package/src/cli/repositories/branch-name-policy.ts +57 -0
- package/src/cli/repositories/command-classifier.ts +600 -0
- package/src/cli/repositories/command-executor.ts +1066 -0
- package/src/cli/repositories/command-normalization.ts +53 -0
- package/src/cli/repositories/command-scope.ts +264 -0
- package/src/cli/repositories/controller-home.ts +88 -0
- package/src/cli/repositories/entity-migration.ts +199 -0
- package/src/cli/repositories/ephemeral-workspace.ts +62 -0
- package/src/cli/repositories/git-command-executor.ts +301 -0
- package/src/cli/repositories/goal-registry.ts +239 -0
- package/src/cli/repositories/identity.ts +64 -0
- package/src/cli/repositories/local-project-onboarding.ts +539 -0
- package/src/cli/repositories/locks.ts +262 -0
- package/src/cli/repositories/registry.ts +1057 -0
- package/src/cli/repositories/runtime-storage.ts +411 -0
- package/src/cli/repositories/safe-patch.ts +421 -0
- package/src/cli/repositories/selected-path-actions.ts +341 -0
- package/src/cli/repositories/structured-git.ts +257 -0
- package/src/cli/repositories/types.ts +155 -0
- package/src/cli/repositories/umbrella.ts +134 -0
- package/src/cli/repositories/workbench.ts +140 -0
- package/src/cli/repositories/worktree-storage.ts +137 -0
- package/src/cli/repository/inspector.ts +637 -0
- package/src/cli/repository/session-cache.ts +453 -0
- package/src/cli/runtime/helper-runner.ts +172 -0
- package/src/cli/runtime-invocation.ts +318 -0
- package/src/cli/tools/codegraph.ts +676 -0
- package/src/cli/v81-entry.ts +29 -0
- package/src/core/adoption/gitignore-plan.ts +127 -0
- package/src/core/adoption/helper-wrapper-plan.ts +159 -0
- package/src/core/adoption/manifest-templates.ts +60 -0
- package/src/core/adoption/modes.ts +7 -0
- package/src/core/adoption/operations.ts +126 -0
- package/src/core/adoption/plan.ts +149 -0
- package/src/core/adoption/render.ts +70 -0
- package/src/core/adoption/rollback.ts +62 -0
- package/src/core/adoption/summary.ts +43 -0
- package/src/core/adoption/workflow-contract-asset.ts +14 -0
- package/src/core/adoption/workflow-contract-plan.ts +31 -0
- package/src/effects/fs-transaction.ts +540 -0
- package/src/effects/git-executable.ts +59 -0
- package/src/effects/managed-block.ts +86 -0
- package/src/effects/path-safety.ts +47 -0
- package/src/effects/process-runner.ts +192 -0
- package/src/runtime/assistant/action-proposals.ts +279 -0
- package/src/runtime/assistant/intent.ts +395 -0
- package/src/runtime/assistant/model-provider.ts +460 -0
- package/src/runtime/assistant/openapi.ts +223 -0
- package/src/runtime/assistant/policy.ts +89 -0
- package/src/runtime/assistant/readiness.ts +258 -0
- package/src/runtime/assistant/routine-runtime.ts +477 -0
- package/src/runtime/assistant/schedule-binding.ts +171 -0
- package/src/runtime/assistant/standing-grants.ts +375 -0
- package/src/runtime/assistant/store.ts +243 -0
- package/src/runtime/assistant/types.ts +135 -0
- package/src/runtime/context/codegraph-read-provider.ts +347 -0
- package/src/runtime/context/codegraph-sidecar.cjs +207 -0
- package/src/runtime/control-plane/dispatch-priority.ts +67 -0
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +151 -0
- package/src/runtime/control-plane/execution/execution-identity.ts +456 -0
- package/src/runtime/control-plane/execution/session-store.ts +175 -0
- package/src/runtime/control-plane/execution/validation.ts +117 -0
- package/src/runtime/control-plane/execution/verification-evidence.ts +191 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +225 -0
- package/src/runtime/control-plane/execution/work-prepare-request-store.ts +159 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +445 -0
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +821 -0
- package/src/runtime/control-plane/facade/capability-registry.ts +279 -0
- package/src/runtime/control-plane/facade/check-normalization.ts +233 -0
- package/src/runtime/control-plane/facade/codex-delegation.ts +196 -0
- package/src/runtime/control-plane/facade/controller-session-store.ts +199 -0
- package/src/runtime/control-plane/facade/facade-result.ts +41 -0
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +177 -0
- package/src/runtime/control-plane/facade/goal-workloop.ts +1216 -0
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +221 -0
- package/src/runtime/control-plane/facade/index.ts +16 -0
- package/src/runtime/control-plane/facade/operation-digest.ts +528 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +403 -0
- package/src/runtime/control-plane/facade/policy-gate.ts +149 -0
- package/src/runtime/control-plane/facade/requirement-board.ts +280 -0
- package/src/runtime/control-plane/facade/self-healing-loop.ts +555 -0
- package/src/runtime/control-plane/facade/suggested-actions.ts +108 -0
- package/src/runtime/control-plane/facade/types.ts +729 -0
- package/src/runtime/control-plane/facade/work-admission-policy.ts +93 -0
- package/src/runtime/control-plane/facade/work-continuation.ts +104 -0
- package/src/runtime/control-plane/facade/work-contract-store.ts +1118 -0
- package/src/runtime/control-plane/facade/work-task-projection.ts +66 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +365 -0
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +920 -0
- package/src/runtime/control-plane/global-scheduler/wake-signal.ts +89 -0
- package/src/runtime/control-plane/goal-loop/config-facade.ts +700 -0
- package/src/runtime/control-plane/goal-loop/config-store.ts +570 -0
- package/src/runtime/control-plane/goal-loop/config-types.ts +213 -0
- package/src/runtime/control-plane/goal-loop/executor-router.ts +109 -0
- package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +149 -0
- package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +304 -0
- package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +1001 -0
- package/src/runtime/control-plane/goal-loop/handoff-packet.ts +160 -0
- package/src/runtime/control-plane/goal-loop/index.ts +13 -0
- package/src/runtime/control-plane/goal-loop/local-tools.ts +290 -0
- package/src/runtime/control-plane/goal-loop/policy-gates.ts +203 -0
- package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +93 -0
- package/src/runtime/control-plane/goal-loop/provider-registry.ts +545 -0
- package/src/runtime/control-plane/goal-loop/types.ts +378 -0
- package/src/runtime/control-plane/governance/access-policy.ts +147 -0
- package/src/runtime/control-plane/governance/authorization.ts +195 -0
- package/src/runtime/control-plane/governance/external-effects.ts +43 -0
- package/src/runtime/control-plane/launcher/thin-launcher.ts +97 -0
- package/src/runtime/control-plane/persistence/metadata-payload-policy.ts +53 -0
- package/src/runtime/control-plane/persistence/requirement-store.ts +241 -0
- package/src/runtime/control-plane/persistence/sqlite-store.ts +539 -0
- package/src/runtime/control-plane/release-retention.ts +447 -0
- package/src/runtime/control-plane/repo-actor/actor.ts +181 -0
- package/src/runtime/control-plane/repo-actor/registry.ts +20 -0
- package/src/runtime/control-plane/routing/index.ts +1 -0
- package/src/runtime/control-plane/routing/route-policy.ts +379 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +727 -0
- package/src/runtime/control-plane/runtime-generation.ts +509 -0
- package/src/runtime/control-plane/runtime-pid-ownership.ts +11 -0
- package/src/runtime/control-plane/runtime-status-client.ts +90 -0
- package/src/runtime/control-plane/startup-recovery.ts +198 -0
- package/src/runtime/diagnostics/connectivity-observability.ts +475 -0
- package/src/runtime/diagnostics/mcp-timing.ts +64 -0
- package/src/runtime/diagnostics/performance.ts +379 -0
- package/src/runtime/diagnostics/process-facade.ts +350 -0
- package/src/runtime/diagnostics/read-only-tool.ts +226 -0
- package/src/runtime/evidence/artifact-store.ts +154 -0
- package/src/runtime/evidence/event-ledger.ts +80 -0
- package/src/runtime/evidence/evidence-store.ts +68 -0
- package/src/runtime/evidence/process-check-receipt.ts +43 -0
- package/src/runtime/evidence/result-store.ts +209 -0
- package/src/runtime/evidence/sensitive-output.ts +353 -0
- package/src/runtime/execution/jobs/child-reference.ts +124 -0
- package/src/runtime/execution/jobs/legacy-adapter.ts +22 -0
- package/src/runtime/execution/jobs/receipt-recovery.ts +119 -0
- package/src/runtime/execution/jobs/receipt-store.ts +243 -0
- package/src/runtime/execution/jobs/store.ts +737 -0
- package/src/runtime/execution/jobs/timeouts.ts +109 -0
- package/src/runtime/execution/jobs/types.ts +230 -0
- package/src/runtime/execution/jobs/wait.ts +45 -0
- package/src/runtime/execution/process-runtime/canary.ts +26 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +272 -0
- package/src/runtime/execution/process-runtime/check-receipt.ts +185 -0
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +60 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +321 -0
- package/src/runtime/execution/process-runtime/gc.ts +171 -0
- package/src/runtime/execution/process-runtime/index.ts +9 -0
- package/src/runtime/execution/process-runtime/interactive-admission.ts +23 -0
- package/src/runtime/execution/process-runtime/process-runner-entry.ts +466 -0
- package/src/runtime/execution/process-runtime/resource-claims.ts +440 -0
- package/src/runtime/execution/process-runtime/runtime.ts +1816 -0
- package/src/runtime/execution/process-runtime/store.ts +687 -0
- package/src/runtime/execution/process-runtime/types.ts +352 -0
- package/src/runtime/execution/thin-harness/async-process.ts +181 -0
- package/src/runtime/execution/thin-harness/batch-executor.ts +671 -0
- package/src/runtime/execution/thin-harness/execution-router.ts +700 -0
- package/src/runtime/execution/thin-harness/fast-executor.ts +1263 -0
- package/src/runtime/execution/thin-harness/fast-receipt.ts +235 -0
- package/src/runtime/execution/thin-harness/fingerprint-worker.ts +191 -0
- package/src/runtime/execution/thin-harness/index.ts +13 -0
- package/src/runtime/execution/thin-harness/latency-trace.ts +107 -0
- package/src/runtime/execution/thin-harness/lightweight-lanes.ts +745 -0
- package/src/runtime/execution/thin-harness/mutation-gate.ts +578 -0
- package/src/runtime/execution/thin-harness/proposal-store.ts +409 -0
- package/src/runtime/execution/thin-harness/request-ledger.ts +595 -0
- package/src/runtime/execution/thin-harness/search-worker.ts +114 -0
- package/src/runtime/execution/thin-harness/types.ts +313 -0
- package/src/runtime/execution/thin-harness/workspace-savepoint.ts +333 -0
- package/src/runtime/execution/workers/executor.ts +687 -0
- package/src/runtime/execution/workers/ownership.ts +94 -0
- package/src/runtime/execution/workers/worker-entry.ts +277 -0
- package/src/runtime/gateway/mcp/execution-tools.ts +2009 -0
- package/src/runtime/gateway/mcp/persisted-check-process.ts +255 -0
- package/src/runtime/gateway/mcp/process-tools.ts +230 -0
- package/src/runtime/gateway/mcp/resource-policy.ts +195 -0
- package/src/runtime/gateway/mcp/router.ts +1088 -0
- package/src/runtime/gateway/mcp/runtime-tools.ts +5994 -0
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +219 -0
- package/src/runtime/health/evaluator.ts +474 -0
- package/src/runtime/health/index.ts +2 -0
- package/src/runtime/health/operational-view.ts +198 -0
- package/src/runtime/maintenance/cleanup.ts +419 -0
- package/src/runtime/model-clients/deepseek-adapter.ts +312 -0
- package/src/runtime/model-clients/index.ts +2 -0
- package/src/runtime/model-clients/types.ts +101 -0
- package/src/runtime/observability/gateway-contention-metrics.ts +129 -0
- package/src/runtime/observability/route-session-benchmark.ts +110 -0
- package/src/runtime/personal-assistant/gmail-triage-manager.ts +231 -0
- package/src/runtime/personal-assistant/reporting-runtime.ts +344 -0
- package/src/runtime/personal-assistant/triage-runtime.ts +748 -0
- package/src/runtime/plugins/app-store-connect-adapter.ts +868 -0
- package/src/runtime/plugins/browser-adapter.ts +2812 -0
- package/src/runtime/plugins/browser-automation-service.ts +46 -0
- package/src/runtime/plugins/browser-handoff-host.ts +304 -0
- package/src/runtime/plugins/browser-handoff.ts +371 -0
- package/src/runtime/plugins/browser-macos-bridge.ts +865 -0
- package/src/runtime/plugins/browser-node-bridge-host.ts +64 -0
- package/src/runtime/plugins/browser-node-bridge.ts +249 -0
- package/src/runtime/plugins/desktop-adapter.ts +374 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +418 -0
- package/src/runtime/plugins/errors.ts +39 -0
- package/src/runtime/plugins/execution-origin.ts +26 -0
- package/src/runtime/plugins/external-adapter.ts +322 -0
- package/src/runtime/plugins/external-registration.ts +308 -0
- package/src/runtime/plugins/external-unix-socket-probe.cjs +79 -0
- package/src/runtime/plugins/external-unix-socket.ts +238 -0
- package/src/runtime/plugins/first-party-registry.ts +37 -0
- package/src/runtime/plugins/github-adapter.ts +266 -0
- package/src/runtime/plugins/gmail-adapter.ts +729 -0
- package/src/runtime/plugins/google-calendar-adapter.ts +468 -0
- package/src/runtime/plugins/google-shared.ts +578 -0
- package/src/runtime/plugins/google-tasks-adapter.ts +535 -0
- package/src/runtime/plugins/interaction-session.ts +252 -0
- package/src/runtime/plugins/ios/agent-device-capabilities.ts +258 -0
- package/src/runtime/plugins/ios/agent-device-failures.ts +95 -0
- package/src/runtime/plugins/ios/agent-device-provider.ts +63 -0
- package/src/runtime/plugins/ios/agent-device-typed-provider.ts +268 -0
- package/src/runtime/plugins/ios/app-adapters.ts +122 -0
- package/src/runtime/plugins/ios-adapter.ts +509 -0
- package/src/runtime/plugins/ios-agent-device.ts +2311 -0
- package/src/runtime/plugins/ios-physical-device.ts +955 -0
- package/src/runtime/plugins/local-system-adapter.ts +1016 -0
- package/src/runtime/plugins/macos-capability-broker.ts +73 -0
- package/src/runtime/plugins/managed-process-adapter.ts +366 -0
- package/src/runtime/plugins/resend-adapter.ts +506 -0
- package/src/runtime/plugins/store.ts +1025 -0
- package/src/runtime/plugins/types.ts +138 -0
- package/src/runtime/projections/controller-context.ts +596 -0
- package/src/runtime/projections/git-status-sampler.ts +84 -0
- package/src/runtime/projections/invalidation.ts +250 -0
- package/src/runtime/projections/materialized-view.ts +694 -0
- package/src/runtime/recovery/actions.ts +158 -0
- package/src/runtime/recovery/audit.ts +38 -0
- package/src/runtime/recovery/classifier.ts +163 -0
- package/src/runtime/recovery/index.ts +9 -0
- package/src/runtime/recovery/local-jobs-repair.ts +393 -0
- package/src/runtime/recovery/maintenance-executor.ts +822 -0
- package/src/runtime/recovery/patch-handoff.ts +50 -0
- package/src/runtime/recovery/probe.ts +308 -0
- package/src/runtime/recovery/store.ts +35 -0
- package/src/runtime/recovery/types.ts +175 -0
- package/src/runtime/release/identity.ts +101 -0
- package/src/runtime/release/release-gate.ts +137 -0
- package/src/runtime/resources/claims/conflicts.ts +205 -0
- package/src/runtime/resources/index.ts +1 -0
- package/src/runtime/resources/leases/store.ts +539 -0
- package/src/runtime/resources/leases/types.ts +50 -0
- package/src/runtime/resources/ownership.ts +72 -0
- package/src/runtime/root/controller-services.ts +42 -0
- package/src/runtime/root/entry.ts +70 -0
- package/src/runtime/root/gateway-adapter.ts +46 -0
- package/src/runtime/root/local-bridge.ts +72 -0
- package/src/runtime/root/mcp-transport.ts +198 -0
- package/src/runtime/root/ownership.ts +94 -0
- package/src/runtime/root/readiness.ts +69 -0
- package/src/runtime/root/release-manifest.ts +147 -0
- package/src/runtime/root/release-materialize.ts +509 -0
- package/src/runtime/root/release-store.ts +236 -0
- package/src/runtime/root/runtime.ts +318 -0
- package/src/runtime/root/scheduler.ts +58 -0
- package/src/runtime/root/service-runner-entry.ts +20 -0
- package/src/runtime/root/service-runner.ts +69 -0
- package/src/runtime/root/service.ts +291 -0
- package/src/runtime/root/status.ts +127 -0
- package/src/runtime/root/types.ts +112 -0
- package/src/runtime/root/write-fence.ts +309 -0
- package/src/runtime/safe-tooling/browser-safe.ts +133 -0
- package/src/runtime/safe-tooling/external-filesystem.ts +304 -0
- package/src/runtime/safe-tooling/google-credential-store.ts +95 -0
- package/src/runtime/safe-tooling/google-oauth-broker.ts +286 -0
- package/src/runtime/safe-tooling/index.ts +11 -0
- package/src/runtime/safe-tooling/ios-development.ts +1039 -0
- package/src/runtime/safe-tooling/job-summary.ts +361 -0
- package/src/runtime/safe-tooling/plugin-summary.ts +53 -0
- package/src/runtime/safe-tooling/resend-oauth.ts +387 -0
- package/src/runtime/safe-tooling/review-artifacts.ts +133 -0
- package/src/runtime/safe-tooling/types.ts +103 -0
- package/src/runtime/safe-tooling/workspace-auth.ts +116 -0
- package/src/runtime/shared/bounded-child-supervisor.ts +235 -0
- package/src/runtime/shared/idempotent-retry.ts +128 -0
- package/src/runtime/shared/json-files.ts +43 -0
- package/src/runtime/shared/local-bridge-surface.ts +231 -0
- package/src/runtime/shared/managed-env.ts +154 -0
- package/src/runtime/shared/node-ts-loader.mjs +50 -0
- package/src/runtime/shared/process-environment.ts +97 -0
- package/src/runtime/shared/process-identity.ts +72 -0
- package/src/runtime/shared/process-tree.ts +227 -0
- package/src/runtime/shared/response-budget.ts +246 -0
- package/src/runtime/standalone-recovery/core.ts +2275 -0
- package/src/runtime/standalone-recovery/entry.ts +686 -0
- package/src/runtime/standalone-recovery/http-transport.ts +317 -0
- package/src/runtime/standalone-recovery/installer.ts +747 -0
- package/src/runtime/standalone-recovery/release.ts +241 -0
- package/src/runtime/watchdog/workflow-watchdog.ts +118 -0
- package/src/runtime/workflow/campaigns/cleanup.ts +263 -0
- package/src/runtime/workflow/campaigns/engine.ts +112 -0
- package/src/runtime/workflow/campaigns/normalize.ts +100 -0
- package/src/runtime/workflow/campaigns/review.ts +315 -0
- package/src/runtime/workflow/campaigns/store.ts +526 -0
- package/src/runtime/workflow/campaigns/supervisor.ts +112 -0
- package/src/runtime/workflow/campaigns/types.ts +290 -0
- package/src/runtime/workflow/campaigns/workspace-agent.ts +155 -0
- package/src/runtime/workflow/campaigns/workspace.ts +200 -0
- package/src/runtime/workflow/findings/store.ts +165 -0
- package/src/runtime/workflow/findings/types.ts +36 -0
- package/src/runtime/workflow/portfolio/engine.ts +20 -0
- package/src/runtime/workflow/portfolio/store.ts +185 -0
- package/src/runtime/workflow/portfolio/types.ts +29 -0
- package/src/runtime/workflow/schedules/engine.ts +620 -0
- package/src/runtime/workflow/schedules/settlement.ts +144 -0
- package/src/runtime/workflow/schedules/store.ts +417 -0
- package/src/runtime/workflow/schedules/types.ts +124 -0
- package/src/runtime/workspace-targets/index.ts +24 -0
- package/src/runtime/workspace-targets/target-grant.ts +721 -0
- package/src/testing/test-governance.ts +626 -0
- package/src/version.ts +21 -0
|
@@ -0,0 +1,2311 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { existsSync, mkdirSync, realpathSync } from 'fs';
|
|
3
|
+
import { isAbsolute, join } from 'path';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import { performance } from 'perf_hooks';
|
|
6
|
+
import { repositoryControllerRoot } from '../../cli/repositories/controller-home';
|
|
7
|
+
import { runBoundedProcess } from '../execution/thin-harness/async-process';
|
|
8
|
+
import { readJsonFile, writeJsonAtomic } from '../shared/json-files';
|
|
9
|
+
import { AssistantPluginError, toAssistantPluginError } from './errors';
|
|
10
|
+
import {
|
|
11
|
+
interactionAutomationEngine,
|
|
12
|
+
interactionMayOwnTarget,
|
|
13
|
+
interactionTargetIdentifiers,
|
|
14
|
+
isInteractionSessionActive,
|
|
15
|
+
listInteractionSessions,
|
|
16
|
+
patchInteractionSession,
|
|
17
|
+
pruneInteractionSessions,
|
|
18
|
+
readInteractionSession,
|
|
19
|
+
writeInteractionSession,
|
|
20
|
+
type InteractionProvider,
|
|
21
|
+
type InteractionSessionRecord,
|
|
22
|
+
} from './interaction-session';
|
|
23
|
+
import type {
|
|
24
|
+
AssistantPluginActionDescriptor,
|
|
25
|
+
AssistantPluginActionExecutionInput,
|
|
26
|
+
AssistantPluginCapability,
|
|
27
|
+
} from './types';
|
|
28
|
+
import {
|
|
29
|
+
appendSettleFlag,
|
|
30
|
+
compileBatchCommand,
|
|
31
|
+
compileSnapshotCommand,
|
|
32
|
+
detectAgentDeviceCapabilities,
|
|
33
|
+
PREFERRED_AGENT_DEVICE_VERSION,
|
|
34
|
+
type AgentDeviceCapabilityProfile,
|
|
35
|
+
type AgentDeviceHelpContract,
|
|
36
|
+
} from './ios/agent-device-capabilities';
|
|
37
|
+
import { classifyAgentDeviceFailure, preserveSessionFailure } from './ios/agent-device-failures';
|
|
38
|
+
import {
|
|
39
|
+
findSemanticNode,
|
|
40
|
+
findSemanticRef,
|
|
41
|
+
JD_IOS_APP_ADAPTER,
|
|
42
|
+
normalizedSemanticRef,
|
|
43
|
+
} from './ios/app-adapters';
|
|
44
|
+
import {
|
|
45
|
+
agentDeviceProviderVersionsMatch,
|
|
46
|
+
configuredAgentDeviceBackendMode,
|
|
47
|
+
type AgentDeviceSnapshotRequest,
|
|
48
|
+
} from './ios/agent-device-provider';
|
|
49
|
+
import {
|
|
50
|
+
isTypedProviderUnavailable,
|
|
51
|
+
TypedAgentDeviceReadProvider,
|
|
52
|
+
typedAgentDeviceIdentity,
|
|
53
|
+
} from './ios/agent-device-typed-provider';
|
|
54
|
+
|
|
55
|
+
export const IOS_AGENT_DEVICE_VERSION = PREFERRED_AGENT_DEVICE_VERSION;
|
|
56
|
+
const SIMULATOR_PROVIDER = 'ios-simulator' as const;
|
|
57
|
+
const DEVICE_PROVIDER = 'ios-device' as const;
|
|
58
|
+
const PROVIDERS: InteractionProvider[] = [SIMULATOR_PROVIDER, DEVICE_PROVIDER];
|
|
59
|
+
const STATUS_TTL_MS = 60_000;
|
|
60
|
+
const MAX_JSON_BYTES = 64 * 1024;
|
|
61
|
+
const SESSION_EXPIRY_MS = 2 * 60 * 60_000;
|
|
62
|
+
const JD_BUNDLE_ID = 'com.360buy.jdmobile';
|
|
63
|
+
const MAX_JD_QUERY_LENGTH = 120;
|
|
64
|
+
const MAX_BATCH_STEPS = 20;
|
|
65
|
+
const DEFAULT_AGENT_DEVICE_IDLE_MS = '300000';
|
|
66
|
+
const BATCH_KINDS = ['snapshot', 'press', 'fill', 'scroll', 'keyboard', 'wait', 'back'] as const;
|
|
67
|
+
type AgentDeviceBatchKind = typeof BATCH_KINDS[number];
|
|
68
|
+
|
|
69
|
+
interface AgentDeviceBatchStep {
|
|
70
|
+
kind: AgentDeviceBatchKind;
|
|
71
|
+
input: Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface PreparedAgentDeviceBatch {
|
|
75
|
+
nativeSteps: Array<{ command: AgentDeviceBatchKind; input: Record<string, unknown> }>;
|
|
76
|
+
redactions: string[];
|
|
77
|
+
}
|
|
78
|
+
const SENSITIVE_SEMANTICS = /secure\s*text|securetextfield|password|passcode|verification|one[ -]?time|otp|2fa|密码|口令|验证码|校验码|短信码|生物识别|biometric|face\s?id|touch\s?id|支付|付款|购买|下单|提交订单|确认订单|结算|checkout|payment|purchase|confirm\s+order|bank|card|cvv|身份证/i;
|
|
79
|
+
|
|
80
|
+
interface AgentDeviceSigningConfig {
|
|
81
|
+
schemaVersion: 1;
|
|
82
|
+
teamId?: string;
|
|
83
|
+
bundleId?: string;
|
|
84
|
+
developerDir?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface CommandResult {
|
|
88
|
+
ok: boolean;
|
|
89
|
+
status: number | null;
|
|
90
|
+
stdout: string;
|
|
91
|
+
stderr: string;
|
|
92
|
+
command: string[];
|
|
93
|
+
timedOut?: boolean;
|
|
94
|
+
cancelled?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface CommandOptions {
|
|
98
|
+
cwd?: string;
|
|
99
|
+
timeoutMs?: number;
|
|
100
|
+
env?: NodeJS.ProcessEnv;
|
|
101
|
+
signal?: AbortSignal;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface IosAgentDeviceRuntimeHooks {
|
|
105
|
+
platform(): NodeJS.Platform;
|
|
106
|
+
now(): Date;
|
|
107
|
+
runCommand(command: string, args: string[], options?: CommandOptions): CommandResult;
|
|
108
|
+
runCommandAsync(command: string, args: string[], options?: CommandOptions): Promise<CommandResult>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const defaultHooks: IosAgentDeviceRuntimeHooks = {
|
|
112
|
+
platform: () => process.platform,
|
|
113
|
+
now: () => new Date(),
|
|
114
|
+
runCommand: (command, args, options = {}) => {
|
|
115
|
+
const result = spawnSync(command, args, {
|
|
116
|
+
cwd: options.cwd,
|
|
117
|
+
env: options.env,
|
|
118
|
+
encoding: 'utf8',
|
|
119
|
+
timeout: options.timeoutMs ?? 30_000,
|
|
120
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
121
|
+
});
|
|
122
|
+
const timedOut = (result.error as NodeJS.ErrnoException | undefined)?.code === 'ETIMEDOUT';
|
|
123
|
+
return {
|
|
124
|
+
ok: result.status === 0 && !timedOut,
|
|
125
|
+
status: result.status,
|
|
126
|
+
stdout: String(result.stdout ?? ''),
|
|
127
|
+
stderr: String(result.stderr ?? result.error?.message ?? ''),
|
|
128
|
+
command: [command, ...args],
|
|
129
|
+
timedOut,
|
|
130
|
+
cancelled: options.signal?.aborted === true,
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
runCommandAsync: async (command, args, options = {}) => {
|
|
134
|
+
const result = await runBoundedProcess(command, args, {
|
|
135
|
+
cwd: options.cwd ?? process.cwd(),
|
|
136
|
+
env: options.env,
|
|
137
|
+
timeoutMs: options.timeoutMs ?? 30_000,
|
|
138
|
+
maxOutputBytes: 4 * 1024 * 1024,
|
|
139
|
+
signal: options.signal,
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
ok: result.ok,
|
|
143
|
+
status: result.exitCode,
|
|
144
|
+
stdout: result.stdout,
|
|
145
|
+
stderr: result.stderr,
|
|
146
|
+
command: [command, ...args],
|
|
147
|
+
timedOut: result.timedOut,
|
|
148
|
+
cancelled: result.cancelled,
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
let hooks: IosAgentDeviceRuntimeHooks = { ...defaultHooks };
|
|
154
|
+
let statusCache: { cacheKey: string; expiresAt: number; value: ReturnType<typeof probeStatus> } | undefined;
|
|
155
|
+
const interactionCommandTails = new Map<string, Promise<void>>();
|
|
156
|
+
|
|
157
|
+
export function setIosAgentDeviceRuntimeHooksForTest(overrides: Partial<IosAgentDeviceRuntimeHooks>): void {
|
|
158
|
+
hooks = { ...defaultHooks, ...overrides };
|
|
159
|
+
if (overrides.runCommand && !overrides.runCommandAsync) {
|
|
160
|
+
hooks.runCommandAsync = async (command, args, options) => overrides.runCommand!(command, args, options);
|
|
161
|
+
}
|
|
162
|
+
statusCache = undefined;
|
|
163
|
+
interactionCommandTails.clear();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function resetIosAgentDeviceRuntimeHooksForTest(): void {
|
|
167
|
+
hooks = { ...defaultHooks };
|
|
168
|
+
statusCache = undefined;
|
|
169
|
+
interactionCommandTails.clear();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function executable(): string {
|
|
173
|
+
return process.env.FORGE_AGENT_DEVICE_EXECUTABLE?.trim() || 'agent-device';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function resolvedExecutable(): string {
|
|
177
|
+
const configured = executable();
|
|
178
|
+
if (isAbsolute(configured)) {
|
|
179
|
+
try { return realpathSync(configured); } catch { return configured; }
|
|
180
|
+
}
|
|
181
|
+
for (const directory of (process.env.PATH ?? '').split(':').filter(Boolean)) {
|
|
182
|
+
const candidate = join(directory, configured);
|
|
183
|
+
if (!existsSync(candidate)) continue;
|
|
184
|
+
try { return realpathSync(candidate); } catch { return candidate; }
|
|
185
|
+
}
|
|
186
|
+
return configured;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function timestamp(): string {
|
|
190
|
+
return hooks.now().toISOString();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function sanitize(value: string): string {
|
|
194
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'ios-agent-device';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function statusProbeCacheKey(): string {
|
|
198
|
+
return [
|
|
199
|
+
hooks.platform(),
|
|
200
|
+
executable(),
|
|
201
|
+
resolvedExecutable(),
|
|
202
|
+
configuredAgentDeviceBackendMode(),
|
|
203
|
+
process.versions.node,
|
|
204
|
+
process.env.PATH ?? '',
|
|
205
|
+
].join('|');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function probeStatus() {
|
|
209
|
+
const backendMode = configuredAgentDeviceBackendMode();
|
|
210
|
+
const typedClient = typedAgentDeviceIdentity();
|
|
211
|
+
if (hooks.platform() !== 'darwin') {
|
|
212
|
+
return {
|
|
213
|
+
available: false,
|
|
214
|
+
expectedVersion: IOS_AGENT_DEVICE_VERSION,
|
|
215
|
+
supportedVersionPolicy: '>=0.19.3 <0.21.0 with reviewed command contract',
|
|
216
|
+
detectedVersion: undefined,
|
|
217
|
+
executable: executable(),
|
|
218
|
+
resolvedExecutable: resolvedExecutable(),
|
|
219
|
+
platform: hooks.platform(),
|
|
220
|
+
capabilityProfile: detectAgentDeviceCapabilities('', {}),
|
|
221
|
+
backendMode,
|
|
222
|
+
typedClient,
|
|
223
|
+
reason: 'agent-device iOS support requires macOS.',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const result = hooks.runCommand(executable(), ['--version'], { timeoutMs: 3_000 });
|
|
227
|
+
const detectedVersion = result.ok ? result.stdout.trim() : undefined;
|
|
228
|
+
const help: AgentDeviceHelpContract = {};
|
|
229
|
+
if (result.ok && detectedVersion) {
|
|
230
|
+
const rootHelp = hooks.runCommand(executable(), ['help'], { timeoutMs: 3_000 });
|
|
231
|
+
if (rootHelp.ok) help.root = rootHelp.stdout || rootHelp.stderr;
|
|
232
|
+
for (const topic of ['snapshot', 'press', 'fill', 'batch', 'keyboard'] as const) {
|
|
233
|
+
const topicResult = hooks.runCommand(executable(), ['help', topic], { timeoutMs: 3_000 });
|
|
234
|
+
if (topicResult.ok) help[topic] = topicResult.stdout || topicResult.stderr;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const capabilityProfile = detectAgentDeviceCapabilities(detectedVersion ?? '', help);
|
|
238
|
+
const typedClientWithCompatibility = {
|
|
239
|
+
...typedClient,
|
|
240
|
+
cliVersion: detectedVersion,
|
|
241
|
+
cliVersionCompatible: agentDeviceProviderVersionsMatch(typedClient.version, detectedVersion),
|
|
242
|
+
};
|
|
243
|
+
const available = result.ok
|
|
244
|
+
&& capabilityProfile.versionSupported
|
|
245
|
+
&& Boolean(capabilityProfile.snapshot.interactiveFlag)
|
|
246
|
+
&& capabilityProfile.press.supported
|
|
247
|
+
&& capabilityProfile.fill.supported
|
|
248
|
+
&& capabilityProfile.batch.supported;
|
|
249
|
+
return {
|
|
250
|
+
available,
|
|
251
|
+
expectedVersion: IOS_AGENT_DEVICE_VERSION,
|
|
252
|
+
supportedVersionPolicy: '>=0.19.3 <0.21.0 with reviewed command contract',
|
|
253
|
+
detectedVersion,
|
|
254
|
+
executable: executable(),
|
|
255
|
+
resolvedExecutable: resolvedExecutable(),
|
|
256
|
+
platform: hooks.platform(),
|
|
257
|
+
capabilityProfile,
|
|
258
|
+
backendMode,
|
|
259
|
+
typedClient: typedClientWithCompatibility,
|
|
260
|
+
reason: !result.ok
|
|
261
|
+
? (result.stderr || result.stdout || 'agent-device is not installed.')
|
|
262
|
+
: !capabilityProfile.versionSupported
|
|
263
|
+
? `Unsupported agent-device version ${detectedVersion || 'unknown'}; expected the reviewed >=0.19.3 <0.21.0 contract.`
|
|
264
|
+
: !available
|
|
265
|
+
? 'The installed agent-device command contract is missing required snapshot, press, fill, or batch capabilities.'
|
|
266
|
+
: undefined,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function iosAgentDeviceStatus(options: { forceRefresh?: boolean } = {}) {
|
|
271
|
+
const nowMs = hooks.now().getTime();
|
|
272
|
+
const cacheKey = statusProbeCacheKey();
|
|
273
|
+
if (!options.forceRefresh
|
|
274
|
+
&& statusCache
|
|
275
|
+
&& statusCache.cacheKey === cacheKey
|
|
276
|
+
&& statusCache.expiresAt > nowMs) return statusCache.value;
|
|
277
|
+
const value = probeStatus();
|
|
278
|
+
statusCache = { cacheKey, expiresAt: nowMs + STATUS_TTL_MS, value };
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function requireDependency(): ReturnType<typeof probeStatus> {
|
|
283
|
+
const status = iosAgentDeviceStatus();
|
|
284
|
+
if (!status.available) {
|
|
285
|
+
throw new AssistantPluginError('PLUGIN_DEPENDENCY_MISSING', status.reason ?? 'A compatible agent-device provider is unavailable.', {
|
|
286
|
+
retryable: false,
|
|
287
|
+
details: status,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return status;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function capabilityProfile(): AgentDeviceCapabilityProfile {
|
|
294
|
+
return requireDependency().capabilityProfile;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function controllerRoot(input: AssistantPluginActionExecutionInput): string {
|
|
298
|
+
return repositoryControllerRoot(input.controllerHome, input.repoId);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function interactionRoot(input: AssistantPluginActionExecutionInput, interactionId: string): string {
|
|
302
|
+
return join(controllerRoot(input), 'interactions', 'ios-agent-device', sanitize(interactionId));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function stateDir(input: AssistantPluginActionExecutionInput, interactionId: string): string {
|
|
306
|
+
const path = join(interactionRoot(input, interactionId), 'state');
|
|
307
|
+
mkdirSync(path, { recursive: true });
|
|
308
|
+
return path;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function signingConfigPath(input: AssistantPluginActionExecutionInput, interactionId: string): string {
|
|
312
|
+
return join(interactionRoot(input, interactionId), 'signing.json');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function readSigningConfig(
|
|
316
|
+
input: AssistantPluginActionExecutionInput,
|
|
317
|
+
interactionId: string,
|
|
318
|
+
): AgentDeviceSigningConfig | undefined {
|
|
319
|
+
const value = readJsonFile<AgentDeviceSigningConfig | undefined>(signingConfigPath(input, interactionId), undefined);
|
|
320
|
+
return value?.schemaVersion === 1 ? value : undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function writeSigningConfig(
|
|
324
|
+
input: AssistantPluginActionExecutionInput,
|
|
325
|
+
interactionId: string,
|
|
326
|
+
config: AgentDeviceSigningConfig,
|
|
327
|
+
): void {
|
|
328
|
+
writeJsonAtomic(signingConfigPath(input, interactionId), config);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function signingEnv(config?: AgentDeviceSigningConfig): NodeJS.ProcessEnv {
|
|
332
|
+
return {
|
|
333
|
+
...(config?.teamId ? { AGENT_DEVICE_IOS_TEAM_ID: config.teamId } : {}),
|
|
334
|
+
...(config?.bundleId ? { AGENT_DEVICE_IOS_BUNDLE_ID: config.bundleId } : {}),
|
|
335
|
+
...(config?.developerDir ? { DEVELOPER_DIR: config.developerDir } : {}),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function artifactDir(input: AssistantPluginActionExecutionInput, interactionId: string): string {
|
|
340
|
+
const path = join(controllerRoot(input), 'artifacts', 'ios', 'agent-device', sanitize(interactionId));
|
|
341
|
+
mkdirSync(path, { recursive: true });
|
|
342
|
+
return path;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function sessionEnv(input: AssistantPluginActionExecutionInput, record: InteractionSessionRecord): NodeJS.ProcessEnv {
|
|
346
|
+
const config = readSigningConfig(input, record.interactionId);
|
|
347
|
+
return {
|
|
348
|
+
...process.env,
|
|
349
|
+
...signingEnv(config),
|
|
350
|
+
AGENT_DEVICE_STATE_DIR: stateDir(input, record.interactionId),
|
|
351
|
+
AGENT_DEVICE_SESSION: record.sessionId,
|
|
352
|
+
AGENT_DEVICE_PLATFORM: 'ios',
|
|
353
|
+
AGENT_DEVICE_SESSION_LOCK: 'reject',
|
|
354
|
+
// Keep the per-interaction daemon and a healthy XCTest runner warm between
|
|
355
|
+
// commands. The previous five-second/zero-retention policy forced repeated
|
|
356
|
+
// cold starts during ordinary multi-step device workflows.
|
|
357
|
+
AGENT_DEVICE_DAEMON_IDLE_TIMEOUT_MS:
|
|
358
|
+
process.env.FORGE_AGENT_DEVICE_DAEMON_IDLE_TIMEOUT_MS?.trim() || DEFAULT_AGENT_DEVICE_IDLE_MS,
|
|
359
|
+
AGENT_DEVICE_IOS_RUNNER_IDLE_STOP_MS:
|
|
360
|
+
process.env.FORGE_AGENT_DEVICE_IOS_RUNNER_IDLE_STOP_MS?.trim() || DEFAULT_AGENT_DEVICE_IDLE_MS,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function probeEnv(input: AssistantPluginActionExecutionInput, config?: AgentDeviceSigningConfig): NodeJS.ProcessEnv {
|
|
365
|
+
const path = join(controllerRoot(input), 'interactions', 'ios-agent-device', 'probe-state');
|
|
366
|
+
mkdirSync(path, { recursive: true });
|
|
367
|
+
return {
|
|
368
|
+
...process.env,
|
|
369
|
+
...signingEnv(config),
|
|
370
|
+
AGENT_DEVICE_STATE_DIR: path,
|
|
371
|
+
AGENT_DEVICE_PLATFORM: 'ios',
|
|
372
|
+
AGENT_DEVICE_DAEMON_IDLE_TIMEOUT_MS:
|
|
373
|
+
process.env.FORGE_AGENT_DEVICE_DAEMON_IDLE_TIMEOUT_MS?.trim() || DEFAULT_AGENT_DEVICE_IDLE_MS,
|
|
374
|
+
AGENT_DEVICE_IOS_RUNNER_IDLE_STOP_MS:
|
|
375
|
+
process.env.FORGE_AGENT_DEVICE_IOS_RUNNER_IDLE_STOP_MS?.trim() || DEFAULT_AGENT_DEVICE_IDLE_MS,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function bounded(value: unknown): unknown {
|
|
380
|
+
const text = JSON.stringify(value);
|
|
381
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
382
|
+
if (bytes <= MAX_JSON_BYTES) return value;
|
|
383
|
+
return {
|
|
384
|
+
truncated: true,
|
|
385
|
+
byteLength: bytes,
|
|
386
|
+
preview: Buffer.from(text, 'utf8').subarray(0, MAX_JSON_BYTES).toString('utf8'),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function redactExactText(value: unknown, text: string): unknown {
|
|
391
|
+
if (!text) return value;
|
|
392
|
+
if (typeof value === 'string') return value.split(text).join('<redacted>');
|
|
393
|
+
if (Array.isArray(value)) return value.map((entry) => redactExactText(entry, text));
|
|
394
|
+
if (value && typeof value === 'object') {
|
|
395
|
+
return Object.fromEntries(Object.entries(value as Record<string, unknown>)
|
|
396
|
+
.map(([key, entry]) => [key, redactExactText(entry, text)]));
|
|
397
|
+
}
|
|
398
|
+
return value;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function redactEventEvidence(value: unknown, key = ''): unknown {
|
|
402
|
+
if (/^(args|arguments|input|payload|text|value)$/i.test(key)) return '<redacted>';
|
|
403
|
+
if (Array.isArray(value)) return value.map((entry) => redactEventEvidence(entry));
|
|
404
|
+
if (value && typeof value === 'object') {
|
|
405
|
+
return Object.fromEntries(Object.entries(value as Record<string, unknown>)
|
|
406
|
+
.map(([entryKey, entry]) => [entryKey, redactEventEvidence(entry, entryKey)]));
|
|
407
|
+
}
|
|
408
|
+
return value;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function redactedCommand(command: string[]): string[] {
|
|
412
|
+
if (command[1] !== 'fill') return command;
|
|
413
|
+
const copy = [...command];
|
|
414
|
+
if (copy.length > 3) copy[3] = '<redacted>';
|
|
415
|
+
return copy;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function parseJsonResult(result: CommandResult, failureCode: string): Record<string, unknown> {
|
|
419
|
+
let parsed: Record<string, unknown> | undefined;
|
|
420
|
+
try {
|
|
421
|
+
const value = JSON.parse(result.stdout.trim());
|
|
422
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) parsed = value as Record<string, unknown>;
|
|
423
|
+
} catch {
|
|
424
|
+
// The structured failure below includes bounded raw diagnostics.
|
|
425
|
+
}
|
|
426
|
+
const success = result.ok && parsed?.success !== false;
|
|
427
|
+
if (!success) {
|
|
428
|
+
const sensitive = result.command[1] === 'fill';
|
|
429
|
+
const secret = sensitive && result.command.length > 3 ? result.command[3]! : '';
|
|
430
|
+
const providerError = parsed?.error && typeof parsed.error === 'object' && !Array.isArray(parsed.error)
|
|
431
|
+
? parsed.error as Record<string, unknown>
|
|
432
|
+
: undefined;
|
|
433
|
+
const providerCode = typeof providerError?.code === 'string' ? providerError.code : undefined;
|
|
434
|
+
const providerHint = typeof providerError?.hint === 'string' ? providerError.hint : undefined;
|
|
435
|
+
const rawMessage = String(
|
|
436
|
+
(typeof providerError?.message === 'string' ? providerError.message : undefined)
|
|
437
|
+
|| result.stderr
|
|
438
|
+
|| result.stdout
|
|
439
|
+
|| 'agent-device command failed.',
|
|
440
|
+
);
|
|
441
|
+
const message = String(redactExactText(rawMessage, secret));
|
|
442
|
+
const terminalCode = result.cancelled
|
|
443
|
+
? 'AGENT_DEVICE_COMMAND_CANCELLED'
|
|
444
|
+
: result.timedOut
|
|
445
|
+
? 'AGENT_DEVICE_COMMAND_TIMEOUT'
|
|
446
|
+
: failureCode;
|
|
447
|
+
throw new AssistantPluginError(terminalCode, message, {
|
|
448
|
+
retryable: result.cancelled === true || result.timedOut === true,
|
|
449
|
+
details: {
|
|
450
|
+
status: result.status,
|
|
451
|
+
command: redactedCommand(result.command),
|
|
452
|
+
providerCode,
|
|
453
|
+
providerHint: providerHint ? redactExactText(providerHint, secret) : undefined,
|
|
454
|
+
stdout: String(redactExactText(result.stdout, secret)).slice(0, 8_000),
|
|
455
|
+
stderr: String(redactExactText(result.stderr, secret)).slice(0, 8_000),
|
|
456
|
+
sensitiveInputRedacted: sensitive,
|
|
457
|
+
timedOut: result.timedOut === true,
|
|
458
|
+
cancelled: result.cancelled === true,
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
return parsed ?? { success: true, data: { stdout: result.stdout.trim() } };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function actionTimeoutError(message = 'The agent-device action exceeded its request deadline.'): AssistantPluginError {
|
|
466
|
+
return new AssistantPluginError('AGENT_DEVICE_COMMAND_TIMEOUT', message, { retryable: true });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function remainingActionTimeout(input: AssistantPluginActionExecutionInput): number | undefined {
|
|
470
|
+
if (typeof input.deadlineAtMs !== 'number' || !Number.isFinite(input.deadlineAtMs)) return undefined;
|
|
471
|
+
return Math.trunc(input.deadlineAtMs - Date.now());
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function effectiveCommandTimeout(input: AssistantPluginActionExecutionInput, timeoutMs = 30_000): number {
|
|
475
|
+
const remaining = remainingActionTimeout(input);
|
|
476
|
+
if (remaining !== undefined && remaining <= 0) throw actionTimeoutError();
|
|
477
|
+
const requestLimit = remaining !== undefined
|
|
478
|
+
? remaining
|
|
479
|
+
: typeof input.timeoutMs === 'number' && Number.isFinite(input.timeoutMs)
|
|
480
|
+
? Math.max(1, Math.trunc(input.timeoutMs))
|
|
481
|
+
: undefined;
|
|
482
|
+
return requestLimit ? Math.max(1, Math.min(timeoutMs, requestLimit)) : timeoutMs;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function runJson(
|
|
486
|
+
input: AssistantPluginActionExecutionInput,
|
|
487
|
+
args: string[],
|
|
488
|
+
options: {
|
|
489
|
+
record?: InteractionSessionRecord;
|
|
490
|
+
signing?: AgentDeviceSigningConfig;
|
|
491
|
+
timeoutMs?: number;
|
|
492
|
+
failureCode: string;
|
|
493
|
+
},
|
|
494
|
+
): Record<string, unknown> {
|
|
495
|
+
const result = hooks.runCommand(executable(), args, {
|
|
496
|
+
cwd: input.repoRoot,
|
|
497
|
+
timeoutMs: effectiveCommandTimeout(input, options.timeoutMs),
|
|
498
|
+
env: options.record ? sessionEnv(input, options.record) : probeEnv(input, options.signing),
|
|
499
|
+
signal: input.signal,
|
|
500
|
+
});
|
|
501
|
+
return parseJsonResult(result, options.failureCode);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function runJsonAsync(
|
|
505
|
+
input: AssistantPluginActionExecutionInput,
|
|
506
|
+
args: string[],
|
|
507
|
+
options: {
|
|
508
|
+
record?: InteractionSessionRecord;
|
|
509
|
+
signing?: AgentDeviceSigningConfig;
|
|
510
|
+
timeoutMs?: number;
|
|
511
|
+
failureCode: string;
|
|
512
|
+
},
|
|
513
|
+
): Promise<Record<string, unknown>> {
|
|
514
|
+
const result = await hooks.runCommandAsync(executable(), args, {
|
|
515
|
+
cwd: input.repoRoot,
|
|
516
|
+
timeoutMs: effectiveCommandTimeout(input, options.timeoutMs),
|
|
517
|
+
env: options.record ? sessionEnv(input, options.record) : probeEnv(input, options.signing),
|
|
518
|
+
signal: input.signal,
|
|
519
|
+
});
|
|
520
|
+
return parseJsonResult(result, options.failureCode);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
interface AgentDeviceEntry {
|
|
524
|
+
platform: string;
|
|
525
|
+
appleOs?: string;
|
|
526
|
+
id: string;
|
|
527
|
+
name: string;
|
|
528
|
+
kind: string;
|
|
529
|
+
target?: string;
|
|
530
|
+
booted: boolean;
|
|
531
|
+
/** Stable provider and hardware identifiers that may name the same target. */
|
|
532
|
+
aliases: string[];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function devices(input: AssistantPluginActionExecutionInput): AgentDeviceEntry[] {
|
|
536
|
+
const response = runJson(input, ['devices', '--platform', 'ios', '--json'], {
|
|
537
|
+
failureCode: 'AGENT_DEVICE_DEVICES_FAILED',
|
|
538
|
+
timeoutMs: 30_000,
|
|
539
|
+
});
|
|
540
|
+
const data = response.data && typeof response.data === 'object' ? response.data as Record<string, unknown> : {};
|
|
541
|
+
return (Array.isArray(data.devices) ? data.devices : [])
|
|
542
|
+
.filter((entry): entry is Record<string, unknown> => Boolean(entry && typeof entry === 'object' && !Array.isArray(entry)))
|
|
543
|
+
.map((entry) => {
|
|
544
|
+
const identifiers = entry.identifiers && typeof entry.identifiers === 'object' && !Array.isArray(entry.identifiers)
|
|
545
|
+
? entry.identifiers as Record<string, unknown>
|
|
546
|
+
: {};
|
|
547
|
+
const ios = entry.ios && typeof entry.ios === 'object' && !Array.isArray(entry.ios)
|
|
548
|
+
? entry.ios as Record<string, unknown>
|
|
549
|
+
: {};
|
|
550
|
+
const id = String(entry.id ?? '');
|
|
551
|
+
const aliases = Array.from(new Set([
|
|
552
|
+
id,
|
|
553
|
+
typeof ios.udid === 'string' ? ios.udid : undefined,
|
|
554
|
+
typeof identifiers.udid === 'string' ? identifiers.udid : undefined,
|
|
555
|
+
typeof identifiers.deviceId === 'string' ? identifiers.deviceId : undefined,
|
|
556
|
+
].filter((value): value is string => Boolean(value?.trim())).map((value) => value.trim())));
|
|
557
|
+
return {
|
|
558
|
+
platform: String(entry.platform ?? ''),
|
|
559
|
+
appleOs: typeof entry.appleOs === 'string' ? entry.appleOs : undefined,
|
|
560
|
+
id,
|
|
561
|
+
name: String(entry.name ?? ''),
|
|
562
|
+
kind: String(entry.kind ?? ''),
|
|
563
|
+
target: typeof entry.target === 'string' ? entry.target : undefined,
|
|
564
|
+
booted: entry.booted === true,
|
|
565
|
+
aliases,
|
|
566
|
+
};
|
|
567
|
+
})
|
|
568
|
+
.filter((entry) => entry.platform === 'ios' && entry.id && entry.name);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function providerForDevice(device: AgentDeviceEntry): InteractionProvider {
|
|
572
|
+
return device.kind === 'simulator' ? SIMULATOR_PROVIDER : DEVICE_PROVIDER;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function selectTarget(input: AssistantPluginActionExecutionInput, selector?: string): AgentDeviceEntry {
|
|
576
|
+
const inventory = devices(input);
|
|
577
|
+
const normalizedSelector = selector?.trim().toLocaleLowerCase('en-US');
|
|
578
|
+
const exact = selector
|
|
579
|
+
? inventory.filter((entry) => entry.name === selector
|
|
580
|
+
|| entry.aliases.some((alias) => alias.toLocaleLowerCase('en-US') === normalizedSelector))
|
|
581
|
+
: inventory.filter((entry) => entry.booted && entry.kind === 'simulator');
|
|
582
|
+
const ready = exact.filter((entry) => entry.booted && (entry.kind === 'simulator' || entry.kind === 'device'));
|
|
583
|
+
if (ready.length === 0) {
|
|
584
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', selector
|
|
585
|
+
? 'Select one connected physical iPhone or already-booted iOS Simulator by exact name or UDID.'
|
|
586
|
+
: 'Select one already-booted iOS Simulator, or provide an exact physical iPhone name or UDID.', {
|
|
587
|
+
retryable: false,
|
|
588
|
+
details: {
|
|
589
|
+
selector,
|
|
590
|
+
matches: exact.map((entry) => ({ id: entry.id, name: entry.name, kind: entry.kind, booted: entry.booted })),
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
if (ready.length !== 1) {
|
|
595
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'The iOS target selection is ambiguous; provide the exact UDID.', {
|
|
596
|
+
retryable: false,
|
|
597
|
+
details: { selector, matches: ready.map((entry) => ({ id: entry.id, name: entry.name, kind: entry.kind })) },
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
return ready[0]!;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function requireString(value: unknown, name: string): string {
|
|
604
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
605
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `${name} is required.`, { retryable: false });
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function optionalString(value: unknown): string | undefined {
|
|
609
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function signingFromArgs(args: Record<string, unknown>): AgentDeviceSigningConfig {
|
|
613
|
+
const developerDir = optionalString(args.developer_dir);
|
|
614
|
+
if (developerDir && (!isAbsolute(developerDir) || !developerDir.endsWith('/Contents/Developer') || !existsSync(developerDir))) {
|
|
615
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'developer_dir must be an existing absolute Xcode Contents/Developer directory.', { retryable: false });
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
schemaVersion: 1,
|
|
619
|
+
teamId: optionalString(args.team_id),
|
|
620
|
+
bundleId: optionalString(args.runner_bundle_id),
|
|
621
|
+
developerDir,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function isAgentDeviceInteraction(record: InteractionSessionRecord): boolean {
|
|
626
|
+
return interactionAutomationEngine(record) === 'agent-device';
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function readAgentDeviceInteraction(repoRoot: string, interactionId: string): InteractionSessionRecord | undefined {
|
|
630
|
+
for (const provider of PROVIDERS) {
|
|
631
|
+
const record = readInteractionSession(repoRoot, provider, interactionId);
|
|
632
|
+
if (record && isAgentDeviceInteraction(record)) return record;
|
|
633
|
+
}
|
|
634
|
+
return undefined;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function listAgentDeviceInteractions(repoRoot: string): InteractionSessionRecord[] {
|
|
638
|
+
return PROVIDERS.flatMap((provider) => listInteractionSessions(repoRoot, provider))
|
|
639
|
+
.filter(isAgentDeviceInteraction);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function requireRecord(input: AssistantPluginActionExecutionInput, allowTerminal = false): InteractionSessionRecord {
|
|
643
|
+
const interactionId = requireString(input.args.interaction_id, 'interaction_id');
|
|
644
|
+
const record = readAgentDeviceInteraction(input.repoRoot, interactionId);
|
|
645
|
+
if (!record) {
|
|
646
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `Unknown iOS agent-device interaction: ${interactionId}`, { retryable: false });
|
|
647
|
+
}
|
|
648
|
+
if (record.status === 'unknown' && !allowTerminal) {
|
|
649
|
+
throw new AssistantPluginError(
|
|
650
|
+
'AGENT_DEVICE_OUTCOME_UNKNOWN',
|
|
651
|
+
'The previous device mutation has an unknown outcome. Do not retry it; explicitly close or reconcile this interaction first.',
|
|
652
|
+
{
|
|
653
|
+
retryable: false,
|
|
654
|
+
details: { interactionId, status: record.status, sessionRetained: true },
|
|
655
|
+
},
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
if (!isInteractionSessionActive(record.status)) {
|
|
659
|
+
if (allowTerminal) return record;
|
|
660
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `iOS agent-device interaction is ${record.status}.`, {
|
|
661
|
+
retryable: false,
|
|
662
|
+
details: { interactionId, status: record.status },
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
if (hooks.now().getTime() >= Date.parse(record.expiresAt)) {
|
|
666
|
+
const closed = bestEffortClose(input, record);
|
|
667
|
+
patchInteractionSession(input.repoRoot, record.provider, interactionId, closed
|
|
668
|
+
? {
|
|
669
|
+
status: 'failed',
|
|
670
|
+
error: { code: 'AGENT_DEVICE_SESSION_EXPIRED', message: 'The agent-device session expired and was closed.' },
|
|
671
|
+
}
|
|
672
|
+
: {
|
|
673
|
+
status: 'closing',
|
|
674
|
+
error: { code: 'AGENT_DEVICE_CLEANUP_FAILED', message: 'The expired agent-device session could not be closed; ownership remains fenced.' },
|
|
675
|
+
});
|
|
676
|
+
throw new AssistantPluginError(
|
|
677
|
+
closed ? 'AGENT_DEVICE_SESSION_EXPIRED' : 'AGENT_DEVICE_CLEANUP_FAILED',
|
|
678
|
+
closed ? 'The agent-device session expired and was closed.' : 'The expired agent-device session could not be closed; retry agent_device_close.',
|
|
679
|
+
{ retryable: !closed },
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
return record;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function providerErrorCode(result: CommandResult): string | undefined {
|
|
686
|
+
try {
|
|
687
|
+
const parsed = JSON.parse(result.stdout.trim()) as { error?: { code?: unknown } };
|
|
688
|
+
return typeof parsed.error?.code === 'string' ? parsed.error.code : undefined;
|
|
689
|
+
} catch {
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function providerSessionAlreadyAbsent(result: CommandResult): boolean {
|
|
695
|
+
return providerErrorCode(result) === 'SESSION_NOT_FOUND';
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function bestEffortClose(input: AssistantPluginActionExecutionInput, record: InteractionSessionRecord): boolean {
|
|
699
|
+
const result = hooks.runCommand(executable(), ['close', '--session', record.sessionId, '--platform', 'ios', '--json'], {
|
|
700
|
+
cwd: input.repoRoot,
|
|
701
|
+
timeoutMs: 30_000,
|
|
702
|
+
env: sessionEnv(input, record),
|
|
703
|
+
});
|
|
704
|
+
if (providerSessionAlreadyAbsent(result)) return true;
|
|
705
|
+
if (!result.ok) return false;
|
|
706
|
+
try {
|
|
707
|
+
const parsed = JSON.parse(result.stdout.trim()) as { success?: unknown };
|
|
708
|
+
return parsed.success !== false;
|
|
709
|
+
} catch {
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function closeProviderSession(
|
|
715
|
+
input: AssistantPluginActionExecutionInput,
|
|
716
|
+
record: InteractionSessionRecord,
|
|
717
|
+
args: string[],
|
|
718
|
+
): Promise<{ result: Record<string, unknown>; providerAlreadyAbsent: boolean }> {
|
|
719
|
+
const commandResult = await hooks.runCommandAsync(executable(), [
|
|
720
|
+
...args,
|
|
721
|
+
'--session', record.sessionId,
|
|
722
|
+
'--platform', 'ios',
|
|
723
|
+
'--json',
|
|
724
|
+
], {
|
|
725
|
+
cwd: input.repoRoot,
|
|
726
|
+
timeoutMs: effectiveCommandTimeout(input, 60_000),
|
|
727
|
+
env: sessionEnv(input, record),
|
|
728
|
+
signal: input.signal,
|
|
729
|
+
});
|
|
730
|
+
if (providerSessionAlreadyAbsent(commandResult)) {
|
|
731
|
+
return {
|
|
732
|
+
providerAlreadyAbsent: true,
|
|
733
|
+
result: { success: true, data: { alreadyClosed: true, providerCode: 'SESSION_NOT_FOUND' } },
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
providerAlreadyAbsent: false,
|
|
738
|
+
result: parseJsonResult(commandResult, 'AGENT_DEVICE_CLOSE_FAILED'),
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function reconcileExpiredSessions(input: AssistantPluginActionExecutionInput): void {
|
|
743
|
+
const nowMs = hooks.now().getTime();
|
|
744
|
+
for (const record of listAgentDeviceInteractions(input.repoRoot)) {
|
|
745
|
+
if (!isInteractionSessionActive(record.status) || nowMs < Date.parse(record.expiresAt)) continue;
|
|
746
|
+
const closed = bestEffortClose(input, record);
|
|
747
|
+
patchInteractionSession(input.repoRoot, record.provider, record.interactionId, closed
|
|
748
|
+
? {
|
|
749
|
+
status: 'failed',
|
|
750
|
+
error: { code: 'AGENT_DEVICE_SESSION_EXPIRED', message: 'The agent-device session expired and was closed before opening another session.' },
|
|
751
|
+
}
|
|
752
|
+
: {
|
|
753
|
+
status: 'closing',
|
|
754
|
+
error: { code: 'AGENT_DEVICE_CLEANUP_FAILED', message: 'The expired agent-device session could not be closed; ownership remains fenced.' },
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function failSession(input: AssistantPluginActionExecutionInput, record: InteractionSessionRecord, error: unknown): never {
|
|
760
|
+
const closed = bestEffortClose(input, record);
|
|
761
|
+
const normalized = toAssistantPluginError(error, {
|
|
762
|
+
code: 'AGENT_DEVICE_COMMAND_FAILED',
|
|
763
|
+
message: 'The agent-device command failed.',
|
|
764
|
+
retryable: false,
|
|
765
|
+
});
|
|
766
|
+
patchInteractionSession(input.repoRoot, record.provider, record.interactionId, closed
|
|
767
|
+
? {
|
|
768
|
+
status: 'failed',
|
|
769
|
+
error: { code: normalized.code, message: normalized.message },
|
|
770
|
+
}
|
|
771
|
+
: {
|
|
772
|
+
status: 'closing',
|
|
773
|
+
error: { code: 'AGENT_DEVICE_CLEANUP_FAILED', message: `${normalized.message}; cleanup failed and ownership remains fenced.` },
|
|
774
|
+
});
|
|
775
|
+
if (!closed) {
|
|
776
|
+
throw new AssistantPluginError('AGENT_DEVICE_CLEANUP_FAILED', 'The agent-device action failed and its session could not be closed; retry agent_device_close.', {
|
|
777
|
+
retryable: true,
|
|
778
|
+
details: { originalCode: normalized.code, interactionId: record.interactionId },
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
throw normalized;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function fenceUnknownSession(
|
|
785
|
+
input: AssistantPluginActionExecutionInput,
|
|
786
|
+
record: InteractionSessionRecord,
|
|
787
|
+
error: unknown,
|
|
788
|
+
): never {
|
|
789
|
+
const normalized = toAssistantPluginError(error, {
|
|
790
|
+
code: 'AGENT_DEVICE_COMMAND_FAILED',
|
|
791
|
+
message: 'The agent-device mutation ended without a trustworthy outcome.',
|
|
792
|
+
retryable: false,
|
|
793
|
+
});
|
|
794
|
+
patchInteractionSession(input.repoRoot, record.provider, record.interactionId, {
|
|
795
|
+
status: 'unknown',
|
|
796
|
+
error: {
|
|
797
|
+
code: 'AGENT_DEVICE_OUTCOME_UNKNOWN',
|
|
798
|
+
message: 'A device mutation may have completed, but the provider did not return trustworthy completion evidence.',
|
|
799
|
+
},
|
|
800
|
+
});
|
|
801
|
+
throw new AssistantPluginError(
|
|
802
|
+
'AGENT_DEVICE_OUTCOME_UNKNOWN',
|
|
803
|
+
'The device mutation may have completed, but its outcome is unknown. Do not retry it; explicitly close or reconcile this interaction first.',
|
|
804
|
+
{
|
|
805
|
+
retryable: false,
|
|
806
|
+
details: {
|
|
807
|
+
originalCode: normalized.code,
|
|
808
|
+
interactionId: record.interactionId,
|
|
809
|
+
outcome: 'unknown',
|
|
810
|
+
sessionRetained: true,
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function isPotentiallyMutatingAgentDeviceCommand(args: string[]): boolean {
|
|
817
|
+
return ['press', 'fill', 'scroll', 'keyboard', 'back', 'batch'].includes(args[0] ?? '');
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function failWorkflowSession(
|
|
821
|
+
input: AssistantPluginActionExecutionInput,
|
|
822
|
+
record: InteractionSessionRecord,
|
|
823
|
+
error: unknown,
|
|
824
|
+
keepSession: boolean,
|
|
825
|
+
): never {
|
|
826
|
+
const classification = classifyAgentDeviceFailure(error);
|
|
827
|
+
if (keepSession && classification.disposition === 'preserve_session') return preserveSessionFailure(error);
|
|
828
|
+
if (classification.disposition === 'fence_unknown') return fenceUnknownSession(input, record, error);
|
|
829
|
+
return failSession(input, record, error);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function interactionCommandKey(record: InteractionSessionRecord): string {
|
|
833
|
+
return `${record.provider}:${record.interactionId}`;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function interactionCancelledError(): AssistantPluginError {
|
|
837
|
+
return new AssistantPluginError('AGENT_DEVICE_COMMAND_CANCELLED', 'The agent-device action was cancelled before it acquired the interaction session.', {
|
|
838
|
+
retryable: true,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async function waitForInteractionTurn(
|
|
843
|
+
previous: Promise<void>,
|
|
844
|
+
signal?: AbortSignal,
|
|
845
|
+
timeoutMs?: number,
|
|
846
|
+
): Promise<void> {
|
|
847
|
+
if (signal?.aborted) throw interactionCancelledError();
|
|
848
|
+
if (timeoutMs !== undefined && timeoutMs <= 0) {
|
|
849
|
+
throw actionTimeoutError('The agent-device action timed out while waiting for the interaction session.');
|
|
850
|
+
}
|
|
851
|
+
if (!signal && timeoutMs === undefined) {
|
|
852
|
+
await previous.catch(() => undefined);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
await new Promise<void>((resolve, reject) => {
|
|
856
|
+
let settled = false;
|
|
857
|
+
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
858
|
+
const finish = (callback: () => void) => {
|
|
859
|
+
if (settled) return;
|
|
860
|
+
settled = true;
|
|
861
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
862
|
+
signal?.removeEventListener('abort', onAbort);
|
|
863
|
+
callback();
|
|
864
|
+
};
|
|
865
|
+
const onAbort = () => finish(() => reject(interactionCancelledError()));
|
|
866
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
867
|
+
if (timeoutMs !== undefined) {
|
|
868
|
+
timeoutHandle = setTimeout(() => finish(() => reject(actionTimeoutError(
|
|
869
|
+
'The agent-device action timed out while waiting for the interaction session.',
|
|
870
|
+
))), Math.max(1, timeoutMs));
|
|
871
|
+
timeoutHandle.unref?.();
|
|
872
|
+
}
|
|
873
|
+
void previous.catch(() => undefined).then(() => finish(resolve));
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async function serializeInteractionCommand<T>(
|
|
878
|
+
input: AssistantPluginActionExecutionInput,
|
|
879
|
+
record: InteractionSessionRecord,
|
|
880
|
+
command: (current: InteractionSessionRecord) => Promise<T>,
|
|
881
|
+
options: { allowTerminal?: boolean } = {},
|
|
882
|
+
): Promise<T> {
|
|
883
|
+
const key = interactionCommandKey(record);
|
|
884
|
+
const previous = interactionCommandTails.get(key) ?? Promise.resolve();
|
|
885
|
+
let release!: () => void;
|
|
886
|
+
const current = new Promise<void>((resolve) => { release = resolve; });
|
|
887
|
+
const tail = previous.catch(() => undefined).then(() => current);
|
|
888
|
+
interactionCommandTails.set(key, tail);
|
|
889
|
+
try {
|
|
890
|
+
await waitForInteractionTurn(previous, input.signal, remainingActionTimeout(input));
|
|
891
|
+
if (input.signal?.aborted) throw interactionCancelledError();
|
|
892
|
+
const current = readAgentDeviceInteraction(input.repoRoot, record.interactionId);
|
|
893
|
+
if (!current || current.sessionId !== record.sessionId) {
|
|
894
|
+
throw new AssistantPluginError('AGENT_DEVICE_SESSION_NOT_ACTIVE', 'The interaction session changed or disappeared while this command was queued.', {
|
|
895
|
+
retryable: false,
|
|
896
|
+
details: { interactionId: record.interactionId, status: current?.status ?? 'missing' },
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
if (current.status === 'unknown' && options.allowTerminal !== true) {
|
|
900
|
+
throw new AssistantPluginError(
|
|
901
|
+
'AGENT_DEVICE_OUTCOME_UNKNOWN',
|
|
902
|
+
'The previous device mutation has an unknown outcome. Do not retry it; explicitly close or reconcile this interaction first.',
|
|
903
|
+
{
|
|
904
|
+
retryable: false,
|
|
905
|
+
details: { interactionId: record.interactionId, status: current.status, sessionRetained: true },
|
|
906
|
+
},
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
if (!isInteractionSessionActive(current.status) && options.allowTerminal !== true) {
|
|
910
|
+
throw new AssistantPluginError('AGENT_DEVICE_SESSION_NOT_ACTIVE', 'The interaction session became terminal while this command was queued.', {
|
|
911
|
+
retryable: false,
|
|
912
|
+
details: { interactionId: record.interactionId, status: current.status },
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
return await command(current);
|
|
916
|
+
} finally {
|
|
917
|
+
release();
|
|
918
|
+
if (interactionCommandTails.get(key) === tail) {
|
|
919
|
+
void tail.finally(() => {
|
|
920
|
+
if (interactionCommandTails.get(key) === tail) interactionCommandTails.delete(key);
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
async function runSessionCommand(
|
|
927
|
+
input: AssistantPluginActionExecutionInput,
|
|
928
|
+
record: InteractionSessionRecord,
|
|
929
|
+
args: string[],
|
|
930
|
+
failureCode: string,
|
|
931
|
+
timeoutMs = 60_000,
|
|
932
|
+
): Promise<Record<string, unknown>> {
|
|
933
|
+
return serializeInteractionCommand(input, record, async () => {
|
|
934
|
+
try {
|
|
935
|
+
return await runJsonAsync(input, [...args, '--session', record.sessionId, '--platform', 'ios', '--json'], {
|
|
936
|
+
record,
|
|
937
|
+
timeoutMs,
|
|
938
|
+
failureCode,
|
|
939
|
+
});
|
|
940
|
+
} catch (error) {
|
|
941
|
+
const classification = classifyAgentDeviceFailure(error);
|
|
942
|
+
if (classification.disposition === 'preserve_session') {
|
|
943
|
+
return preserveSessionFailure(error);
|
|
944
|
+
}
|
|
945
|
+
if (classification.disposition === 'fence_unknown' && isPotentiallyMutatingAgentDeviceCommand(args)) {
|
|
946
|
+
return fenceUnknownSession(input, record, error);
|
|
947
|
+
}
|
|
948
|
+
return failSession(input, record, error);
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
async function runSessionCommandAttempt(
|
|
954
|
+
input: AssistantPluginActionExecutionInput,
|
|
955
|
+
record: InteractionSessionRecord,
|
|
956
|
+
args: string[],
|
|
957
|
+
failureCode: string,
|
|
958
|
+
timeoutMs = 60_000,
|
|
959
|
+
): Promise<Record<string, unknown>> {
|
|
960
|
+
return serializeInteractionCommand(input, record, async () => runJsonAsync(
|
|
961
|
+
input,
|
|
962
|
+
[...args, '--session', record.sessionId, '--platform', 'ios', '--json'],
|
|
963
|
+
{ record, timeoutMs, failureCode },
|
|
964
|
+
));
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function boundedProviderTimeout(
|
|
968
|
+
input: AssistantPluginActionExecutionInput,
|
|
969
|
+
requested: number,
|
|
970
|
+
): number {
|
|
971
|
+
const remaining = remainingActionTimeout(input);
|
|
972
|
+
if (remaining !== undefined && remaining <= 0) throw actionTimeoutError();
|
|
973
|
+
return Math.max(1, Math.min(requested, remaining ?? requested));
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function executeSessionSnapshotBackend(
|
|
977
|
+
input: AssistantPluginActionExecutionInput,
|
|
978
|
+
record: InteractionSessionRecord,
|
|
979
|
+
request: AgentDeviceSnapshotRequest,
|
|
980
|
+
failureCode: string,
|
|
981
|
+
timeoutMs: number,
|
|
982
|
+
): Promise<Record<string, unknown>> {
|
|
983
|
+
const mode = configuredAgentDeviceBackendMode();
|
|
984
|
+
let backendFallbackReason: 'typed_unavailable' | 'typed_cli_version_mismatch' | undefined;
|
|
985
|
+
let fallbackTypedVersion: string | undefined;
|
|
986
|
+
let fallbackCliVersion: string | undefined;
|
|
987
|
+
if (mode !== 'cli') {
|
|
988
|
+
const dependency = requireDependency();
|
|
989
|
+
const typed = new TypedAgentDeviceReadProvider();
|
|
990
|
+
const versionsMatch = agentDeviceProviderVersionsMatch(
|
|
991
|
+
typed.identity.version,
|
|
992
|
+
dependency.detectedVersion,
|
|
993
|
+
);
|
|
994
|
+
if (typed.identity.available && !versionsMatch) {
|
|
995
|
+
fallbackTypedVersion = typed.identity.version;
|
|
996
|
+
fallbackCliVersion = dependency.detectedVersion;
|
|
997
|
+
if (mode === 'typed') {
|
|
998
|
+
throw new AssistantPluginError(
|
|
999
|
+
'AGENT_DEVICE_TYPED_PROVIDER_VERSION_MISMATCH',
|
|
1000
|
+
`The typed agent-device module (${typed.identity.version ?? 'unknown'}) does not match the active CLI (${dependency.detectedVersion ?? 'unknown'}).`,
|
|
1001
|
+
{
|
|
1002
|
+
retryable: false,
|
|
1003
|
+
details: {
|
|
1004
|
+
providerBackend: 'typed',
|
|
1005
|
+
providerCode: 'UNSUPPORTED_OPERATION',
|
|
1006
|
+
typedVersion: typed.identity.version,
|
|
1007
|
+
cliVersion: dependency.detectedVersion,
|
|
1008
|
+
},
|
|
1009
|
+
},
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
backendFallbackReason = 'typed_cli_version_mismatch';
|
|
1013
|
+
} else if (typed.identity.available || mode === 'typed') {
|
|
1014
|
+
try {
|
|
1015
|
+
return await typed.snapshot({
|
|
1016
|
+
stateDir: stateDir(input, record.interactionId),
|
|
1017
|
+
session: record.sessionId,
|
|
1018
|
+
device: record.targetId,
|
|
1019
|
+
platform: 'ios',
|
|
1020
|
+
requestId: input.requestId,
|
|
1021
|
+
cwd: input.repoRoot,
|
|
1022
|
+
timeoutMs: boundedProviderTimeout(input, timeoutMs),
|
|
1023
|
+
}, request) as unknown as Record<string, unknown>;
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
if (mode === 'auto' && isTypedProviderUnavailable(error)) {
|
|
1026
|
+
// The optional package was removed or became unloadable after the
|
|
1027
|
+
// synchronous status probe. CLI remains the compatibility fallback.
|
|
1028
|
+
backendFallbackReason = 'typed_unavailable';
|
|
1029
|
+
fallbackTypedVersion = typed.identity.version;
|
|
1030
|
+
fallbackCliVersion = dependency.detectedVersion;
|
|
1031
|
+
} else if (isTypedProviderUnavailable(error)) {
|
|
1032
|
+
throw new AssistantPluginError(
|
|
1033
|
+
'AGENT_DEVICE_TYPED_PROVIDER_UNAVAILABLE',
|
|
1034
|
+
error instanceof Error ? error.message : 'The typed agent-device provider is unavailable.',
|
|
1035
|
+
{
|
|
1036
|
+
retryable: false,
|
|
1037
|
+
details: {
|
|
1038
|
+
providerBackend: 'typed',
|
|
1039
|
+
sessionPreserved: true,
|
|
1040
|
+
identity: typed.identity,
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
);
|
|
1044
|
+
} else {
|
|
1045
|
+
throw error;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
} else if (mode === 'auto') {
|
|
1049
|
+
backendFallbackReason = 'typed_unavailable';
|
|
1050
|
+
fallbackTypedVersion = typed.identity.version;
|
|
1051
|
+
fallbackCliVersion = dependency.detectedVersion;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const args = compileSnapshotCommand(capabilityProfile(), request);
|
|
1056
|
+
const result = await runJsonAsync(
|
|
1057
|
+
input,
|
|
1058
|
+
[...args, '--session', record.sessionId, '--platform', 'ios', '--json'],
|
|
1059
|
+
{ record, timeoutMs, failureCode },
|
|
1060
|
+
);
|
|
1061
|
+
return {
|
|
1062
|
+
...result,
|
|
1063
|
+
provider: 'cli',
|
|
1064
|
+
...(backendFallbackReason ? {
|
|
1065
|
+
backendFallbackReason,
|
|
1066
|
+
typedVersion: fallbackTypedVersion,
|
|
1067
|
+
cliVersion: fallbackCliVersion,
|
|
1068
|
+
} : {}),
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
async function runSessionSnapshotAttempt(
|
|
1073
|
+
input: AssistantPluginActionExecutionInput,
|
|
1074
|
+
record: InteractionSessionRecord,
|
|
1075
|
+
request: AgentDeviceSnapshotRequest,
|
|
1076
|
+
failureCode = 'AGENT_DEVICE_SNAPSHOT_FAILED',
|
|
1077
|
+
timeoutMs = 60_000,
|
|
1078
|
+
): Promise<Record<string, unknown>> {
|
|
1079
|
+
return serializeInteractionCommand(
|
|
1080
|
+
input,
|
|
1081
|
+
record,
|
|
1082
|
+
async () => executeSessionSnapshotBackend(input, record, request, failureCode, timeoutMs),
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
async function runSessionSnapshot(
|
|
1087
|
+
input: AssistantPluginActionExecutionInput,
|
|
1088
|
+
record: InteractionSessionRecord,
|
|
1089
|
+
request: AgentDeviceSnapshotRequest,
|
|
1090
|
+
failureCode = 'AGENT_DEVICE_SNAPSHOT_FAILED',
|
|
1091
|
+
timeoutMs = 60_000,
|
|
1092
|
+
): Promise<Record<string, unknown>> {
|
|
1093
|
+
try {
|
|
1094
|
+
return await runSessionSnapshotAttempt(input, record, request, failureCode, timeoutMs);
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
if (isTypedProviderUnavailable(error)) throw error;
|
|
1097
|
+
const classification = classifyAgentDeviceFailure(error);
|
|
1098
|
+
// Snapshot is observation-only. A timeout/cancel cannot create an unknown
|
|
1099
|
+
// device-side mutation, so retain the session unless the provider supplied
|
|
1100
|
+
// concrete Runner/transport-death evidence.
|
|
1101
|
+
if (classification.disposition !== 'terminate_session') return preserveSessionFailure(error);
|
|
1102
|
+
return failSession(input, record, error);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function isStaleAccessibilityRefError(error: unknown): boolean {
|
|
1107
|
+
const normalized = toAssistantPluginError(error, {
|
|
1108
|
+
code: 'AGENT_DEVICE_COMMAND_FAILED',
|
|
1109
|
+
message: 'The agent-device command failed.',
|
|
1110
|
+
retryable: false,
|
|
1111
|
+
});
|
|
1112
|
+
const evidence = `${normalized.message}\n${JSON.stringify(normalized.details ?? {})}`;
|
|
1113
|
+
return /(?:accessibility|element|ref|@e\d+)/i.test(evidence)
|
|
1114
|
+
&& /(?:stale|expired|missing|not[\s_-]*found|no[\s_-]*such)/i.test(evidence);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function isExactEvidenceWaitMiss(error: unknown): boolean {
|
|
1118
|
+
const normalized = toAssistantPluginError(error, {
|
|
1119
|
+
code: 'AGENT_DEVICE_COMMAND_FAILED',
|
|
1120
|
+
message: 'The agent-device command failed.',
|
|
1121
|
+
retryable: false,
|
|
1122
|
+
});
|
|
1123
|
+
const evidence = `${normalized.message}\n${JSON.stringify(normalized.details ?? {})}`;
|
|
1124
|
+
const waitFailure = /(?:wait|expected\s+(?:text|selector)|text\s+match|selector\s+match)/i.test(evidence)
|
|
1125
|
+
&& /(?:timed?\s*out|timeout|not[\s_-]*found|no\s+match|missing)/i.test(evidence);
|
|
1126
|
+
const infrastructureFailure = /(?:runner|transport|connection|socket|daemon|xctest|device\s+disconnect|spawn|broken\s+pipe)/i.test(evidence);
|
|
1127
|
+
return waitFailure && !infrastructureFailure;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function hasAccessibilityEvidence(value: unknown): boolean {
|
|
1131
|
+
return stringEvidence(value).some((text) => /(?:@e\d+|\b(?:StaticText|SearchField|TextField|Button|Cell|Image|Switch|Link)\b|(?:label|identifier)=)/i.test(text));
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function stringEvidence(value: unknown, output: string[] = []): string[] {
|
|
1135
|
+
if (typeof value === 'string') output.push(value);
|
|
1136
|
+
else if (Array.isArray(value)) value.forEach((entry) => stringEvidence(entry, output));
|
|
1137
|
+
else if (value && typeof value === 'object') {
|
|
1138
|
+
Object.values(value as Record<string, unknown>).forEach((entry) => stringEvidence(entry, output));
|
|
1139
|
+
}
|
|
1140
|
+
return output;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function interactiveRef(value: unknown, terms: RegExp): string | undefined {
|
|
1144
|
+
for (const text of stringEvidence(value)) {
|
|
1145
|
+
for (const line of text.split('\n')) {
|
|
1146
|
+
if (!terms.test(line)) continue;
|
|
1147
|
+
const match = line.match(/@e\d+(?:~s\d+)?/);
|
|
1148
|
+
if (match) return match[0];
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return undefined;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function boundedVisibleText(value: unknown, query: string): string[] {
|
|
1155
|
+
const seen = new Set<string>();
|
|
1156
|
+
const lines: string[] = [];
|
|
1157
|
+
for (const text of stringEvidence(value)) {
|
|
1158
|
+
for (const rawLine of text.split('\n')) {
|
|
1159
|
+
const line = rawLine.trim().split(query).join('<query>');
|
|
1160
|
+
if (!line || line.length > 500 || seen.has(line)) continue;
|
|
1161
|
+
seen.add(line);
|
|
1162
|
+
lines.push(line);
|
|
1163
|
+
if (Buffer.byteLength(lines.join('\n'), 'utf8') >= 8_000) return lines;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return lines;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function validateJdQuery(value: unknown): string {
|
|
1170
|
+
const query = requireString(value, 'query');
|
|
1171
|
+
if (query.length > MAX_JD_QUERY_LENGTH || /[\u0000-\u001f\u007f]/.test(query)) {
|
|
1172
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `query must be at most ${MAX_JD_QUERY_LENGTH} printable characters.`, { retryable: false });
|
|
1173
|
+
}
|
|
1174
|
+
if (SENSITIVE_SEMANTICS.test(query)) {
|
|
1175
|
+
throw new AssistantPluginError('IOS_DEVICE_SENSITIVE_ACTION_BLOCKED', 'JD search accepts product-information queries only; credentials, verification, checkout, purchase and payment semantics are blocked.', { retryable: false });
|
|
1176
|
+
}
|
|
1177
|
+
return query;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function batchInput(value: unknown, index: number): Record<string, unknown> {
|
|
1181
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1182
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `steps[${index}].input must be an object.`, { retryable: false });
|
|
1183
|
+
}
|
|
1184
|
+
return value as Record<string, unknown>;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function assertBatchKeys(
|
|
1188
|
+
input: Record<string, unknown>,
|
|
1189
|
+
allowed: string[],
|
|
1190
|
+
index: number,
|
|
1191
|
+
kind: AgentDeviceBatchKind,
|
|
1192
|
+
): void {
|
|
1193
|
+
const unexpected = Object.keys(input).filter((key) => !allowed.includes(key));
|
|
1194
|
+
if (unexpected.length > 0) {
|
|
1195
|
+
throw new AssistantPluginError(
|
|
1196
|
+
'PLUGIN_ACTION_ARGUMENT_INVALID',
|
|
1197
|
+
`steps[${index}] ${kind} contains unsupported fields: ${unexpected.join(', ')}`,
|
|
1198
|
+
{ retryable: false },
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function batchInteger(value: unknown, fallback: number, min: number, max: number): number {
|
|
1204
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
1205
|
+
? Math.max(min, Math.min(max, Math.trunc(value)))
|
|
1206
|
+
: fallback;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function assertPhysicalBatchText(record: InteractionSessionRecord, values: Array<string | undefined>): void {
|
|
1210
|
+
if (record.provider !== DEVICE_PROVIDER) return;
|
|
1211
|
+
if (values.some((value) => value && SENSITIVE_SEMANTICS.test(value))) {
|
|
1212
|
+
throw new AssistantPluginError(
|
|
1213
|
+
'IOS_DEVICE_SENSITIVE_ACTION_BLOCKED',
|
|
1214
|
+
'Batch steps involving credentials, verification, biometrics, checkout, purchase or payment require human interaction.',
|
|
1215
|
+
{ retryable: false },
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function nativeBatchTarget(target: string): Record<string, unknown> {
|
|
1221
|
+
const ref = target.match(/^@?(e\d+(?:~s\d+)?)$/i)?.[1];
|
|
1222
|
+
return ref
|
|
1223
|
+
? { kind: 'ref', ref }
|
|
1224
|
+
: { kind: 'selector', selector: target };
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function prepareAgentDeviceBatch(
|
|
1228
|
+
rawSteps: unknown,
|
|
1229
|
+
record: InteractionSessionRecord,
|
|
1230
|
+
): PreparedAgentDeviceBatch {
|
|
1231
|
+
if (!Array.isArray(rawSteps) || rawSteps.length === 0 || rawSteps.length > MAX_BATCH_STEPS) {
|
|
1232
|
+
throw new AssistantPluginError(
|
|
1233
|
+
'PLUGIN_ACTION_ARGUMENT_INVALID',
|
|
1234
|
+
`steps must contain between 1 and ${MAX_BATCH_STEPS} typed entries.`,
|
|
1235
|
+
{ retryable: false },
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const nativeSteps: PreparedAgentDeviceBatch['nativeSteps'] = [];
|
|
1240
|
+
const redactions: string[] = [];
|
|
1241
|
+
rawSteps.forEach((rawStep, index) => {
|
|
1242
|
+
if (!rawStep || typeof rawStep !== 'object' || Array.isArray(rawStep)) {
|
|
1243
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `steps[${index}] must be an object.`, { retryable: false });
|
|
1244
|
+
}
|
|
1245
|
+
const step = rawStep as Record<string, unknown>;
|
|
1246
|
+
const rawKind = requireString(step.kind, `steps[${index}].kind`);
|
|
1247
|
+
if (!BATCH_KINDS.includes(rawKind as AgentDeviceBatchKind)) {
|
|
1248
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `Unsupported batch step kind: ${rawKind}`, { retryable: false });
|
|
1249
|
+
}
|
|
1250
|
+
const kind = rawKind as AgentDeviceBatchKind;
|
|
1251
|
+
const input = batchInput(step.input ?? {}, index);
|
|
1252
|
+
let nativeInput: Record<string, unknown>;
|
|
1253
|
+
|
|
1254
|
+
switch (kind) {
|
|
1255
|
+
case 'snapshot': {
|
|
1256
|
+
assertBatchKeys(input, ['interactive', 'raw', 'depth', 'scope', 'diff', 'force_full', 'timeout_ms'], index, kind);
|
|
1257
|
+
const scope = optionalString(input.scope);
|
|
1258
|
+
nativeInput = {
|
|
1259
|
+
...(input.interactive === true ? { interactive: true } : {}),
|
|
1260
|
+
...(input.raw === true ? { raw: true } : {}),
|
|
1261
|
+
...(input.diff === true ? { diff: true } : {}),
|
|
1262
|
+
...(input.force_full === true ? { forceFull: true } : {}),
|
|
1263
|
+
...(scope ? { scope } : {}),
|
|
1264
|
+
...(typeof input.depth === 'number'
|
|
1265
|
+
? { depth: batchInteger(input.depth, 8, 1, 20) }
|
|
1266
|
+
: {}),
|
|
1267
|
+
...(typeof input.timeout_ms === 'number'
|
|
1268
|
+
? { timeoutMs: batchInteger(input.timeout_ms, 15_000, 100, 60_000) }
|
|
1269
|
+
: {}),
|
|
1270
|
+
};
|
|
1271
|
+
break;
|
|
1272
|
+
}
|
|
1273
|
+
case 'press': {
|
|
1274
|
+
assertBatchKeys(input, ['target', 'x', 'y'], index, kind);
|
|
1275
|
+
const target = optionalString(input.target);
|
|
1276
|
+
const hasPoint = typeof input.x === 'number' && typeof input.y === 'number';
|
|
1277
|
+
if (!target && !hasPoint) {
|
|
1278
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `steps[${index}] press requires target or x/y.`, { retryable: false });
|
|
1279
|
+
}
|
|
1280
|
+
assertPhysicalBatchText(record, [target]);
|
|
1281
|
+
nativeInput = target
|
|
1282
|
+
? { target: nativeBatchTarget(target), settle: true }
|
|
1283
|
+
: {
|
|
1284
|
+
target: { kind: 'point', x: Number(input.x), y: Number(input.y) },
|
|
1285
|
+
settle: true,
|
|
1286
|
+
};
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1289
|
+
case 'fill': {
|
|
1290
|
+
assertBatchKeys(input, ['target', 'text', 'delay_ms'], index, kind);
|
|
1291
|
+
const target = requireString(input.target, `steps[${index}].input.target`);
|
|
1292
|
+
if (typeof input.text !== 'string') {
|
|
1293
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `steps[${index}] fill requires text.`, { retryable: false });
|
|
1294
|
+
}
|
|
1295
|
+
const text = input.text;
|
|
1296
|
+
assertPhysicalBatchText(record, [target, text]);
|
|
1297
|
+
redactions.push(text);
|
|
1298
|
+
nativeInput = {
|
|
1299
|
+
target: nativeBatchTarget(target),
|
|
1300
|
+
text,
|
|
1301
|
+
settle: true,
|
|
1302
|
+
...(typeof input.delay_ms === 'number'
|
|
1303
|
+
? { delayMs: batchInteger(input.delay_ms, 0, 0, 5_000) }
|
|
1304
|
+
: {}),
|
|
1305
|
+
};
|
|
1306
|
+
break;
|
|
1307
|
+
}
|
|
1308
|
+
case 'scroll': {
|
|
1309
|
+
assertBatchKeys(input, ['direction', 'amount'], index, kind);
|
|
1310
|
+
const direction = requireString(input.direction, `steps[${index}].input.direction`);
|
|
1311
|
+
if (!['up', 'down', 'left', 'right', 'top', 'bottom'].includes(direction)) {
|
|
1312
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `Unsupported batch scroll direction: ${direction}`, { retryable: false });
|
|
1313
|
+
}
|
|
1314
|
+
nativeInput = {
|
|
1315
|
+
direction,
|
|
1316
|
+
...(typeof input.amount === 'number'
|
|
1317
|
+
? { amount: batchInteger(input.amount, 1, 1, 100) }
|
|
1318
|
+
: {}),
|
|
1319
|
+
};
|
|
1320
|
+
break;
|
|
1321
|
+
}
|
|
1322
|
+
case 'keyboard': {
|
|
1323
|
+
assertBatchKeys(input, ['action'], index, kind);
|
|
1324
|
+
const action = requireString(input.action, `steps[${index}].input.action`);
|
|
1325
|
+
if (!['status', 'dismiss'].includes(action)) {
|
|
1326
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `Unsupported keyboard action: ${action}`, { retryable: false });
|
|
1327
|
+
}
|
|
1328
|
+
nativeInput = { action };
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
case 'wait': {
|
|
1332
|
+
assertBatchKeys(input, ['wait_type', 'text', 'selector', 'duration_ms', 'quiet_ms', 'timeout_ms'], index, kind);
|
|
1333
|
+
const waitType = optionalString(input.wait_type) ?? 'stable';
|
|
1334
|
+
if (!['stable', 'text', 'selector', 'duration'].includes(waitType)) {
|
|
1335
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `Unsupported wait type: ${waitType}`, { retryable: false });
|
|
1336
|
+
}
|
|
1337
|
+
const timeoutMs = batchInteger(input.timeout_ms, 15_000, 100, 60_000);
|
|
1338
|
+
if (waitType === 'text') {
|
|
1339
|
+
const text = requireString(input.text, `steps[${index}].input.text`);
|
|
1340
|
+
assertPhysicalBatchText(record, [text]);
|
|
1341
|
+
nativeInput = { kind: 'text', text, timeoutMs };
|
|
1342
|
+
} else if (waitType === 'selector') {
|
|
1343
|
+
const selector = requireString(input.selector, `steps[${index}].input.selector`);
|
|
1344
|
+
assertPhysicalBatchText(record, [selector]);
|
|
1345
|
+
nativeInput = { kind: 'selector', selector, timeoutMs };
|
|
1346
|
+
} else if (waitType === 'duration') {
|
|
1347
|
+
nativeInput = {
|
|
1348
|
+
kind: 'duration',
|
|
1349
|
+
durationMs: batchInteger(input.duration_ms, 500, 0, 60_000),
|
|
1350
|
+
};
|
|
1351
|
+
} else {
|
|
1352
|
+
nativeInput = {
|
|
1353
|
+
kind: 'stable',
|
|
1354
|
+
quietMs: batchInteger(input.quiet_ms, 500, 100, 5_000),
|
|
1355
|
+
timeoutMs,
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
break;
|
|
1359
|
+
}
|
|
1360
|
+
case 'back': {
|
|
1361
|
+
assertBatchKeys(input, [], index, kind);
|
|
1362
|
+
nativeInput = {};
|
|
1363
|
+
break;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
nativeSteps.push({ command: kind, input: nativeInput });
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
return { nativeSteps, redactions };
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
async function runSessionBatch(
|
|
1373
|
+
input: AssistantPluginActionExecutionInput,
|
|
1374
|
+
record: InteractionSessionRecord,
|
|
1375
|
+
prepared: PreparedAgentDeviceBatch,
|
|
1376
|
+
timeoutMs = 120_000,
|
|
1377
|
+
): Promise<Record<string, unknown>> {
|
|
1378
|
+
let result: unknown = await runSessionCommand(
|
|
1379
|
+
input,
|
|
1380
|
+
record,
|
|
1381
|
+
compileBatchCommand(capabilityProfile(), prepared.nativeSteps, MAX_BATCH_STEPS, { includeCost: true }),
|
|
1382
|
+
'AGENT_DEVICE_BATCH_FAILED',
|
|
1383
|
+
timeoutMs,
|
|
1384
|
+
);
|
|
1385
|
+
for (const text of prepared.redactions) result = redactExactText(result, text);
|
|
1386
|
+
return result as Record<string, unknown>;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
async function runSessionBatchAttempt(
|
|
1390
|
+
input: AssistantPluginActionExecutionInput,
|
|
1391
|
+
record: InteractionSessionRecord,
|
|
1392
|
+
prepared: PreparedAgentDeviceBatch,
|
|
1393
|
+
timeoutMs = 120_000,
|
|
1394
|
+
): Promise<Record<string, unknown>> {
|
|
1395
|
+
let result: unknown = await runSessionCommandAttempt(
|
|
1396
|
+
input,
|
|
1397
|
+
record,
|
|
1398
|
+
compileBatchCommand(capabilityProfile(), prepared.nativeSteps, MAX_BATCH_STEPS, { includeCost: true }),
|
|
1399
|
+
'AGENT_DEVICE_BATCH_FAILED',
|
|
1400
|
+
timeoutMs,
|
|
1401
|
+
);
|
|
1402
|
+
for (const text of prepared.redactions) result = redactExactText(result, text);
|
|
1403
|
+
return result as Record<string, unknown>;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
async function recoverExactWaitEvidence(
|
|
1407
|
+
input: AssistantPluginActionExecutionInput,
|
|
1408
|
+
record: InteractionSessionRecord,
|
|
1409
|
+
scope: string | undefined,
|
|
1410
|
+
depth: number,
|
|
1411
|
+
keepSession: boolean,
|
|
1412
|
+
): Promise<{
|
|
1413
|
+
snapshot: Record<string, unknown>;
|
|
1414
|
+
tier: 'scoped_snapshot' | 'full_snapshot';
|
|
1415
|
+
snapshotRequests: number;
|
|
1416
|
+
}> {
|
|
1417
|
+
let snapshotRequests = 0;
|
|
1418
|
+
if (scope) {
|
|
1419
|
+
snapshotRequests += 1;
|
|
1420
|
+
try {
|
|
1421
|
+
const scoped = await runSessionSnapshotAttempt(
|
|
1422
|
+
input,
|
|
1423
|
+
record,
|
|
1424
|
+
{
|
|
1425
|
+
interactiveOnly: true,
|
|
1426
|
+
depth,
|
|
1427
|
+
scope,
|
|
1428
|
+
},
|
|
1429
|
+
'AGENT_DEVICE_SNAPSHOT_FAILED',
|
|
1430
|
+
30_000,
|
|
1431
|
+
);
|
|
1432
|
+
if (hasAccessibilityEvidence(scoped)) {
|
|
1433
|
+
return { snapshot: scoped, tier: 'scoped_snapshot', snapshotRequests };
|
|
1434
|
+
}
|
|
1435
|
+
} catch (error) {
|
|
1436
|
+
if (!isExactEvidenceWaitMiss(error)) {
|
|
1437
|
+
return failWorkflowSession(input, record, error, keepSession);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
snapshotRequests += 1;
|
|
1443
|
+
try {
|
|
1444
|
+
const full = await runSessionSnapshotAttempt(
|
|
1445
|
+
input,
|
|
1446
|
+
record,
|
|
1447
|
+
{
|
|
1448
|
+
interactiveOnly: true,
|
|
1449
|
+
depth,
|
|
1450
|
+
forceFull: true,
|
|
1451
|
+
},
|
|
1452
|
+
'AGENT_DEVICE_SNAPSHOT_FAILED',
|
|
1453
|
+
45_000,
|
|
1454
|
+
);
|
|
1455
|
+
return { snapshot: full, tier: 'full_snapshot', snapshotRequests };
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
return failWorkflowSession(input, record, error, keepSession);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function subActionInput(
|
|
1462
|
+
input: AssistantPluginActionExecutionInput,
|
|
1463
|
+
actionId: string,
|
|
1464
|
+
args: Record<string, unknown>,
|
|
1465
|
+
): AssistantPluginActionExecutionInput {
|
|
1466
|
+
return { ...input, actionId, args, requestId: `${input.requestId}:${actionId}` };
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
interface JdSearchTargets {
|
|
1470
|
+
editableTarget?: string;
|
|
1471
|
+
entryTarget?: string;
|
|
1472
|
+
submitTarget?: string;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function discoverJdSearchTargets(snapshot: unknown, preferredTarget?: string): JdSearchTargets {
|
|
1476
|
+
const adapter = JD_IOS_APP_ADAPTER.search!;
|
|
1477
|
+
const preferredRef = normalizedSemanticRef(preferredTarget);
|
|
1478
|
+
const preferredNode = preferredRef
|
|
1479
|
+
? findSemanticNode(snapshot, (node) => normalizedSemanticRef(node.ref) === preferredRef)
|
|
1480
|
+
: undefined;
|
|
1481
|
+
const editableTarget = preferredNode && adapter.isEditableSearchField(preferredNode)
|
|
1482
|
+
? preferredRef
|
|
1483
|
+
: findSemanticRef(snapshot, adapter.isEditableSearchField);
|
|
1484
|
+
const entryTarget = preferredNode && adapter.isSearchEntry(preferredNode)
|
|
1485
|
+
? preferredRef
|
|
1486
|
+
: findSemanticRef(snapshot, adapter.isSearchEntry);
|
|
1487
|
+
return {
|
|
1488
|
+
editableTarget,
|
|
1489
|
+
entryTarget,
|
|
1490
|
+
submitTarget: findSemanticRef(snapshot, adapter.isSubmit),
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
async function executeJdSearch(input: AssistantPluginActionExecutionInput): Promise<Record<string, unknown>> {
|
|
1495
|
+
const workflowStartedAt = performance.now();
|
|
1496
|
+
const timingsMs = {
|
|
1497
|
+
targetSelection: 0,
|
|
1498
|
+
open: 0,
|
|
1499
|
+
targetDiscovery: 0,
|
|
1500
|
+
navigation: 0,
|
|
1501
|
+
interactionAndEvidence: 0,
|
|
1502
|
+
screenshot: 0,
|
|
1503
|
+
close: 0,
|
|
1504
|
+
total: 0,
|
|
1505
|
+
};
|
|
1506
|
+
const targetSelectionStartedAt = performance.now();
|
|
1507
|
+
const query = validateJdQuery(input.args.query);
|
|
1508
|
+
const deviceSelector = requireString(input.args.device, 'device');
|
|
1509
|
+
const requestedInteractionId = optionalString(input.args.interaction_id);
|
|
1510
|
+
const keepSession = typeof input.args.keep_session === 'boolean'
|
|
1511
|
+
? input.args.keep_session
|
|
1512
|
+
: Boolean(requestedInteractionId);
|
|
1513
|
+
const captureScreenshot = typeof input.args.capture_screenshot === 'boolean'
|
|
1514
|
+
? input.args.capture_screenshot
|
|
1515
|
+
: !requestedInteractionId;
|
|
1516
|
+
let selected: AgentDeviceEntry;
|
|
1517
|
+
let interaction: InteractionSessionRecord | undefined;
|
|
1518
|
+
let sessionReused = false;
|
|
1519
|
+
let deviceInventoryRequests = 0;
|
|
1520
|
+
|
|
1521
|
+
if (requestedInteractionId) {
|
|
1522
|
+
interaction = requireRecord(subActionInput(input, 'agent_device_jd_search', {
|
|
1523
|
+
interaction_id: requestedInteractionId,
|
|
1524
|
+
}));
|
|
1525
|
+
if (interaction.provider !== DEVICE_PROVIDER || interaction.instructions !== JD_BUNDLE_ID) {
|
|
1526
|
+
throw new AssistantPluginError(
|
|
1527
|
+
'PLUGIN_ACTION_ARGUMENT_INVALID',
|
|
1528
|
+
'interaction_id must reference an active physical-iPhone JD session opened for com.360buy.jdmobile.',
|
|
1529
|
+
{
|
|
1530
|
+
retryable: false,
|
|
1531
|
+
details: {
|
|
1532
|
+
interactionId: interaction.interactionId,
|
|
1533
|
+
provider: interaction.provider,
|
|
1534
|
+
app: interaction.instructions,
|
|
1535
|
+
},
|
|
1536
|
+
},
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
selected = {
|
|
1540
|
+
platform: 'ios',
|
|
1541
|
+
appleOs: 'ios',
|
|
1542
|
+
id: interaction.targetId,
|
|
1543
|
+
name: deviceSelector,
|
|
1544
|
+
kind: 'device',
|
|
1545
|
+
target: 'mobile',
|
|
1546
|
+
booted: true,
|
|
1547
|
+
aliases: interactionTargetIdentifiers(interaction),
|
|
1548
|
+
};
|
|
1549
|
+
sessionReused = true;
|
|
1550
|
+
} else {
|
|
1551
|
+
deviceInventoryRequests += 1;
|
|
1552
|
+
selected = selectTarget(input, deviceSelector);
|
|
1553
|
+
if (selected.kind !== 'device') {
|
|
1554
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'agent_device_jd_search requires one exact connected physical iPhone.', {
|
|
1555
|
+
retryable: false,
|
|
1556
|
+
details: { device: { id: selected.id, name: selected.name, kind: selected.kind } },
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
timingsMs.targetSelection = performance.now() - targetSelectionStartedAt;
|
|
1561
|
+
|
|
1562
|
+
if (!interaction) {
|
|
1563
|
+
const sharedArgs = {
|
|
1564
|
+
device: selected.name,
|
|
1565
|
+
team_id: optionalString(input.args.team_id),
|
|
1566
|
+
runner_bundle_id: optionalString(input.args.runner_bundle_id),
|
|
1567
|
+
developer_dir: optionalString(input.args.developer_dir),
|
|
1568
|
+
};
|
|
1569
|
+
const openStartedAt = performance.now();
|
|
1570
|
+
const opened = await executeIosAgentDeviceAction(subActionInput(input, 'agent_device_open', {
|
|
1571
|
+
...sharedArgs,
|
|
1572
|
+
app: JD_BUNDLE_ID,
|
|
1573
|
+
// Foreground an already-running app by default. Relaunch is opt-in because
|
|
1574
|
+
// it restarts app state and pays another cold-start/navigation cost.
|
|
1575
|
+
relaunch: input.args.relaunch === true,
|
|
1576
|
+
}));
|
|
1577
|
+
interaction = opened.interaction as InteractionSessionRecord | undefined;
|
|
1578
|
+
timingsMs.open = performance.now() - openStartedAt;
|
|
1579
|
+
}
|
|
1580
|
+
if (!interaction) {
|
|
1581
|
+
throw new AssistantPluginError('AGENT_DEVICE_OPEN_FAILED', 'agent-device did not return an interaction for JD.', { retryable: false });
|
|
1582
|
+
}
|
|
1583
|
+
const interactionId = interaction.interactionId;
|
|
1584
|
+
const record = requireRecord(subActionInput(input, 'agent_device_batch', { interaction_id: interactionId }));
|
|
1585
|
+
let finalSnapshot: Record<string, unknown> | undefined;
|
|
1586
|
+
let screenshot: Record<string, unknown> | undefined;
|
|
1587
|
+
const resultText = optionalString(input.args.result_text);
|
|
1588
|
+
const resultSelector = optionalString(input.args.result_selector);
|
|
1589
|
+
const exactResultWait = Boolean(resultText || resultSelector);
|
|
1590
|
+
const resultScope = optionalString(input.args.result_scope);
|
|
1591
|
+
const snapshotDepth = batchInteger(input.args.snapshot_depth, 20, 1, 20);
|
|
1592
|
+
let accessibilityEvidenceTier: 'exact_wait' | 'scoped_snapshot' | 'full_snapshot' = exactResultWait
|
|
1593
|
+
? 'exact_wait'
|
|
1594
|
+
: resultScope
|
|
1595
|
+
? 'scoped_snapshot'
|
|
1596
|
+
: 'full_snapshot';
|
|
1597
|
+
let initialAccessibilitySnapshot = false;
|
|
1598
|
+
let staleRefRecovery = false;
|
|
1599
|
+
let exactWaitFallback = false;
|
|
1600
|
+
let accessibilitySnapshotRequests = 0;
|
|
1601
|
+
let navigationSnapshotRequests = 0;
|
|
1602
|
+
let searchFlow: 'explicit_selector' | 'explicit_target' | 'direct_editable' | 'navigated_from_entry' = 'direct_editable';
|
|
1603
|
+
let nativeBatchRequests = 0;
|
|
1604
|
+
let nativeBatchSteps = 0;
|
|
1605
|
+
let targetDiscoveryStartedAt = 0;
|
|
1606
|
+
let interactionStartedAt = 0;
|
|
1607
|
+
try {
|
|
1608
|
+
// Stable selectors can address an already-focused editable control without
|
|
1609
|
+
// a discovery round trip. Snapshot-scoped refs are always revalidated before
|
|
1610
|
+
// mutation because app foregrounding or a page transition may invalidate them.
|
|
1611
|
+
const explicitSelector = optionalString(input.args.search_selector);
|
|
1612
|
+
const explicitTarget = optionalString(input.args.search_target);
|
|
1613
|
+
let searchTarget = explicitSelector;
|
|
1614
|
+
let discoveredSubmitTarget: string | undefined;
|
|
1615
|
+
if (explicitSelector) searchFlow = 'explicit_selector';
|
|
1616
|
+
if (!searchTarget && explicitTarget && !normalizedSemanticRef(explicitTarget)) {
|
|
1617
|
+
searchTarget = explicitTarget;
|
|
1618
|
+
searchFlow = 'explicit_target';
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
targetDiscoveryStartedAt = performance.now();
|
|
1622
|
+
if (!searchTarget) {
|
|
1623
|
+
initialAccessibilitySnapshot = true;
|
|
1624
|
+
accessibilitySnapshotRequests += 1;
|
|
1625
|
+
const discovery = JD_IOS_APP_ADAPTER.search!.discovery;
|
|
1626
|
+
let initialSnapshot: Record<string, unknown>;
|
|
1627
|
+
try {
|
|
1628
|
+
initialSnapshot = await runSessionSnapshotAttempt(
|
|
1629
|
+
input,
|
|
1630
|
+
record,
|
|
1631
|
+
{
|
|
1632
|
+
interactiveOnly: discovery.interactiveOnly,
|
|
1633
|
+
raw: discovery.raw,
|
|
1634
|
+
depth: discovery.depth,
|
|
1635
|
+
scope: discovery.scope,
|
|
1636
|
+
},
|
|
1637
|
+
'AGENT_DEVICE_SNAPSHOT_FAILED',
|
|
1638
|
+
30_000,
|
|
1639
|
+
);
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
return failWorkflowSession(input, record, error, keepSession);
|
|
1642
|
+
}
|
|
1643
|
+
const initialTargets = discoverJdSearchTargets(initialSnapshot, explicitTarget);
|
|
1644
|
+
const initialEntryTarget = initialTargets.entryTarget
|
|
1645
|
+
?? interactiveRef(initialSnapshot, /搜索|搜一搜|search|searchfield|请输入/i);
|
|
1646
|
+
searchTarget = initialTargets.editableTarget;
|
|
1647
|
+
discoveredSubmitTarget = initialTargets.submitTarget;
|
|
1648
|
+
|
|
1649
|
+
if (!searchTarget && initialEntryTarget) {
|
|
1650
|
+
searchFlow = 'navigated_from_entry';
|
|
1651
|
+
timingsMs.targetDiscovery = performance.now() - targetDiscoveryStartedAt;
|
|
1652
|
+
const navigationStartedAt = performance.now();
|
|
1653
|
+
nativeBatchRequests += 1;
|
|
1654
|
+
nativeBatchSteps += 2;
|
|
1655
|
+
try {
|
|
1656
|
+
await runSessionBatchAttempt(input, record, prepareAgentDeviceBatch([
|
|
1657
|
+
{ kind: 'press', input: { target: initialEntryTarget } },
|
|
1658
|
+
{ kind: 'wait', input: { wait_type: 'stable', quiet_ms: 500, timeout_ms: 10_000 } },
|
|
1659
|
+
], record), 20_000);
|
|
1660
|
+
} catch (error) {
|
|
1661
|
+
return failWorkflowSession(input, record, error, keepSession);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// Never trust the provider's incremental diff as authorization for the
|
|
1665
|
+
// next mutation. A JD page transition has returned contradictory zero
|
|
1666
|
+
// diffs in production, so force one fresh tree and issue new refs.
|
|
1667
|
+
accessibilitySnapshotRequests += 1;
|
|
1668
|
+
navigationSnapshotRequests += 1;
|
|
1669
|
+
let searchPageSnapshot: Record<string, unknown>;
|
|
1670
|
+
try {
|
|
1671
|
+
searchPageSnapshot = await runSessionSnapshotAttempt(
|
|
1672
|
+
input,
|
|
1673
|
+
record,
|
|
1674
|
+
{
|
|
1675
|
+
interactiveOnly: discovery.interactiveOnly,
|
|
1676
|
+
raw: discovery.raw,
|
|
1677
|
+
depth: discovery.depth,
|
|
1678
|
+
scope: discovery.scope,
|
|
1679
|
+
forceFull: true,
|
|
1680
|
+
},
|
|
1681
|
+
'AGENT_DEVICE_SNAPSHOT_FAILED',
|
|
1682
|
+
45_000,
|
|
1683
|
+
);
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
return failWorkflowSession(input, record, error, keepSession);
|
|
1686
|
+
}
|
|
1687
|
+
const searchPageTargets = discoverJdSearchTargets(searchPageSnapshot);
|
|
1688
|
+
// Newer JD builds expose TextView/TextField; some versions expose a real
|
|
1689
|
+
// SearchField on the dedicated page. The latter is safe only after the
|
|
1690
|
+
// forced post-navigation snapshot.
|
|
1691
|
+
searchTarget = searchPageTargets.editableTarget
|
|
1692
|
+
?? searchPageTargets.entryTarget
|
|
1693
|
+
?? interactiveRef(searchPageSnapshot, /搜索|搜一搜|search|searchfield|请输入/i);
|
|
1694
|
+
discoveredSubmitTarget = searchPageTargets.submitTarget
|
|
1695
|
+
?? interactiveRef(searchPageSnapshot, /^(搜索|搜一搜|search)$/i);
|
|
1696
|
+
timingsMs.navigation = performance.now() - navigationStartedAt;
|
|
1697
|
+
} else {
|
|
1698
|
+
searchFlow = 'direct_editable';
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
if (!searchTarget) {
|
|
1702
|
+
throw new AssistantPluginError('JD_SEARCH_FIELD_NOT_FOUND', 'JD opened, but the App adapter could not find a bounded editable search field.', {
|
|
1703
|
+
retryable: false,
|
|
1704
|
+
details: {
|
|
1705
|
+
adapter: JD_IOS_APP_ADAPTER.id,
|
|
1706
|
+
snapshotPolicy: JD_IOS_APP_ADAPTER.search!.discovery,
|
|
1707
|
+
searchFlow,
|
|
1708
|
+
sessionRetained: keepSession,
|
|
1709
|
+
},
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
if (timingsMs.targetDiscovery === 0) {
|
|
1714
|
+
timingsMs.targetDiscovery = performance.now() - targetDiscoveryStartedAt;
|
|
1715
|
+
}
|
|
1716
|
+
interactionStartedAt = performance.now();
|
|
1717
|
+
const submitTarget = optionalString(input.args.submit_selector)
|
|
1718
|
+
?? optionalString(input.args.submit_target)
|
|
1719
|
+
?? discoveredSubmitTarget;
|
|
1720
|
+
const waitStep: AgentDeviceBatchStep = resultText
|
|
1721
|
+
? { kind: 'wait', input: { wait_type: 'text', text: resultText, timeout_ms: 15_000 } }
|
|
1722
|
+
: resultSelector
|
|
1723
|
+
? { kind: 'wait', input: { wait_type: 'selector', selector: resultSelector, timeout_ms: 15_000 } }
|
|
1724
|
+
: { kind: 'wait', input: { wait_type: 'stable', quiet_ms: 500, timeout_ms: 15_000 } };
|
|
1725
|
+
const evidenceSteps: AgentDeviceBatchStep[] = exactResultWait
|
|
1726
|
+
? [waitStep]
|
|
1727
|
+
: [
|
|
1728
|
+
waitStep,
|
|
1729
|
+
{
|
|
1730
|
+
kind: 'snapshot',
|
|
1731
|
+
input: {
|
|
1732
|
+
interactive: true,
|
|
1733
|
+
depth: snapshotDepth,
|
|
1734
|
+
...(resultScope ? { scope: resultScope } : {}),
|
|
1735
|
+
},
|
|
1736
|
+
},
|
|
1737
|
+
];
|
|
1738
|
+
let fillStep: AgentDeviceBatchStep = {
|
|
1739
|
+
kind: 'fill',
|
|
1740
|
+
input: { target: searchTarget, text: query, delay_ms: 20 },
|
|
1741
|
+
};
|
|
1742
|
+
const cachedSearchRef = !optionalString(input.args.search_selector) && /^@e\d+(?:~s\d+)?$/.test(searchTarget);
|
|
1743
|
+
if (cachedSearchRef) {
|
|
1744
|
+
nativeBatchRequests += 1;
|
|
1745
|
+
nativeBatchSteps += 1;
|
|
1746
|
+
try {
|
|
1747
|
+
await runSessionBatchAttempt(input, record, prepareAgentDeviceBatch([fillStep], record), 20_000);
|
|
1748
|
+
} catch (error) {
|
|
1749
|
+
if (!isStaleAccessibilityRefError(error)) return failWorkflowSession(input, record, error, keepSession);
|
|
1750
|
+
staleRefRecovery = true;
|
|
1751
|
+
initialAccessibilitySnapshot = true;
|
|
1752
|
+
accessibilitySnapshotRequests += 1;
|
|
1753
|
+
let refreshedSnapshot: Record<string, unknown>;
|
|
1754
|
+
try {
|
|
1755
|
+
const discovery = JD_IOS_APP_ADAPTER.search!.discovery;
|
|
1756
|
+
refreshedSnapshot = await runSessionSnapshotAttempt(
|
|
1757
|
+
input,
|
|
1758
|
+
record,
|
|
1759
|
+
{
|
|
1760
|
+
interactiveOnly: discovery.interactiveOnly,
|
|
1761
|
+
raw: discovery.raw,
|
|
1762
|
+
depth: discovery.depth,
|
|
1763
|
+
scope: discovery.scope,
|
|
1764
|
+
},
|
|
1765
|
+
'AGENT_DEVICE_SNAPSHOT_FAILED',
|
|
1766
|
+
30_000,
|
|
1767
|
+
);
|
|
1768
|
+
} catch (snapshotError) {
|
|
1769
|
+
return failWorkflowSession(input, record, snapshotError, keepSession);
|
|
1770
|
+
}
|
|
1771
|
+
const refreshedTargets = discoverJdSearchTargets(refreshedSnapshot);
|
|
1772
|
+
const refreshedTarget = refreshedTargets.editableTarget
|
|
1773
|
+
?? refreshedTargets.entryTarget
|
|
1774
|
+
?? interactiveRef(refreshedSnapshot, /搜索|搜一搜|search|searchfield|请输入/i);
|
|
1775
|
+
if (!refreshedTarget) {
|
|
1776
|
+
return failWorkflowSession(input, record, new AssistantPluginError(
|
|
1777
|
+
'JD_SEARCH_FIELD_NOT_FOUND',
|
|
1778
|
+
'The cached JD search ref was stale and no replacement search field was found.',
|
|
1779
|
+
{ retryable: false, details: { providerCode: 'ELEMENT_NOT_FOUND' } },
|
|
1780
|
+
), keepSession);
|
|
1781
|
+
}
|
|
1782
|
+
fillStep = { kind: 'fill', input: { target: refreshedTarget, text: query, delay_ms: 20 } };
|
|
1783
|
+
nativeBatchRequests += 1;
|
|
1784
|
+
nativeBatchSteps += 1;
|
|
1785
|
+
await runSessionBatch(input, record, prepareAgentDeviceBatch([fillStep], record), 20_000);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
const fallbackScope = resultScope ?? resultText ?? resultSelector;
|
|
1789
|
+
if (submitTarget) {
|
|
1790
|
+
nativeBatchRequests += 1;
|
|
1791
|
+
nativeBatchSteps += (cachedSearchRef ? 1 : 2) + evidenceSteps.length;
|
|
1792
|
+
const prepared = prepareAgentDeviceBatch([
|
|
1793
|
+
...(cachedSearchRef ? [] : [fillStep]),
|
|
1794
|
+
{ kind: 'press', input: { target: submitTarget } },
|
|
1795
|
+
...evidenceSteps,
|
|
1796
|
+
], record);
|
|
1797
|
+
if (exactResultWait) {
|
|
1798
|
+
try {
|
|
1799
|
+
finalSnapshot = await runSessionBatchAttempt(input, record, prepared, 30_000);
|
|
1800
|
+
} catch (error) {
|
|
1801
|
+
if (!isExactEvidenceWaitMiss(error)) {
|
|
1802
|
+
return failWorkflowSession(input, record, error, keepSession);
|
|
1803
|
+
}
|
|
1804
|
+
exactWaitFallback = true;
|
|
1805
|
+
const recovered = await recoverExactWaitEvidence(input, record, fallbackScope, snapshotDepth, keepSession);
|
|
1806
|
+
finalSnapshot = recovered.snapshot;
|
|
1807
|
+
accessibilityEvidenceTier = recovered.tier;
|
|
1808
|
+
accessibilitySnapshotRequests += recovered.snapshotRequests;
|
|
1809
|
+
}
|
|
1810
|
+
} else {
|
|
1811
|
+
finalSnapshot = await runSessionBatch(input, record, prepared, 30_000);
|
|
1812
|
+
accessibilitySnapshotRequests += 1;
|
|
1813
|
+
}
|
|
1814
|
+
} else {
|
|
1815
|
+
nativeBatchRequests += cachedSearchRef ? 1 : 2;
|
|
1816
|
+
nativeBatchSteps += (cachedSearchRef ? 0 : 1) + evidenceSteps.length;
|
|
1817
|
+
// The current batch schema supports only status/dismiss, so Return stays
|
|
1818
|
+
// a separate provider command, but only when the negotiated contract
|
|
1819
|
+
// explicitly advertises it.
|
|
1820
|
+
if (!capabilityProfile().keyboard.returnSupported) {
|
|
1821
|
+
return failWorkflowSession(input, record, new AssistantPluginError(
|
|
1822
|
+
'PLUGIN_ACTION_NOT_SUPPORTED',
|
|
1823
|
+
'The detected agent-device contract does not support keyboard Return.',
|
|
1824
|
+
{ retryable: false, details: { providerCode: 'UNSUPPORTED_OPERATION' } },
|
|
1825
|
+
), keepSession);
|
|
1826
|
+
}
|
|
1827
|
+
if (!cachedSearchRef) await runSessionBatch(input, record, prepareAgentDeviceBatch([fillStep], record), 20_000);
|
|
1828
|
+
await runSessionCommand(input, record, ['keyboard', 'return'], 'JD_SEARCH_SUBMIT_FAILED');
|
|
1829
|
+
const prepared = prepareAgentDeviceBatch(evidenceSteps, record);
|
|
1830
|
+
if (exactResultWait) {
|
|
1831
|
+
try {
|
|
1832
|
+
finalSnapshot = await runSessionBatchAttempt(input, record, prepared, 30_000);
|
|
1833
|
+
} catch (error) {
|
|
1834
|
+
if (!isExactEvidenceWaitMiss(error)) {
|
|
1835
|
+
return failWorkflowSession(input, record, error, keepSession);
|
|
1836
|
+
}
|
|
1837
|
+
exactWaitFallback = true;
|
|
1838
|
+
const recovered = await recoverExactWaitEvidence(input, record, fallbackScope, snapshotDepth, keepSession);
|
|
1839
|
+
finalSnapshot = recovered.snapshot;
|
|
1840
|
+
accessibilityEvidenceTier = recovered.tier;
|
|
1841
|
+
accessibilitySnapshotRequests += recovered.snapshotRequests;
|
|
1842
|
+
}
|
|
1843
|
+
} else {
|
|
1844
|
+
finalSnapshot = await runSessionBatch(input, record, prepared, 30_000);
|
|
1845
|
+
accessibilitySnapshotRequests += 1;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
timingsMs.interactionAndEvidence = performance.now() - interactionStartedAt;
|
|
1849
|
+
if (captureScreenshot) {
|
|
1850
|
+
const screenshotStartedAt = performance.now();
|
|
1851
|
+
screenshot = await executeIosAgentDeviceAction(subActionInput(input, 'agent_device_screenshot', {
|
|
1852
|
+
interaction_id: interactionId,
|
|
1853
|
+
label: 'jd-search-results',
|
|
1854
|
+
max_size: 1600,
|
|
1855
|
+
}));
|
|
1856
|
+
timingsMs.screenshot = performance.now() - screenshotStartedAt;
|
|
1857
|
+
}
|
|
1858
|
+
} finally {
|
|
1859
|
+
if (!keepSession) {
|
|
1860
|
+
const closeStartedAt = performance.now();
|
|
1861
|
+
await executeIosAgentDeviceAction(subActionInput(input, 'agent_device_close', { interaction_id: interactionId }));
|
|
1862
|
+
timingsMs.close = performance.now() - closeStartedAt;
|
|
1863
|
+
}
|
|
1864
|
+
timingsMs.total = performance.now() - workflowStartedAt;
|
|
1865
|
+
}
|
|
1866
|
+
return {
|
|
1867
|
+
provider: 'agent-device',
|
|
1868
|
+
workflow: 'jd_product_search',
|
|
1869
|
+
app: JD_BUNDLE_ID,
|
|
1870
|
+
device: selected,
|
|
1871
|
+
query: '<redacted>',
|
|
1872
|
+
runnerReadiness: sessionReused ? 'verified_by_active_session' : 'verified_by_open',
|
|
1873
|
+
executionPlan: {
|
|
1874
|
+
relaunch: input.args.relaunch === true,
|
|
1875
|
+
sessionReused,
|
|
1876
|
+
sessionKept: keepSession,
|
|
1877
|
+
screenshotCaptured: captureScreenshot,
|
|
1878
|
+
deviceInventoryRequests,
|
|
1879
|
+
nativeBatchRequests,
|
|
1880
|
+
nativeBatchSteps,
|
|
1881
|
+
exactResultWait,
|
|
1882
|
+
accessibilityEvidenceTier,
|
|
1883
|
+
initialAccessibilitySnapshot,
|
|
1884
|
+
staleRefRecovery,
|
|
1885
|
+
exactWaitFallback,
|
|
1886
|
+
accessibilitySnapshotRequests,
|
|
1887
|
+
navigationSnapshotRequests,
|
|
1888
|
+
searchFlow,
|
|
1889
|
+
fullAccessibilitySnapshot: accessibilityEvidenceTier === 'full_snapshot',
|
|
1890
|
+
resultScope: resultScope ?? null,
|
|
1891
|
+
snapshotDepth: accessibilityEvidenceTier === 'exact_wait' ? null : snapshotDepth,
|
|
1892
|
+
timingsMs: Object.fromEntries(
|
|
1893
|
+
Object.entries(timingsMs).map(([key, value]) => [key, Math.round(value * 100) / 100]),
|
|
1894
|
+
),
|
|
1895
|
+
},
|
|
1896
|
+
visibleResultText: boundedVisibleText(finalSnapshot, query),
|
|
1897
|
+
result: bounded(redactExactText(finalSnapshot, query)),
|
|
1898
|
+
artifactCandidates: screenshot?.artifactCandidates,
|
|
1899
|
+
interaction: readAgentDeviceInteraction(input.repoRoot, interactionId),
|
|
1900
|
+
safety: {
|
|
1901
|
+
allowed: 'product_information_search',
|
|
1902
|
+
blocked: ['credentials', 'verification', 'biometrics', 'checkout', 'purchase', 'payment'],
|
|
1903
|
+
},
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
export function isIosAgentDeviceAction(actionId: string): boolean {
|
|
1908
|
+
return actionId.startsWith('agent_device_');
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
export function iosAgentDeviceCapabilities(): AssistantPluginCapability[] {
|
|
1912
|
+
const actions = iosAgentDeviceActions().map((action) => action.actionId);
|
|
1913
|
+
return [
|
|
1914
|
+
{
|
|
1915
|
+
capabilityId: 'ios-agent-device-simulator',
|
|
1916
|
+
title: 'agent-device iOS Simulator',
|
|
1917
|
+
description: 'Optional capability-negotiated agent-device sessions for bounded iOS Simulator inspection and interaction.',
|
|
1918
|
+
scopes: ['ios.discover', 'ios.simulator'],
|
|
1919
|
+
actions,
|
|
1920
|
+
},
|
|
1921
|
+
{
|
|
1922
|
+
capabilityId: 'ios-agent-device-physical',
|
|
1923
|
+
title: 'agent-device physical iPhone',
|
|
1924
|
+
description: 'Optional signed, capability-negotiated agent-device XCTest sessions for one exact connected physical iPhone.',
|
|
1925
|
+
scopes: ['ios.discover', 'ios.device'],
|
|
1926
|
+
actions,
|
|
1927
|
+
},
|
|
1928
|
+
];
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
export function iosAgentDeviceActions(): AssistantPluginActionDescriptor[] {
|
|
1932
|
+
const read = [{ resource: 'repo-state' as const, mode: 'write' as const }];
|
|
1933
|
+
const write = [
|
|
1934
|
+
{ resource: 'workspace' as const, mode: 'write' as const },
|
|
1935
|
+
{ resource: 'repo-state' as const, mode: 'write' as const },
|
|
1936
|
+
];
|
|
1937
|
+
const interactionProperty = { interaction_id: { type: 'string' } };
|
|
1938
|
+
return [
|
|
1939
|
+
{
|
|
1940
|
+
actionId: 'agent_device_status', title: 'agent-device status',
|
|
1941
|
+
description: 'Resolve the exact executable and verify a reviewed agent-device command contract.',
|
|
1942
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 5_000, cancellable: true, idempotent: true,
|
|
1943
|
+
scopes: ['ios.discover'], resourceClaims: [], argumentsSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
1944
|
+
},
|
|
1945
|
+
{
|
|
1946
|
+
actionId: 'agent_device_doctor', title: 'agent-device doctor',
|
|
1947
|
+
description: 'Run the typed local iOS doctor command. This may warm the local XCTest runner cache.',
|
|
1948
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 4 * 60_000, cancellable: true, idempotent: false,
|
|
1949
|
+
scopes: ['ios.discover', 'ios.simulator', 'ios.device'], resourceClaims: write,
|
|
1950
|
+
argumentsSchema: { type: 'object', properties: { app: { type: 'string' }, device: { type: 'string' }, team_id: { type: 'string' }, runner_bundle_id: { type: 'string' }, developer_dir: { type: 'string' } }, additionalProperties: false },
|
|
1951
|
+
},
|
|
1952
|
+
{
|
|
1953
|
+
actionId: 'agent_device_prepare', title: 'Prepare signed iOS Runner',
|
|
1954
|
+
description: 'Build, sign, install and health-check the agent-device XCTest Runner for one exact iOS target.',
|
|
1955
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 10 * 60_000, cancellable: true, idempotent: false,
|
|
1956
|
+
scopes: ['ios.discover', 'ios.simulator', 'ios.device'], resourceClaims: write,
|
|
1957
|
+
argumentsSchema: {
|
|
1958
|
+
type: 'object', properties: { device: { type: 'string' }, team_id: { type: 'string' }, runner_bundle_id: { type: 'string' }, developer_dir: { type: 'string' } },
|
|
1959
|
+
required: ['device'], additionalProperties: false,
|
|
1960
|
+
},
|
|
1961
|
+
},
|
|
1962
|
+
{
|
|
1963
|
+
actionId: 'agent_device_open', title: 'Open agent-device iOS session',
|
|
1964
|
+
description: 'Open an app on one exact connected physical iPhone or already-booted iOS Simulator.',
|
|
1965
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 4 * 60_000, cancellable: true, idempotent: false,
|
|
1966
|
+
scopes: ['ios.simulator', 'ios.device'], resourceClaims: write,
|
|
1967
|
+
argumentsSchema: {
|
|
1968
|
+
type: 'object', properties: { app: { type: 'string' }, device: { type: 'string' }, relaunch: { type: 'boolean' }, team_id: { type: 'string' }, runner_bundle_id: { type: 'string' }, developer_dir: { type: 'string' } },
|
|
1969
|
+
required: ['app'], additionalProperties: false,
|
|
1970
|
+
},
|
|
1971
|
+
},
|
|
1972
|
+
{
|
|
1973
|
+
actionId: 'agent_device_batch', title: 'Run fast typed iOS action batch',
|
|
1974
|
+
description: `Run up to ${MAX_BATCH_STEPS} allowlisted session steps in one agent-device process and one daemon request. Mutating steps always settle before the next step.`,
|
|
1975
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 2 * 60_000, cancellable: true, idempotent: false,
|
|
1976
|
+
scopes: ['ios.simulator', 'ios.device'], resourceClaims: write,
|
|
1977
|
+
argumentsSchema: {
|
|
1978
|
+
type: 'object',
|
|
1979
|
+
properties: {
|
|
1980
|
+
...interactionProperty,
|
|
1981
|
+
steps: {
|
|
1982
|
+
type: 'array', minItems: 1, maxItems: MAX_BATCH_STEPS,
|
|
1983
|
+
items: {
|
|
1984
|
+
type: 'object',
|
|
1985
|
+
properties: {
|
|
1986
|
+
kind: { type: 'string', enum: [...BATCH_KINDS] },
|
|
1987
|
+
input: { type: 'object', additionalProperties: true },
|
|
1988
|
+
},
|
|
1989
|
+
required: ['kind', 'input'],
|
|
1990
|
+
additionalProperties: false,
|
|
1991
|
+
},
|
|
1992
|
+
},
|
|
1993
|
+
timeout_ms: { type: 'number' },
|
|
1994
|
+
},
|
|
1995
|
+
required: ['interaction_id', 'steps'],
|
|
1996
|
+
additionalProperties: false,
|
|
1997
|
+
},
|
|
1998
|
+
},
|
|
1999
|
+
{
|
|
2000
|
+
actionId: 'agent_device_jd_search', title: 'Search JD on a physical iPhone',
|
|
2001
|
+
description: 'Search JD with one bounded non-sensitive product query. A fresh one-shot call captures a PNG and closes; an explicitly reused active JD interaction defaults to no screenshot and remains warm. Login, verification, checkout, purchase, payment and biometrics remain human-only.',
|
|
2002
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 10 * 60_000, cancellable: true, idempotent: false,
|
|
2003
|
+
scopes: ['ios.device'], resourceClaims: write,
|
|
2004
|
+
argumentsSchema: {
|
|
2005
|
+
type: 'object', properties: {
|
|
2006
|
+
device: { type: 'string' }, query: { type: 'string' }, interaction_id: { type: 'string' },
|
|
2007
|
+
keep_session: { type: 'boolean' }, capture_screenshot: { type: 'boolean' },
|
|
2008
|
+
team_id: { type: 'string' }, runner_bundle_id: { type: 'string' }, developer_dir: { type: 'string' },
|
|
2009
|
+
search_target: { type: 'string' }, search_selector: { type: 'string' },
|
|
2010
|
+
submit_target: { type: 'string' }, submit_selector: { type: 'string' }, relaunch: { type: 'boolean' },
|
|
2011
|
+
result_text: { type: 'string' }, result_selector: { type: 'string' },
|
|
2012
|
+
result_scope: { type: 'string' }, snapshot_depth: { type: 'number' },
|
|
2013
|
+
},
|
|
2014
|
+
required: ['device', 'query'], additionalProperties: false,
|
|
2015
|
+
},
|
|
2016
|
+
},
|
|
2017
|
+
{
|
|
2018
|
+
actionId: 'agent_device_snapshot', title: 'Snapshot agent-device session',
|
|
2019
|
+
description: 'Capture bounded accessibility state from an active agent-device iOS session.',
|
|
2020
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
2021
|
+
scopes: ['ios.simulator', 'ios.device'], resourceClaims: read,
|
|
2022
|
+
argumentsSchema: { type: 'object', properties: { ...interactionProperty, interactive: { type: 'boolean' }, raw: { type: 'boolean' }, depth: { type: 'number' }, scope: { type: 'string' }, force_full: { type: 'boolean' } }, required: ['interaction_id'], additionalProperties: false },
|
|
2023
|
+
},
|
|
2024
|
+
{
|
|
2025
|
+
actionId: 'agent_device_press', title: 'Press agent-device target',
|
|
2026
|
+
description: 'Press one ref, selector, or explicit coordinate pair and return a settled bounded diff.',
|
|
2027
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
2028
|
+
scopes: ['ios.simulator', 'ios.device'], resourceClaims: write,
|
|
2029
|
+
argumentsSchema: { type: 'object', properties: { ...interactionProperty, target: { type: 'string' }, x: { type: 'number' }, y: { type: 'number' } }, required: ['interaction_id'], additionalProperties: false },
|
|
2030
|
+
},
|
|
2031
|
+
{
|
|
2032
|
+
actionId: 'agent_device_fill', title: 'Fill agent-device target',
|
|
2033
|
+
description: 'Replace non-sensitive text in one ref or selector and return a redacted settled diff. Use manual UI entry for passwords or verification codes.',
|
|
2034
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
2035
|
+
scopes: ['ios.simulator', 'ios.device'], resourceClaims: write,
|
|
2036
|
+
argumentsSchema: { type: 'object', properties: { ...interactionProperty, target: { type: 'string' }, text: { type: 'string' }, delay_ms: { type: 'number' } }, required: ['interaction_id', 'target', 'text'], additionalProperties: false },
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
actionId: 'agent_device_scroll', title: 'Scroll agent-device session',
|
|
2040
|
+
description: 'Scroll one active agent-device iOS session serially.',
|
|
2041
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
2042
|
+
scopes: ['ios.simulator', 'ios.device'], resourceClaims: write,
|
|
2043
|
+
argumentsSchema: { type: 'object', properties: { ...interactionProperty, direction: { type: 'string', enum: ['up', 'down', 'left', 'right', 'top', 'bottom'] }, amount: { type: 'number' } }, required: ['interaction_id', 'direction'], additionalProperties: false },
|
|
2044
|
+
},
|
|
2045
|
+
{
|
|
2046
|
+
actionId: 'agent_device_screenshot', title: 'Capture agent-device screenshot',
|
|
2047
|
+
description: 'Capture a bounded PNG into Controller-owned iOS artifact storage.',
|
|
2048
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'none', defaultTimeoutMs: 60_000, cancellable: true, idempotent: false,
|
|
2049
|
+
scopes: ['ios.simulator', 'ios.device'], resourceClaims: write,
|
|
2050
|
+
argumentsSchema: { type: 'object', properties: { ...interactionProperty, label: { type: 'string' }, overlay_refs: { type: 'boolean' }, max_size: { type: 'number' } }, required: ['interaction_id'], additionalProperties: false },
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
actionId: 'agent_device_events', title: 'Read agent-device events',
|
|
2054
|
+
description: 'Read a bounded page of daemon-owned session events.',
|
|
2055
|
+
readOnly: true, risk: 'readonly', confirmation: 'none', defaultTimeoutMs: 30_000, cancellable: true, idempotent: true,
|
|
2056
|
+
scopes: ['ios.simulator', 'ios.device'], resourceClaims: read,
|
|
2057
|
+
argumentsSchema: { type: 'object', properties: { ...interactionProperty, limit: { type: 'number' }, cursor: { type: 'string' } }, required: ['interaction_id'], additionalProperties: false },
|
|
2058
|
+
},
|
|
2059
|
+
{
|
|
2060
|
+
actionId: 'agent_device_close', title: 'Close agent-device session',
|
|
2061
|
+
description: 'Close the provider session. Shutdown applies only to simulators; physical iPhones are never shut down.',
|
|
2062
|
+
readOnly: false, risk: 'workspace_write', confirmation: 'authorization', defaultTimeoutMs: 60_000, cancellable: true, idempotent: true,
|
|
2063
|
+
scopes: ['ios.simulator', 'ios.device'], resourceClaims: write,
|
|
2064
|
+
argumentsSchema: { type: 'object', properties: { ...interactionProperty, shutdown_simulator: { type: 'boolean' } }, required: ['interaction_id'], additionalProperties: false },
|
|
2065
|
+
},
|
|
2066
|
+
];
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
export async function executeIosAgentDeviceAction(input: AssistantPluginActionExecutionInput): Promise<Record<string, unknown>> {
|
|
2070
|
+
if (input.deadlineAtMs === undefined && typeof input.timeoutMs === 'number' && Number.isFinite(input.timeoutMs)) {
|
|
2071
|
+
input = { ...input, deadlineAtMs: Date.now() + Math.max(1, Math.trunc(input.timeoutMs)) };
|
|
2072
|
+
}
|
|
2073
|
+
if (input.actionId === 'agent_device_status') return { provider: 'agent-device', ...iosAgentDeviceStatus() };
|
|
2074
|
+
if (input.actionId === 'agent_device_close') {
|
|
2075
|
+
const interactionId = requireString(input.args.interaction_id, 'interaction_id');
|
|
2076
|
+
const existing = readAgentDeviceInteraction(input.repoRoot, interactionId);
|
|
2077
|
+
if (existing && !isInteractionSessionActive(existing.status)) {
|
|
2078
|
+
return { provider: 'agent-device', interaction: existing, alreadyClosed: true };
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
const dependency = requireDependency();
|
|
2082
|
+
|
|
2083
|
+
if (input.actionId === 'agent_device_jd_search') return executeJdSearch(input);
|
|
2084
|
+
|
|
2085
|
+
if (input.actionId === 'agent_device_doctor') {
|
|
2086
|
+
const selected = selectTarget(input, optionalString(input.args.device));
|
|
2087
|
+
const args = ['doctor', '--platform', 'ios', '--device', selected.name];
|
|
2088
|
+
const app = optionalString(input.args.app);
|
|
2089
|
+
if (app) args.push('--app', app);
|
|
2090
|
+
args.push('--json');
|
|
2091
|
+
return {
|
|
2092
|
+
provider: 'agent-device',
|
|
2093
|
+
version: dependency.detectedVersion ?? IOS_AGENT_DEVICE_VERSION,
|
|
2094
|
+
contractFingerprint: dependency.capabilityProfile.contractFingerprint,
|
|
2095
|
+
device: selected,
|
|
2096
|
+
physicalDeviceSupported: selected.kind === 'device',
|
|
2097
|
+
result: bounded(await runJsonAsync(input, args, {
|
|
2098
|
+
signing: signingFromArgs(input.args),
|
|
2099
|
+
failureCode: 'AGENT_DEVICE_DOCTOR_FAILED',
|
|
2100
|
+
timeoutMs: 4 * 60_000,
|
|
2101
|
+
})),
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
if (input.actionId === 'agent_device_prepare') {
|
|
2106
|
+
const selected = selectTarget(input, requireString(input.args.device, 'device'));
|
|
2107
|
+
const signing = signingFromArgs(input.args);
|
|
2108
|
+
return {
|
|
2109
|
+
provider: 'agent-device',
|
|
2110
|
+
version: dependency.detectedVersion ?? IOS_AGENT_DEVICE_VERSION,
|
|
2111
|
+
contractFingerprint: dependency.capabilityProfile.contractFingerprint,
|
|
2112
|
+
device: selected,
|
|
2113
|
+
physicalDeviceSupported: selected.kind === 'device',
|
|
2114
|
+
result: bounded(await runJsonAsync(input, [
|
|
2115
|
+
'prepare', 'ios-runner', '--platform', 'ios', '--device', selected.name, '--timeout', '600000', '--json',
|
|
2116
|
+
], {
|
|
2117
|
+
signing,
|
|
2118
|
+
failureCode: 'AGENT_DEVICE_PREPARE_FAILED',
|
|
2119
|
+
timeoutMs: 10 * 60_000,
|
|
2120
|
+
})),
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
if (input.actionId === 'agent_device_open') {
|
|
2125
|
+
reconcileExpiredSessions(input);
|
|
2126
|
+
const app = requireString(input.args.app, 'app');
|
|
2127
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(app)) {
|
|
2128
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'agent_device_open accepts an app name or bundle identifier, not a URL or tokenized deep link.', { retryable: false });
|
|
2129
|
+
}
|
|
2130
|
+
const selected = selectTarget(input, optionalString(input.args.device));
|
|
2131
|
+
const provider = providerForDevice(selected);
|
|
2132
|
+
const conflict = listInteractionSessions(input.repoRoot, provider).find((entry) =>
|
|
2133
|
+
isInteractionSessionActive(entry.status) && interactionMayOwnTarget(entry, selected.aliases));
|
|
2134
|
+
if (conflict) {
|
|
2135
|
+
throw new AssistantPluginError('PLUGIN_RESOURCE_BUSY', 'The selected iOS target already has an active interaction.', {
|
|
2136
|
+
retryable: true,
|
|
2137
|
+
details: {
|
|
2138
|
+
interactionId: conflict.interactionId,
|
|
2139
|
+
targetId: conflict.targetId,
|
|
2140
|
+
resourceProvider: conflict.provider,
|
|
2141
|
+
automationEngine: interactionAutomationEngine(conflict),
|
|
2142
|
+
},
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
pruneInteractionSessions(input.repoRoot, provider, 100);
|
|
2146
|
+
const interactionId = `ios_agent_device_${randomUUID()}`;
|
|
2147
|
+
const createdAt = timestamp();
|
|
2148
|
+
const record: InteractionSessionRecord = {
|
|
2149
|
+
schemaVersion: 1,
|
|
2150
|
+
interactionId,
|
|
2151
|
+
provider,
|
|
2152
|
+
engine: 'agent-device',
|
|
2153
|
+
sessionId: `forge-${sanitize(interactionId).slice(-40)}`,
|
|
2154
|
+
targetId: selected.id,
|
|
2155
|
+
targetAliases: selected.aliases,
|
|
2156
|
+
status: 'starting',
|
|
2157
|
+
reason: selected.kind === 'simulator' ? 'ios_simulator_automation' : 'ios_physical_device_automation',
|
|
2158
|
+
instructions: app,
|
|
2159
|
+
owner: { repoId: input.repoId, requestId: input.requestId, jobId: input.jobId },
|
|
2160
|
+
createdAt,
|
|
2161
|
+
updatedAt: createdAt,
|
|
2162
|
+
expiresAt: new Date(hooks.now().getTime() + SESSION_EXPIRY_MS).toISOString(),
|
|
2163
|
+
};
|
|
2164
|
+
writeInteractionSession(input.repoRoot, record);
|
|
2165
|
+
writeSigningConfig(input, interactionId, signingFromArgs(input.args));
|
|
2166
|
+
const args = ['open', app, '--device', selected.name];
|
|
2167
|
+
if (input.args.relaunch === true) args.push('--relaunch');
|
|
2168
|
+
try {
|
|
2169
|
+
const result = await runJsonAsync(input, [...args, '--session', record.sessionId, '--platform', 'ios', '--json'], {
|
|
2170
|
+
record,
|
|
2171
|
+
timeoutMs: 4 * 60_000,
|
|
2172
|
+
failureCode: 'AGENT_DEVICE_OPEN_FAILED',
|
|
2173
|
+
});
|
|
2174
|
+
const active = patchInteractionSession(input.repoRoot, provider, interactionId, { status: 'waiting_for_user' }) ?? record;
|
|
2175
|
+
return {
|
|
2176
|
+
provider: 'agent-device',
|
|
2177
|
+
version: dependency.detectedVersion ?? IOS_AGENT_DEVICE_VERSION,
|
|
2178
|
+
contractFingerprint: dependency.capabilityProfile.contractFingerprint,
|
|
2179
|
+
interaction: active,
|
|
2180
|
+
device: selected,
|
|
2181
|
+
physicalDeviceSupported: selected.kind === 'device',
|
|
2182
|
+
result: bounded(result),
|
|
2183
|
+
};
|
|
2184
|
+
} catch (error) {
|
|
2185
|
+
return failSession(input, record, error);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
const record = requireRecord(input, input.actionId === 'agent_device_close');
|
|
2190
|
+
switch (input.actionId) {
|
|
2191
|
+
case 'agent_device_batch': {
|
|
2192
|
+
const prepared = prepareAgentDeviceBatch(input.args.steps, record);
|
|
2193
|
+
const timeoutMs = batchInteger(input.args.timeout_ms, 120_000, 1_000, 180_000);
|
|
2194
|
+
return {
|
|
2195
|
+
provider: 'agent-device',
|
|
2196
|
+
interaction: record,
|
|
2197
|
+
batched: true,
|
|
2198
|
+
stepCount: prepared.nativeSteps.length,
|
|
2199
|
+
result: bounded(await runSessionBatch(input, record, prepared, timeoutMs)),
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
case 'agent_device_snapshot': {
|
|
2203
|
+
const request: AgentDeviceSnapshotRequest = {
|
|
2204
|
+
interactiveOnly: input.args.interactive === true,
|
|
2205
|
+
raw: input.args.raw === true,
|
|
2206
|
+
depth: typeof input.args.depth === 'number' ? input.args.depth : undefined,
|
|
2207
|
+
scope: optionalString(input.args.scope),
|
|
2208
|
+
forceFull: input.args.force_full === true,
|
|
2209
|
+
};
|
|
2210
|
+
const result = await runSessionSnapshot(input, record, request);
|
|
2211
|
+
return {
|
|
2212
|
+
provider: 'agent-device',
|
|
2213
|
+
backend: result.provider === 'typed' ? 'typed' : 'cli',
|
|
2214
|
+
configuredBackend: configuredAgentDeviceBackendMode(),
|
|
2215
|
+
interaction: record,
|
|
2216
|
+
result: bounded(result),
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
case 'agent_device_press': {
|
|
2220
|
+
const target = optionalString(input.args.target);
|
|
2221
|
+
const hasPoint = typeof input.args.x === 'number' && typeof input.args.y === 'number';
|
|
2222
|
+
if (!target && !hasPoint) throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'target or x/y is required.', { retryable: false });
|
|
2223
|
+
if (record.provider === DEVICE_PROVIDER && target && SENSITIVE_SEMANTICS.test(target)) {
|
|
2224
|
+
throw new AssistantPluginError('IOS_DEVICE_SENSITIVE_ACTION_BLOCKED', 'Press targets involving credentials, verification, biometrics, checkout, purchase or payment require human interaction.', { retryable: false });
|
|
2225
|
+
}
|
|
2226
|
+
const args = appendSettleFlag(
|
|
2227
|
+
capabilityProfile(),
|
|
2228
|
+
['press', ...(target ? [target] : [String(input.args.x), String(input.args.y)])],
|
|
2229
|
+
'press',
|
|
2230
|
+
);
|
|
2231
|
+
return { provider: 'agent-device', interaction: record, result: bounded(await runSessionCommand(input, record, args, 'AGENT_DEVICE_PRESS_FAILED')) };
|
|
2232
|
+
}
|
|
2233
|
+
case 'agent_device_fill': {
|
|
2234
|
+
const target = requireString(input.args.target, 'target');
|
|
2235
|
+
const text = typeof input.args.text === 'string' ? input.args.text : undefined;
|
|
2236
|
+
if (text === undefined) throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'text is required.', { retryable: false });
|
|
2237
|
+
if (record.provider === DEVICE_PROVIDER && (SENSITIVE_SEMANTICS.test(target) || SENSITIVE_SEMANTICS.test(text))) {
|
|
2238
|
+
throw new AssistantPluginError('IOS_DEVICE_SENSITIVE_ACTION_BLOCKED', 'Sensitive text and credential, verification, checkout, purchase or payment targets require human interaction.', { retryable: false });
|
|
2239
|
+
}
|
|
2240
|
+
const profile = capabilityProfile();
|
|
2241
|
+
const args = appendSettleFlag(profile, ['fill', target, text]);
|
|
2242
|
+
if (typeof input.args.delay_ms === 'number') {
|
|
2243
|
+
if (!profile.fill.delayFlag) {
|
|
2244
|
+
throw new AssistantPluginError('PLUGIN_ACTION_NOT_SUPPORTED', 'The detected agent-device contract does not support delayed fill.', { retryable: false });
|
|
2245
|
+
}
|
|
2246
|
+
args.push(profile.fill.delayFlag, String(Math.max(0, Math.min(5_000, Math.trunc(input.args.delay_ms)))));
|
|
2247
|
+
}
|
|
2248
|
+
const result = await runSessionCommand(input, record, args, 'AGENT_DEVICE_FILL_FAILED');
|
|
2249
|
+
return { provider: 'agent-device', interaction: record, result: bounded(redactExactText(result, text)) };
|
|
2250
|
+
}
|
|
2251
|
+
case 'agent_device_scroll': {
|
|
2252
|
+
const direction = requireString(input.args.direction, 'direction');
|
|
2253
|
+
if (!['up', 'down', 'left', 'right', 'top', 'bottom'].includes(direction)) {
|
|
2254
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Unsupported scroll direction.', { retryable: false });
|
|
2255
|
+
}
|
|
2256
|
+
const args = ['scroll', direction];
|
|
2257
|
+
if (typeof input.args.amount === 'number') args.push(String(Math.max(1, Math.min(100, Math.trunc(input.args.amount)))));
|
|
2258
|
+
return { provider: 'agent-device', interaction: record, result: bounded(await runSessionCommand(input, record, args, 'AGENT_DEVICE_SCROLL_FAILED')) };
|
|
2259
|
+
}
|
|
2260
|
+
case 'agent_device_screenshot': {
|
|
2261
|
+
const label = sanitize(optionalString(input.args.label) ?? 'screenshot');
|
|
2262
|
+
const path = join(artifactDir(input, record.interactionId), `${label}-${hooks.now().getTime()}.png`);
|
|
2263
|
+
const args = ['screenshot', path];
|
|
2264
|
+
if (input.args.overlay_refs === true) args.push('--overlay-refs');
|
|
2265
|
+
if (typeof input.args.max_size === 'number') args.push('--max-size', String(Math.max(320, Math.min(4_096, Math.trunc(input.args.max_size)))));
|
|
2266
|
+
const result = await runSessionCommand(input, record, args, 'AGENT_DEVICE_SCREENSHOT_FAILED');
|
|
2267
|
+
if (!existsSync(path)) {
|
|
2268
|
+
return failSession(input, record, new AssistantPluginError('AGENT_DEVICE_SCREENSHOT_MISSING', 'agent-device succeeded without creating the requested screenshot.', { retryable: false }));
|
|
2269
|
+
}
|
|
2270
|
+
return {
|
|
2271
|
+
provider: 'agent-device', interaction: record, result: bounded(result),
|
|
2272
|
+
artifactCandidates: [{ kind: 'ios_agent_device_screenshot', mediaType: 'image/png', path }],
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
case 'agent_device_events': {
|
|
2276
|
+
const args = ['events'];
|
|
2277
|
+
if (typeof input.args.limit === 'number') args.push(String(Math.max(1, Math.min(200, Math.trunc(input.args.limit)))));
|
|
2278
|
+
const cursor = optionalString(input.args.cursor);
|
|
2279
|
+
if (cursor) args.push(cursor);
|
|
2280
|
+
const result = await runSessionCommand(input, record, args, 'AGENT_DEVICE_EVENTS_FAILED', 30_000);
|
|
2281
|
+
return { provider: 'agent-device', interaction: record, result: bounded(redactEventEvidence(result)) };
|
|
2282
|
+
}
|
|
2283
|
+
case 'agent_device_close': {
|
|
2284
|
+
if (!isInteractionSessionActive(record.status)) {
|
|
2285
|
+
return { provider: 'agent-device', interaction: record, alreadyClosed: true };
|
|
2286
|
+
}
|
|
2287
|
+
return serializeInteractionCommand(input, record, async (current) => {
|
|
2288
|
+
if (!isInteractionSessionActive(current.status)) {
|
|
2289
|
+
return { provider: 'agent-device', interaction: current, alreadyClosed: true };
|
|
2290
|
+
}
|
|
2291
|
+
const args = ['close'];
|
|
2292
|
+
if (current.provider === SIMULATOR_PROVIDER && input.args.shutdown_simulator === true) args.push('--shutdown');
|
|
2293
|
+
patchInteractionSession(input.repoRoot, current.provider, current.interactionId, { status: 'closing' });
|
|
2294
|
+
try {
|
|
2295
|
+
const closedProvider = await closeProviderSession(input, current, args);
|
|
2296
|
+
const closed = patchInteractionSession(input.repoRoot, current.provider, current.interactionId, { status: 'closed' }) ?? current;
|
|
2297
|
+
return {
|
|
2298
|
+
provider: 'agent-device',
|
|
2299
|
+
interaction: closed,
|
|
2300
|
+
result: bounded(closedProvider.result),
|
|
2301
|
+
providerAlreadyAbsent: closedProvider.providerAlreadyAbsent,
|
|
2302
|
+
};
|
|
2303
|
+
} catch (error) {
|
|
2304
|
+
return failSession(input, current, error);
|
|
2305
|
+
}
|
|
2306
|
+
}, { allowTerminal: true });
|
|
2307
|
+
}
|
|
2308
|
+
default:
|
|
2309
|
+
throw new AssistantPluginError('PLUGIN_ACTION_NOT_SUPPORTED', `ios/${input.actionId} is not supported.`, { retryable: false });
|
|
2310
|
+
}
|
|
2311
|
+
}
|