@moretea-labs/forge 1.8.1 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -2
- package/CHANGELOG.md +20 -0
- package/README.en.md +24 -3
- package/README.md +10 -6
- package/README.zh-CN.md +5 -3
- package/THIRD_PARTY_NOTICES.md +1 -2
- package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
- package/adapters/chatgpt/provider-delivery.ts +1 -1
- package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
- package/adapters/computer/chatgpt-extension-target.ts +295 -50
- package/adapters/computer/chrome-extension/background.js +227 -71
- package/adapters/computer/chrome-extension/content.js +181 -157
- package/adapters/computer/chrome-extension/manifest.json +3 -1
- package/adapters/computer/desktop-operator-provider.ts +18 -6
- package/adapters/computer/interaction-target-authority.ts +39 -9
- package/adapters/mcp/access-mode.ts +18 -39
- package/adapters/mcp/auth.ts +2 -6
- package/adapters/mcp/instructions.ts +2 -2
- package/adapters/mcp/multi-repository.ts +7 -28
- package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
- package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
- package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
- package/adapters/mcp/runtime-gateway/router.ts +3 -5
- package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
- package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
- package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
- package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
- package/adapters/mcp/server.ts +24 -3
- package/adapters/mcp/setup.ts +4 -10
- package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
- package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
- package/adapters/mcp/toolset-names.ts +11 -42
- package/adapters/mcp/toolset.ts +16 -90
- package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
- package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
- package/adapters/mcp/transports/http-observation.ts +18 -6
- package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
- package/adapters/mcp/transports/http.ts +54 -14
- package/adapters/mcp/types.ts +0 -6
- package/assets/hooks/lib/session-state.sh +0 -38
- package/assets/hooks/post-bash.sh +1 -1
- package/assets/hooks/post-tool-observer.sh +0 -5
- package/assets/hooks/prompt-guard.sh +0 -55
- package/assets/partials/04-project-structure.partial.md +1 -1
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -1
- package/assets/partials-agents/08-deep-docs.partial.md +1 -1
- package/assets/reference-configs/agentic-development-flow.md +1 -1
- package/assets/reference-configs/external-tooling.md +0 -76
- package/assets/reference-configs/release-deploy.md +1 -1
- package/assets/templates/helpers/architecture-queue.sh +1 -1
- package/assets/templates/helpers/check-agent-tooling.sh +1 -327
- package/assets/templates/helpers/check-task-workflow.sh +1 -1
- package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
- package/assets/templates/helpers/migrate-project-template.sh +2 -2
- package/assets/workflow-contract.v1.json +0 -2
- package/docs/README.md +5 -2
- package/docs/architecture/CURRENT.md +114 -30
- package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
- package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
- package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
- package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
- package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
- package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
- package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
- package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
- package/docs/architecture/domains/public-surface.md +3 -5
- package/docs/architecture/domains/verification.md +5 -5
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
- package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
- package/docs/forge-plugin-management.md +14 -2
- package/docs/operations/controller-browser-plugin.md +23 -0
- package/docs/operations/features.md +6 -7
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/github-repository.md +4 -4
- package/docs/operations/github-semantic-release-projection.md +20 -0
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +13 -54
- package/docs/operations/runtime-performance-diagnostics.md +8 -0
- package/docs/operations/standalone-disaster-recovery.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/03-first-repository-task.md +2 -0
- package/docs/wiki/Computer-and-Plugins.md +78 -0
- package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
- package/docs/wiki/Home.md +16 -2
- package/docs/wiki/Implementation.md +24 -12
- package/docs/wiki/Installation.md +28 -32
- package/docs/wiki/Integrations.md +4 -0
- package/docs/wiki/Quick-Start.md +35 -22
- package/docs/wiki/Releases-and-Upgrades.md +15 -25
- package/docs/wiki/_Sidebar.md +5 -1
- package/package.json +1 -12
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
- package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
- package/packages/kernel/scheduler/application/eligibility.ts +4 -1
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
- package/packages/kernel/work/application/work-service.ts +1 -0
- package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
- package/packages/kernel/work/domain/state-machine.ts +9 -2
- package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
- package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
- package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
- package/scripts/assemble-template.ts +11 -72
- package/scripts/check-mcp-compatibility.ts +55 -74
- package/scripts/check-package-identity.mjs +0 -5
- package/scripts/check-runtime-architecture.mjs +305 -28
- package/scripts/init-project.sh +0 -46
- package/scripts/install-ios-hid-toolchain.sh +60 -0
- package/scripts/install-standalone-recovery.ts +1 -1
- package/scripts/lib/project-init-lib.sh +3 -3
- package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
- package/scripts/open-source-audit-allowlist.txt +0 -1
- package/scripts/run-governed-gate.ts +2 -0
- package/scripts/smoke-ios-physical-device.ts +38 -10
- package/scripts/smoke-mcp-http-runtime.ts +7 -8
- package/scripts/smoke-mcp-tool-surface.ts +27 -41
- package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
- package/scripts/sync-generated-authority.mjs +0 -1
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/agent-jobs/integration.ts +2 -2
- package/src/cli/chatgpt-browser/file-policy.ts +2 -2
- package/src/cli/commands/doctor.ts +1 -143
- package/src/cli/commands/global-runtime.ts +3 -196
- package/src/cli/commands/init-hook.ts +6 -23
- package/src/cli/commands/init.ts +3 -252
- package/src/cli/commands/mcp.ts +0 -5
- package/src/cli/commands/prompt-guard-decision.ts +0 -2
- package/src/cli/commands/recovery.ts +2 -14
- package/src/cli/commands/repository.ts +17 -1
- package/src/cli/commands/setup-execution.ts +41 -0
- package/src/cli/controller/completion-orchestrator.ts +1 -1
- package/src/cli/controller/composite-operations.ts +1 -1
- package/src/cli/controller/context/instruction-context.ts +1 -1
- package/src/cli/controller/context/known-paths.ts +2 -2
- package/src/cli/controller/context/query-planning.ts +11 -4
- package/src/cli/controller/context/rule-resolution.ts +2 -2
- package/src/cli/controller/context/source-materializer.ts +2 -2
- package/src/cli/controller/context/structural-provider.ts +84 -0
- package/src/cli/controller/context/structural.ts +6 -6
- package/src/cli/controller/context/types.ts +8 -9
- package/src/cli/controller/context-pack.ts +97 -76
- package/src/cli/controller/operational-plan.ts +1 -1
- package/src/cli/controller/systemd-user.ts +4 -10
- package/src/cli/editing/edit-session.ts +17 -5
- package/src/cli/github/github.ts +4 -4
- package/src/cli/github/semantic-projection.ts +174 -0
- package/src/cli/hook/prompt-intents.ts +0 -9
- package/src/cli/index.ts +1 -43
- package/src/cli/local-bridge/connector-freshness.ts +1 -12
- package/src/cli/local-bridge/console-automations.ts +1 -1
- package/src/cli/local-bridge/console-view-models.ts +1 -1
- package/src/cli/local-bridge/facade-api.ts +0 -2
- package/src/cli/repositories/command-classifier.ts +0 -9
- package/src/cli/repositories/command-scope.ts +2 -2
- package/src/cli/repositories/locks.ts +20 -0
- package/src/cli/repositories/registry.ts +2 -1
- package/src/cli/repositories/safe-patch.ts +2 -12
- package/src/cli/repositories/structured-git.ts +39 -3
- package/src/cli/repository/inspector.ts +185 -343
- package/src/effects/process-runner.ts +17 -1
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +7 -0
- package/src/runtime/context/semantic-navigation.ts +1 -1
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
- package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
- package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
- package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
- package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
- package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
- package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
- package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
- package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
- package/src/runtime/control-plane/governance/external-effects.ts +1 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
- package/src/runtime/evidence/sensitive-output.ts +22 -2
- package/src/runtime/execution/jobs/child-reference.ts +11 -0
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
- package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
- package/src/runtime/execution/process-runtime/runtime.ts +20 -16
- package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
- package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
- package/src/runtime/execution/workers/executor.ts +1 -2
- package/src/runtime/maintenance/cleanup.ts +41 -5
- package/src/runtime/plugins/action-application.ts +12 -1
- package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
- package/src/runtime/plugins/browser-adapter.ts +381 -313
- package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
- package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
- package/src/runtime/plugins/browser-registration.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +26 -35
- package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
- package/src/runtime/plugins/computer-registration.ts +76 -9
- package/src/runtime/plugins/github-adapter.ts +30 -3
- package/src/runtime/plugins/gmail-adapter.ts +166 -1
- package/src/runtime/plugins/google-shared.ts +10 -5
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
- package/src/runtime/plugins/ios-adapter.ts +45 -40
- package/src/runtime/plugins/ios-agent-device.ts +54 -4
- package/src/runtime/plugins/ios-device-ownership.ts +103 -0
- package/src/runtime/plugins/ios-physical-device.ts +497 -79
- package/src/runtime/plugins/local-system-adapter.ts +1 -99
- package/src/runtime/plugins/store.ts +80 -7
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/projections/git-status-sampler.ts +2 -19
- package/src/runtime/recovery/actions.ts +1 -1
- package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
- package/src/runtime/recovery/probe.ts +2 -2
- package/src/runtime/release/release-coordinator.ts +20 -4
- package/src/runtime/root/controller-progression-composition.ts +67 -5
- package/src/runtime/root/package-connector-service.ts +123 -11
- package/src/runtime/root/package-runtime-release.ts +6 -18
- package/src/runtime/root/package-runtime-service.ts +7 -13
- package/src/runtime/root/release-execution-canary.ts +18 -5
- package/src/runtime/root/release-manifest.ts +0 -34
- package/src/runtime/root/release-materialize.ts +0 -78
- package/src/runtime/root/runtime.ts +1 -2
- package/src/runtime/root/scheduled-controller-composition.ts +2 -1
- package/src/runtime/root/service.ts +90 -11
- package/src/runtime/root/types.ts +0 -6
- package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
- package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
- package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
- package/src/runtime/safe-tooling/ios-development.ts +22 -8
- package/src/runtime/safe-tooling/job-summary.ts +1 -1
- package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
- package/src/runtime/shared/atomic-text-file.ts +21 -0
- package/src/runtime/shared/json-files.ts +8 -3
- package/src/runtime/standalone-recovery/core.ts +189 -174
- package/src/runtime/standalone-recovery/entry.ts +62 -30
- package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
- package/src/runtime/workflow/schedules/engine.ts +36 -14
- package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
- package/supervisor/client.ts +31 -14
- package/supervisor/control-plane.ts +210 -115
- package/supervisor/forge-validators.ts +88 -21
- package/supervisor/native-browser-adapter.ts +393 -28
- package/supervisor/native-messaging/host.ts +21 -2
- package/supervisor/protocol.ts +74 -26
- package/supervisor/server.ts +16 -8
- package/supervisor/store.ts +476 -57
- package/supervisor/types.ts +19 -13
- package/SKILL.md +0 -427
- package/adapters/computer/chatgpt-target-router.ts +0 -103
- package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
- package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
- package/adapters/mcp/toolset-selection.ts +0 -41
- package/agents/openai.yaml +0 -4
- package/assets/skill-hooks.json +0 -41
- package/assets/skill-version.json +0 -189
- package/assets/skills/claude-review/SKILL.md +0 -231
- package/assets/skills/codex-review/SKILL.md +0 -103
- package/assets/templates/helpers/check-skill-version.ts +0 -193
- package/scripts/check-skill-version.ts +0 -5
- package/scripts/ensure-codegraph.sh +0 -16
- package/scripts/run-skill-hook.ts +0 -244
- package/scripts/sync-codex-installed-copies.sh +0 -182
- package/src/cli/commands/tools.ts +0 -167
- package/src/cli/mcp/access-mode.ts +0 -2
- package/src/cli/mcp/access-tools.ts +0 -2
- package/src/cli/mcp/audit.ts +0 -2
- package/src/cli/mcp/auth.ts +0 -2
- package/src/cli/mcp/instructions.ts +0 -2
- package/src/cli/mcp/legacy-context.ts +0 -2
- package/src/cli/mcp/legacy-tool-service.ts +0 -2
- package/src/cli/mcp/multi-repository.ts +0 -2
- package/src/cli/mcp/oauth.ts +0 -2
- package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
- package/src/cli/mcp/paths.ts +0 -2
- package/src/cli/mcp/policy.ts +0 -2
- package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
- package/src/cli/mcp/proxy-env.ts +0 -2
- package/src/cli/mcp/readiness-projection.ts +0 -2
- package/src/cli/mcp/redaction.ts +0 -2
- package/src/cli/mcp/repo.ts +0 -2
- package/src/cli/mcp/repository-tools.ts +0 -2
- package/src/cli/mcp/server.ts +0 -2
- package/src/cli/mcp/setup.ts +0 -2
- package/src/cli/mcp/tool-capability-cache.ts +0 -2
- package/src/cli/mcp/tool-contract.ts +0 -2
- package/src/cli/mcp/tools.ts +0 -2
- package/src/cli/mcp/toolset-names.ts +0 -2
- package/src/cli/mcp/toolset-selection.ts +0 -2
- package/src/cli/mcp/toolset.ts +0 -2
- package/src/cli/mcp/transports/http.ts +0 -2
- package/src/cli/mcp/transports/session-registry.ts +0 -2
- package/src/cli/mcp/transports/stdio.ts +0 -2
- package/src/cli/mcp/types.ts +0 -2
- package/src/cli/mcp/umbrella-tools.ts +0 -2
- package/src/cli/tools/codegraph.ts +0 -695
- package/src/core/adoption/helper-wrapper-plan.ts +0 -159
- package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
- package/src/runtime/context/codegraph-read-provider.ts +0 -611
- package/src/runtime/context/codegraph-sidecar.cjs +0 -248
- package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
- package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
- package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
- package/src/runtime/gateway/mcp/process-tools.ts +0 -2
- package/src/runtime/gateway/mcp/router.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
- package/src/runtime/plugins/browser-provider-registry.ts +0 -2
- package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
|
@@ -33,14 +33,7 @@ import {
|
|
|
33
33
|
iosAgentDeviceStatus,
|
|
34
34
|
isIosAgentDeviceAction,
|
|
35
35
|
} from './ios-agent-device';
|
|
36
|
-
import {
|
|
37
|
-
executeIosPhysicalDeviceAction,
|
|
38
|
-
iosPhysicalDeviceActions,
|
|
39
|
-
iosPhysicalDeviceCapabilities,
|
|
40
|
-
iosPhysicalDeviceStatus,
|
|
41
|
-
isIosPhysicalDeviceAction,
|
|
42
|
-
resolveIosPhysicalDeviceAuthorizationContext,
|
|
43
|
-
} from './ios-physical-device';
|
|
36
|
+
import { iosPhysicalDeviceStatus, isIosPhysicalDeviceAction } from './ios-physical-device';
|
|
44
37
|
|
|
45
38
|
const IOS_PLUGIN_ID = 'ios';
|
|
46
39
|
|
|
@@ -99,26 +92,36 @@ function health(repoRoot: string): AssistantPluginHealth {
|
|
|
99
92
|
} as RepositoryRecord);
|
|
100
93
|
const currentPlatform = iosDevelopmentPlatform();
|
|
101
94
|
const platformOk = currentPlatform === 'darwin';
|
|
102
|
-
const
|
|
95
|
+
const developmentReady = platformOk && Boolean(xcode && 'ready' in xcode && xcode.ready);
|
|
96
|
+
const physicalDevice = iosPhysicalDeviceStatus();
|
|
97
|
+
const physicalReady = platformOk && physicalDevice.coreDeviceReady;
|
|
98
|
+
const agentDevice = iosAgentDeviceStatus({ repoRoot });
|
|
99
|
+
const semanticReady = agentDevice.available === true;
|
|
100
|
+
const pluginReady = platformOk && (developmentReady || physicalReady || semanticReady);
|
|
103
101
|
const warnings: string[] = [];
|
|
104
|
-
if (!platformOk) warnings.push('iOS
|
|
105
|
-
if (platformOk && !
|
|
106
|
-
|
|
102
|
+
if (!platformOk) warnings.push('iOS capabilities require a macOS host.');
|
|
103
|
+
if (platformOk && !developmentReady) warnings.push('Xcode/Simulator development capability is not fully ready on this host.');
|
|
104
|
+
if (platformOk && !physicalReady) warnings.push('CoreDevice physical-iPhone capability is not ready on this host.');
|
|
107
105
|
if (!discovery.ready) warnings.push('No .xcworkspace/.xcodeproj/Package.swift discovered in this repository yet.');
|
|
108
|
-
const status = userFacingStatusFrom(
|
|
106
|
+
const status = userFacingStatusFrom(developmentReady, discovery.ready, Boolean(xcode && 'simctlAvailable' in xcode && xcode.simctlAvailable), physicalReady);
|
|
109
107
|
return {
|
|
110
|
-
state: !platformOk ? 'disabled' :
|
|
108
|
+
state: !platformOk ? 'disabled' : pluginReady ? 'ready' : 'degraded',
|
|
111
109
|
checkedAt: now(),
|
|
112
|
-
ready:
|
|
113
|
-
probed: xcodeSnapshot.probed,
|
|
110
|
+
ready: pluginReady,
|
|
111
|
+
probed: xcodeSnapshot.probed || physicalDevice.probed || Boolean(agentDevice.available),
|
|
114
112
|
errors: [],
|
|
115
113
|
warnings,
|
|
116
114
|
details: {
|
|
117
|
-
provider: 'local-
|
|
115
|
+
provider: 'local-ios',
|
|
118
116
|
platform: currentPlatform,
|
|
119
117
|
userFacingStatus: status,
|
|
120
118
|
discoverable: discovery.ready,
|
|
121
119
|
defaultContainer: discovery.defaultContainer,
|
|
120
|
+
capabilities: {
|
|
121
|
+
development: developmentReady,
|
|
122
|
+
physicalDevice: physicalReady,
|
|
123
|
+
semanticAgentDevice: semanticReady,
|
|
124
|
+
},
|
|
122
125
|
xcode: xcode && 'ready' in xcode ? {
|
|
123
126
|
ready: xcode.ready,
|
|
124
127
|
xcodeSelectPath: 'xcodeSelectPath' in xcode ? xcode.xcodeSelectPath : undefined,
|
|
@@ -127,8 +130,8 @@ function health(repoRoot: string): AssistantPluginHealth {
|
|
|
127
130
|
probed: xcodeSnapshot.probed,
|
|
128
131
|
cached: xcodeSnapshot.cached,
|
|
129
132
|
} : xcode,
|
|
130
|
-
agentDevice
|
|
131
|
-
physicalDevice
|
|
133
|
+
agentDevice,
|
|
134
|
+
physicalDevice,
|
|
132
135
|
artifactRoots: {
|
|
133
136
|
repoLocal: '.forge/ios/',
|
|
134
137
|
controller: 'controller-home/repositories/<repoId>/artifacts/ios/',
|
|
@@ -137,21 +140,22 @@ function health(repoRoot: string): AssistantPluginHealth {
|
|
|
137
140
|
};
|
|
138
141
|
}
|
|
139
142
|
|
|
140
|
-
function userFacingStatusFrom(toolingReady: boolean, discoverable: boolean, simReady: boolean): string {
|
|
141
|
-
if (!toolingReady && !discoverable) return 'not_configured';
|
|
142
|
-
if (
|
|
143
|
+
function userFacingStatusFrom(toolingReady: boolean, discoverable: boolean, simReady: boolean, physicalReady: boolean): string {
|
|
144
|
+
if (!toolingReady && !physicalReady && !discoverable) return 'not_configured';
|
|
145
|
+
if (toolingReady && discoverable && simReady) return 'buildable';
|
|
146
|
+
if (physicalReady && !toolingReady) return 'physical_device_ready';
|
|
143
147
|
if (toolingReady && simReady) return 'simulator_ready';
|
|
144
148
|
if (discoverable) return 'discoverable';
|
|
145
|
-
if (
|
|
149
|
+
if (physicalReady) return 'physical_device_ready';
|
|
146
150
|
return 'not_configured';
|
|
147
151
|
}
|
|
148
152
|
|
|
149
|
-
function permissions(
|
|
153
|
+
function permissions(input: { developmentReady: boolean; physicalReady: boolean }): AssistantPluginPermissionScope[] {
|
|
150
154
|
return [
|
|
151
|
-
{ scope: 'ios.discover', mode: 'read', description: 'Discover
|
|
152
|
-
{ scope: 'ios.build', mode: 'write', description: 'Build iOS app targets into bounded DerivedData.', granted:
|
|
153
|
-
{ scope: 'ios.simulator', mode: 'write', description: 'Boot simulators, install, launch, screenshot, and collect logs.', granted:
|
|
154
|
-
{ scope: 'ios.device', mode: 'write', description: '
|
|
155
|
+
{ scope: 'ios.discover', mode: 'read', description: 'Discover iOS projects and paired physical devices.', granted: input.developmentReady || input.physicalReady, required: true },
|
|
156
|
+
{ scope: 'ios.build', mode: 'write', description: 'Build iOS app targets into bounded DerivedData.', granted: input.developmentReady, required: true },
|
|
157
|
+
{ scope: 'ios.simulator', mode: 'write', description: 'Boot simulators, install, launch, screenshot, and collect logs.', granted: input.developmentReady, required: true },
|
|
158
|
+
{ scope: 'ios.device', mode: 'write', description: 'Explicit paired-iPhone development/XCTest actions; everyday CoreDevice app interaction belongs to Computer.', granted: input.physicalReady, required: false },
|
|
155
159
|
];
|
|
156
160
|
}
|
|
157
161
|
|
|
@@ -172,7 +176,6 @@ function capabilities(): AssistantPluginCapability[] {
|
|
|
172
176
|
actions: ['smoke_review', 'build', 'xcode_test', 'launch_simulator', 'capture_screenshot'],
|
|
173
177
|
},
|
|
174
178
|
...iosAgentDeviceCapabilities(),
|
|
175
|
-
...iosPhysicalDeviceCapabilities(),
|
|
176
179
|
];
|
|
177
180
|
}
|
|
178
181
|
|
|
@@ -395,7 +398,6 @@ function actions(): AssistantPluginActionDescriptor[] {
|
|
|
395
398
|
},
|
|
396
399
|
},
|
|
397
400
|
...iosAgentDeviceActions(),
|
|
398
|
-
...iosPhysicalDeviceActions(),
|
|
399
401
|
];
|
|
400
402
|
}
|
|
401
403
|
|
|
@@ -408,7 +410,7 @@ export function buildIosPluginManifest(previousRevision = 0, previousUpdatedAt?:
|
|
|
408
410
|
revision: Math.max(1, previousRevision || 1),
|
|
409
411
|
pluginId: IOS_PLUGIN_ID,
|
|
410
412
|
provider: 'apple-local',
|
|
411
|
-
displayName: 'iOS Development
|
|
413
|
+
displayName: 'iOS Development / Simulator / XCTest',
|
|
412
414
|
pluginVersion: '1.0.0',
|
|
413
415
|
authority: {
|
|
414
416
|
strategy: 'derived',
|
|
@@ -419,13 +421,16 @@ export function buildIosPluginManifest(previousRevision = 0, previousUpdatedAt?:
|
|
|
419
421
|
lifecycle: {
|
|
420
422
|
state: !enabled ? 'disabled' : state.ready ? 'enabled' : 'degraded',
|
|
421
423
|
reason: !enabled
|
|
422
|
-
? 'iOS
|
|
424
|
+
? 'iOS capabilities are only available on macOS hosts.'
|
|
423
425
|
: state.ready
|
|
424
|
-
? '
|
|
425
|
-
: state.warnings[0] ?? 'iOS
|
|
426
|
+
? 'At least one iOS development or physical-device capability is ready.'
|
|
427
|
+
: state.warnings[0] ?? 'iOS capabilities are partially available.',
|
|
426
428
|
},
|
|
427
429
|
health: state,
|
|
428
|
-
permissions: permissions(
|
|
430
|
+
permissions: permissions({
|
|
431
|
+
developmentReady: Boolean(state.details?.capabilities && typeof state.details.capabilities === 'object' && (state.details.capabilities as Record<string, unknown>).development === true),
|
|
432
|
+
physicalReady: Boolean(state.details?.capabilities && typeof state.details.capabilities === 'object' && (state.details.capabilities as Record<string, unknown>).physicalDevice === true),
|
|
433
|
+
}),
|
|
429
434
|
capabilities: capabilities(),
|
|
430
435
|
actions: actions(),
|
|
431
436
|
updatedAt: previousUpdatedAt ?? now(),
|
|
@@ -434,7 +439,9 @@ export function buildIosPluginManifest(previousRevision = 0, previousUpdatedAt?:
|
|
|
434
439
|
|
|
435
440
|
export async function executeIosPluginAction(input: AssistantPluginActionExecutionInput): Promise<Record<string, unknown>> {
|
|
436
441
|
if (isIosAgentDeviceAction(input.actionId)) return executeIosAgentDeviceAction(input);
|
|
437
|
-
if (isIosPhysicalDeviceAction(input.actionId))
|
|
442
|
+
if (isIosPhysicalDeviceAction(input.actionId)) {
|
|
443
|
+
throw new AssistantPluginError('PLUGIN_ACTION_NOT_SUPPORTED', `Physical iPhone app actions are available only through computer/${input.actionId}.`, { retryable: false });
|
|
444
|
+
}
|
|
438
445
|
const currentPlatform = iosDevelopmentPlatform();
|
|
439
446
|
if (currentPlatform !== 'darwin' && input.actionId !== 'discover_project' && input.actionId !== 'xcode_status') {
|
|
440
447
|
throw new AssistantPluginError('PLUGIN_DEPENDENCY_MISSING', 'iOS plugin actions require macOS with Xcode/Simulator.', {
|
|
@@ -590,9 +597,7 @@ export async function executeIosPluginAction(input: AssistantPluginActionExecuti
|
|
|
590
597
|
export const iosPluginAdapter = {
|
|
591
598
|
pluginId: IOS_PLUGIN_ID,
|
|
592
599
|
buildManifest: buildIosPluginManifest,
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
return resolveIosPhysicalDeviceAuthorizationContext(input);
|
|
596
|
-
},
|
|
600
|
+
// CoreDevice actions are public only through the Computer manifest; Xcode and XCTest stay here.
|
|
601
|
+
resolveAuthorizationContext: async () => undefined,
|
|
597
602
|
executeAction: executeIosPluginAction,
|
|
598
603
|
};
|
|
@@ -8,6 +8,7 @@ import { runBoundedProcess } from '../execution/thin-harness/async-process';
|
|
|
8
8
|
import { readJsonFile, writeJsonAtomic } from '../shared/json-files';
|
|
9
9
|
import { resolveTrustedNodeExecutable } from '../shared/trusted-node-executable';
|
|
10
10
|
import { AssistantPluginError, toAssistantPluginError } from './errors';
|
|
11
|
+
import { ensureIosDeviceOwnership, releaseIosDeviceOwnership, withIosDeviceTransientOwnership } from './ios-device-ownership';
|
|
11
12
|
import {
|
|
12
13
|
interactionAutomationEngine,
|
|
13
14
|
interactionMayOwnTarget,
|
|
@@ -734,6 +735,15 @@ function providerForDevice(device: AgentDeviceEntry): InteractionProvider {
|
|
|
734
735
|
return device.kind === 'simulator' ? SIMULATOR_PROVIDER : DEVICE_PROVIDER;
|
|
735
736
|
}
|
|
736
737
|
|
|
738
|
+
async function runPhysicalDeviceCommand<T>(
|
|
739
|
+
input: AssistantPluginActionExecutionInput,
|
|
740
|
+
device: AgentDeviceEntry,
|
|
741
|
+
operation: () => Promise<T>,
|
|
742
|
+
): Promise<T> {
|
|
743
|
+
if (device.kind !== 'device') return operation();
|
|
744
|
+
return withIosDeviceTransientOwnership(input.controllerHome, input.repoId, input.requestId, device.id, operation);
|
|
745
|
+
}
|
|
746
|
+
|
|
737
747
|
function selectTarget(input: AssistantPluginActionExecutionInput, selector?: string): AgentDeviceEntry {
|
|
738
748
|
const inventory = devices(input);
|
|
739
749
|
const normalizedSelector = selector?.trim().toLocaleLowerCase('en-US');
|
|
@@ -835,12 +845,26 @@ function requireRecord(input: AssistantPluginActionExecutionInput, allowTerminal
|
|
|
835
845
|
status: 'closing',
|
|
836
846
|
error: { code: 'AGENT_DEVICE_CLEANUP_FAILED', message: 'The expired agent-device session could not be closed; ownership remains fenced.' },
|
|
837
847
|
});
|
|
848
|
+
if (record.provider === DEVICE_PROVIDER && closed) releaseIosDeviceOwnership(input.controllerHome, record.owner.repoId, interactionId, record.targetId);
|
|
838
849
|
throw new AssistantPluginError(
|
|
839
850
|
closed ? 'AGENT_DEVICE_SESSION_EXPIRED' : 'AGENT_DEVICE_CLEANUP_FAILED',
|
|
840
851
|
closed ? 'The agent-device session expired and was closed.' : 'The expired agent-device session could not be closed; retry agent_device_close.',
|
|
841
852
|
{ retryable: !closed },
|
|
842
853
|
);
|
|
843
854
|
}
|
|
855
|
+
if (record.provider === DEVICE_PROVIDER) {
|
|
856
|
+
const ownership = ensureIosDeviceOwnership(input.controllerHome, input.repoId, interactionId, record.targetId);
|
|
857
|
+
if (!ownership.acquired) {
|
|
858
|
+
throw new AssistantPluginError('PLUGIN_RESOURCE_BUSY', 'The selected iPhone is already owned by another active Forge interaction.', {
|
|
859
|
+
retryable: true,
|
|
860
|
+
details: { interactionId, targetId: record.targetId, blocker: ownership.blocker },
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
const active = patchInteractionSession(input.repoRoot, record.provider, interactionId, {
|
|
864
|
+
expiresAt: new Date(hooks.now().getTime() + SESSION_EXPIRY_MS).toISOString(),
|
|
865
|
+
});
|
|
866
|
+
if (active) return active;
|
|
867
|
+
}
|
|
844
868
|
return record;
|
|
845
869
|
}
|
|
846
870
|
|
|
@@ -915,6 +939,7 @@ function reconcileExpiredSessions(input: AssistantPluginActionExecutionInput): v
|
|
|
915
939
|
status: 'closing',
|
|
916
940
|
error: { code: 'AGENT_DEVICE_CLEANUP_FAILED', message: 'The expired agent-device session could not be closed; ownership remains fenced.' },
|
|
917
941
|
});
|
|
942
|
+
if (record.provider === DEVICE_PROVIDER && closed) releaseIosDeviceOwnership(input.controllerHome, record.owner.repoId, record.interactionId, record.targetId);
|
|
918
943
|
}
|
|
919
944
|
}
|
|
920
945
|
|
|
@@ -934,6 +959,7 @@ function failSession(input: AssistantPluginActionExecutionInput, record: Interac
|
|
|
934
959
|
status: 'closing',
|
|
935
960
|
error: { code: 'AGENT_DEVICE_CLEANUP_FAILED', message: `${normalized.message}; cleanup failed and ownership remains fenced.` },
|
|
936
961
|
});
|
|
962
|
+
if (closed && record.provider === DEVICE_PROVIDER) releaseIosDeviceOwnership(input.controllerHome, record.owner.repoId, record.interactionId, record.targetId);
|
|
937
963
|
if (!closed) {
|
|
938
964
|
throw new AssistantPluginError('AGENT_DEVICE_CLEANUP_FAILED', 'The agent-device action failed and its session could not be closed; retry agent_device_close.', {
|
|
939
965
|
retryable: true,
|
|
@@ -2258,12 +2284,12 @@ export async function executeIosAgentDeviceAction(input: AssistantPluginActionEx
|
|
|
2258
2284
|
contractFingerprint: dependency.capabilityProfile.contractFingerprint,
|
|
2259
2285
|
device: selected,
|
|
2260
2286
|
physicalDeviceSupported: selected.kind === 'device',
|
|
2261
|
-
result: bounded(await runJsonAsync(input, args, {
|
|
2287
|
+
result: bounded(await runPhysicalDeviceCommand(input, selected, () => runJsonAsync(input, args, {
|
|
2262
2288
|
signing: signingFromArgs(input.args),
|
|
2263
2289
|
stateDir: targetRuntimeStateDir(input, selected),
|
|
2264
2290
|
failureCode: 'AGENT_DEVICE_DOCTOR_FAILED',
|
|
2265
2291
|
timeoutMs: 4 * 60_000,
|
|
2266
|
-
})),
|
|
2292
|
+
}))),
|
|
2267
2293
|
};
|
|
2268
2294
|
}
|
|
2269
2295
|
|
|
@@ -2276,14 +2302,14 @@ export async function executeIosAgentDeviceAction(input: AssistantPluginActionEx
|
|
|
2276
2302
|
contractFingerprint: dependency.capabilityProfile.contractFingerprint,
|
|
2277
2303
|
device: selected,
|
|
2278
2304
|
physicalDeviceSupported: selected.kind === 'device',
|
|
2279
|
-
result: bounded(await runJsonAsync(input, [
|
|
2305
|
+
result: bounded(await runPhysicalDeviceCommand(input, selected, () => runJsonAsync(input, [
|
|
2280
2306
|
'prepare', 'ios-runner', '--platform', 'ios', '--device', selected.name, '--timeout', '600000', '--json',
|
|
2281
2307
|
], {
|
|
2282
2308
|
signing,
|
|
2283
2309
|
stateDir: targetRuntimeStateDir(input, selected),
|
|
2284
2310
|
failureCode: 'AGENT_DEVICE_PREPARE_FAILED',
|
|
2285
2311
|
timeoutMs: 10 * 60_000,
|
|
2286
|
-
})),
|
|
2312
|
+
}))),
|
|
2287
2313
|
};
|
|
2288
2314
|
}
|
|
2289
2315
|
|
|
@@ -2298,6 +2324,19 @@ export async function executeIosAgentDeviceAction(input: AssistantPluginActionEx
|
|
|
2298
2324
|
const conflict = listInteractionSessions(input.repoRoot, provider).find((entry) =>
|
|
2299
2325
|
isInteractionSessionActive(entry.status) && interactionMayOwnTarget(entry, selected.aliases));
|
|
2300
2326
|
if (conflict) {
|
|
2327
|
+
if (selected.kind === 'device' && provider === DEVICE_PROVIDER && conflict.instructions === app && input.args.relaunch !== true) {
|
|
2328
|
+
const ownership = ensureIosDeviceOwnership(input.controllerHome, input.repoId, conflict.interactionId, selected.id);
|
|
2329
|
+
if (!ownership.acquired) {
|
|
2330
|
+
throw new AssistantPluginError('PLUGIN_RESOURCE_BUSY', 'The selected iPhone is already owned by another active Forge interaction.', {
|
|
2331
|
+
retryable: true,
|
|
2332
|
+
details: { interactionId: conflict.interactionId, targetId: conflict.targetId, blocker: ownership.blocker },
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
const active = patchInteractionSession(input.repoRoot, provider, conflict.interactionId, {
|
|
2336
|
+
expiresAt: new Date(hooks.now().getTime() + SESSION_EXPIRY_MS).toISOString(),
|
|
2337
|
+
}) ?? conflict;
|
|
2338
|
+
return { provider: 'agent-device', version: dependency.detectedVersion ?? IOS_AGENT_DEVICE_VERSION, contractFingerprint: dependency.capabilityProfile.contractFingerprint, interaction: active, device: selected, physicalDeviceSupported: true, reusedSession: true };
|
|
2339
|
+
}
|
|
2301
2340
|
throw new AssistantPluginError('PLUGIN_RESOURCE_BUSY', 'The selected iOS target already has an active interaction.', {
|
|
2302
2341
|
retryable: true,
|
|
2303
2342
|
details: {
|
|
@@ -2310,6 +2349,15 @@ export async function executeIosAgentDeviceAction(input: AssistantPluginActionEx
|
|
|
2310
2349
|
}
|
|
2311
2350
|
pruneInteractionSessions(input.repoRoot, provider, 100);
|
|
2312
2351
|
const interactionId = `ios_agent_device_${randomUUID()}`;
|
|
2352
|
+
if (selected.kind === 'device') {
|
|
2353
|
+
const ownership = ensureIosDeviceOwnership(input.controllerHome, input.repoId, interactionId, selected.id);
|
|
2354
|
+
if (!ownership.acquired) {
|
|
2355
|
+
throw new AssistantPluginError('PLUGIN_RESOURCE_BUSY', 'The selected iPhone is already owned by another active Forge interaction.', {
|
|
2356
|
+
retryable: true,
|
|
2357
|
+
details: { interactionId, targetId: selected.id, blocker: ownership.blocker },
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2313
2361
|
const createdAt = timestamp();
|
|
2314
2362
|
const record: InteractionSessionRecord = {
|
|
2315
2363
|
schemaVersion: 1,
|
|
@@ -2348,6 +2396,7 @@ export async function executeIosAgentDeviceAction(input: AssistantPluginActionEx
|
|
|
2348
2396
|
result: bounded(result),
|
|
2349
2397
|
};
|
|
2350
2398
|
} catch (error) {
|
|
2399
|
+
if (selected.kind === 'device') releaseIosDeviceOwnership(input.controllerHome, input.repoId, interactionId, selected.id);
|
|
2351
2400
|
return failSession(input, record, error);
|
|
2352
2401
|
}
|
|
2353
2402
|
}
|
|
@@ -2464,6 +2513,7 @@ export async function executeIosAgentDeviceAction(input: AssistantPluginActionEx
|
|
|
2464
2513
|
try {
|
|
2465
2514
|
const closedProvider = await closeProviderSession(input, current, args);
|
|
2466
2515
|
const closed = patchInteractionSession(input.repoRoot, current.provider, current.interactionId, { status: 'closed' }) ?? current;
|
|
2516
|
+
if (current.provider === DEVICE_PROVIDER) releaseIosDeviceOwnership(input.controllerHome, current.owner.repoId, current.interactionId, current.targetId);
|
|
2467
2517
|
return {
|
|
2468
2518
|
provider: 'agent-device',
|
|
2469
2519
|
interaction: closed,
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
acquireControllerLock,
|
|
3
|
+
ControllerLockContentionError,
|
|
4
|
+
readControllerLock,
|
|
5
|
+
releaseControllerLock,
|
|
6
|
+
renewControllerLock,
|
|
7
|
+
type ControllerLockRecord,
|
|
8
|
+
} from '../../cli/repositories/locks';
|
|
9
|
+
|
|
10
|
+
const IOS_DEVICE_LOCK_PREFIX = 'ios-device:';
|
|
11
|
+
const IOS_DEVICE_LOCK_TTL_MS = 2 * 60 * 60_000;
|
|
12
|
+
|
|
13
|
+
export interface IosDeviceOwnershipResult {
|
|
14
|
+
acquired: boolean;
|
|
15
|
+
lock?: ControllerLockRecord;
|
|
16
|
+
blocker?: Pick<ControllerLockRecord, 'lockId' | 'owner' | 'pid' | 'acquiredAt' | 'heartbeatAt' | 'expiresAt'>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function deviceResourceKey(deviceIdentifier: string): string {
|
|
20
|
+
const normalized = deviceIdentifier.trim().toUpperCase();
|
|
21
|
+
if (!normalized) throw new Error('IOS_DEVICE_IDENTIFIER_REQUIRED');
|
|
22
|
+
return `${IOS_DEVICE_LOCK_PREFIX}${normalized}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sessionOwner(repoId: string, interactionId: string): string {
|
|
26
|
+
const repo = repoId.trim();
|
|
27
|
+
const interaction = interactionId.trim();
|
|
28
|
+
if (!repo || !interaction) throw new Error('IOS_DEVICE_OWNERSHIP_ID_REQUIRED');
|
|
29
|
+
return `ios-device-session:${repo}:${interaction}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function blocker(lock: ControllerLockRecord) {
|
|
33
|
+
return {
|
|
34
|
+
lockId: lock.lockId,
|
|
35
|
+
owner: lock.owner,
|
|
36
|
+
pid: lock.pid,
|
|
37
|
+
acquiredAt: lock.acquiredAt,
|
|
38
|
+
heartbeatAt: lock.heartbeatAt,
|
|
39
|
+
...(lock.expiresAt ? { expiresAt: lock.expiresAt } : {}),
|
|
40
|
+
} satisfies IosDeviceOwnershipResult['blocker'];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Use the hardware UDID as the single physical iPhone identity shared with XCTest.
|
|
44
|
+
// CoreDevice identifier is a transport-local locator, not the agent-device device id.
|
|
45
|
+
export function iosCoreDeviceOwnershipId(device: { identifier: string; udid?: string }): string {
|
|
46
|
+
const udid = device.udid?.trim();
|
|
47
|
+
if (!udid) throw new Error('IOS_DEVICE_UDID_REQUIRED');
|
|
48
|
+
return udid;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ensureIosDeviceOwnership(
|
|
52
|
+
controllerHome: string,
|
|
53
|
+
repoId: string,
|
|
54
|
+
interactionId: string,
|
|
55
|
+
deviceIdentifier: string,
|
|
56
|
+
): IosDeviceOwnershipResult {
|
|
57
|
+
const key = { scope: 'global' as const, resource: deviceResourceKey(deviceIdentifier) };
|
|
58
|
+
const owner = sessionOwner(repoId, interactionId);
|
|
59
|
+
const current = readControllerLock(controllerHome, key);
|
|
60
|
+
if (current) {
|
|
61
|
+
if (current.owner !== owner) return { acquired: false, blocker: blocker(current) };
|
|
62
|
+
return { acquired: true, lock: renewControllerLock(controllerHome, key, current.lockId, IOS_DEVICE_LOCK_TTL_MS) };
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return { acquired: true, lock: acquireControllerLock(controllerHome, key, owner, IOS_DEVICE_LOCK_TTL_MS) };
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (!(error instanceof ControllerLockContentionError)) throw error;
|
|
68
|
+
const raced = readControllerLock(controllerHome, key);
|
|
69
|
+
if (!raced) return { acquired: false };
|
|
70
|
+
if (raced.owner !== owner) return { acquired: false, blocker: blocker(raced) };
|
|
71
|
+
return { acquired: true, lock: renewControllerLock(controllerHome, key, raced.lockId, IOS_DEVICE_LOCK_TTL_MS) };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function releaseIosDeviceOwnership(
|
|
76
|
+
controllerHome: string,
|
|
77
|
+
repoId: string,
|
|
78
|
+
interactionId: string,
|
|
79
|
+
deviceIdentifier: string,
|
|
80
|
+
): boolean {
|
|
81
|
+
const key = { scope: 'global' as const, resource: deviceResourceKey(deviceIdentifier) };
|
|
82
|
+
const current = readControllerLock(controllerHome, key);
|
|
83
|
+
if (!current || current.owner !== sessionOwner(repoId, interactionId)) return false;
|
|
84
|
+
releaseControllerLock(controllerHome, key, current.lockId);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function withIosDeviceTransientOwnership<T>(
|
|
89
|
+
controllerHome: string,
|
|
90
|
+
repoId: string,
|
|
91
|
+
requestId: string,
|
|
92
|
+
deviceIdentifier: string,
|
|
93
|
+
operation: () => T | Promise<T>,
|
|
94
|
+
): Promise<T> {
|
|
95
|
+
const key = { scope: 'global' as const, resource: deviceResourceKey(deviceIdentifier) };
|
|
96
|
+
const owner = `ios-device-transient:${repoId}:${requestId}`;
|
|
97
|
+
const lock = acquireControllerLock(controllerHome, key, owner, undefined);
|
|
98
|
+
try {
|
|
99
|
+
return await operation();
|
|
100
|
+
} finally {
|
|
101
|
+
releaseControllerLock(controllerHome, key, lock.lockId);
|
|
102
|
+
}
|
|
103
|
+
}
|