From d447e86742fb10dc4024520da0bfae6a168776de Mon Sep 17 00:00:00 2001 From: vypbot Date: Wed, 21 Oct 2026 14:58:56 +0200 Subject: [PATCH 1/8] docs(sdd): specify issue contract in specs/repository-verification-setup-hooks.md --- specs/CATALOG.md | 4 +- specs/catalog.json | 3 +- specs/repository-verification-setup-hooks.md | 349 +++++++++++++++++++ 3 files changed, 353 insertions(+), 3 deletions(-) create mode 100644 specs/repository-verification-setup-hooks.md diff --git a/specs/CATALOG.md b/specs/CATALOG.md index e935d9366..ec95a641c 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -24,7 +24,7 @@ debt or convert unknown historic intent into a design decision. | `managed-issue-lifecycle` | As-built baseline | Convert typed issues into traceable work branches, project state, and lifecycle state | [Managed issue and branch lifecycle](./managed-issue-and-branch-lifecycle.md) | 71 paths · 2026-10-20 | | `comment-automation` | Implemented | Admit only explicit commands or exact mentions, then route them while protecting repository mutations | [Comment automation and authorization](./comment-automation-and-authorization.md) | 79 paths · 2026-10-11 | | `bugbot-analysis-and-autofix` | Implemented | Select one canonical PR, exhaustively analyze its bounded diff partitions, publish stable findings atomically, and apply authorized verified fixes | [Bugbot analysis, finding publication, and autofix](./bugbot-analysis-publication-and-autofix.md) + 2 companion | 109 paths · 2026-10-13 | -| `branch-synchronization` | Implemented | Observe parent drift with one localized status card and transition-only notifications, then safely merge a parent branch into a linked working branch | [Branch synchronization and conflict recovery](./branch-synchronization-and-conflict-recovery.md) + 1 companion | 90 paths · 2026-10-20 | +| `branch-synchronization` | Implemented | Observe parent drift with one localized status card and transition-only notifications, then safely merge a parent branch into a linked working branch | [Branch synchronization and conflict recovery](./branch-synchronization-and-conflict-recovery.md) + 2 companion | 90 paths · 2026-10-20 | | `pull-request-lifecycle` | Implemented | Enrich linked and unlinked pull requests with safe issue linkage, projects, metadata, reviewers, concise descriptions, and distinct workflow evidence | [Pull request lifecycle and enrichment](./pull-request-lifecycle-and-enrichment.md) | 77 paths · 2026-10-20 | | `agent-runtime` | Implemented | Resolve, provision, authenticate, authorize, and execute only the agent roles reachable by a run | [Agent runtime, provider, model, and role routing](./agent-runtime-provider-and-model-routing.md) + 1 companion | 99 paths · 2026-10-17 | | `cli-and-single-actions` | As-built baseline | Expose bounded local commands and workflow-dispatched operations through the shared application core | [CLI and single-action execution](./cli-and-single-action-execution.md) | 44 paths · 2026-10-19 | @@ -192,7 +192,7 @@ debt or convert unknown historic intent into a design decision. - Owner: Copilot maintainers - Last verified: 2026-10-20 -- Specifications: [`specs/branch-synchronization-and-conflict-recovery.md`](./branch-synchronization-and-conflict-recovery.md) · [`specs/repository-verification-runtime-prerequisites.md`](./repository-verification-runtime-prerequisites.md) +- Specifications: [`specs/branch-synchronization-and-conflict-recovery.md`](./branch-synchronization-and-conflict-recovery.md) · [`specs/repository-verification-runtime-prerequisites.md`](./repository-verification-runtime-prerequisites.md) · [`specs/repository-verification-setup-hooks.md`](./repository-verification-setup-hooks.md) - Workflows: [`.github/workflows/copilot_branch_sync.yml`](../.github/workflows/copilot_branch_sync.yml) · [`setup/workflows/copilot_branch_sync.yml`](../setup/workflows/copilot_branch_sync.yml) · [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`setup/workflows/copilot_issue.yml`](../setup/workflows/copilot_issue.yml) · [`setup/workflows/copilot_issue_comment.yml`](../setup/workflows/copilot_issue_comment.yml) · [`setup/workflows/copilot_pull_request_comment.yml`](../setup/workflows/copilot_pull_request_comment.yml) - Entrypoints: [`src/application/usecases/branch_sync/sync_branch_use_case.ts`](../src/application/usecases/branch_sync/sync_branch_use_case.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) - Core code: [`src/domain/branch_sync_command.ts`](../src/domain/branch_sync_command.ts) · [`src/application/ports/branch_sync_ports.ts`](../src/application/ports/branch_sync_ports.ts) · [`src/application/policies/branch_sync_message_catalog.ts`](../src/application/policies/branch_sync_message_catalog.ts) · [`src/application/policies/branch_sync_notification_policy.ts`](../src/application/policies/branch_sync_notification_policy.ts) · [`src/application/usecases/branch_sync/branch_sync_execution_policy.ts`](../src/application/usecases/branch_sync/branch_sync_execution_policy.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/application/usecases/steps/common/transition_notification_workflow.ts`](../src/application/usecases/steps/common/transition_notification_workflow.ts) · [`src/data/repository/branch_sync/branch_dependency_policy.ts`](../src/data/repository/branch_sync/branch_dependency_policy.ts) · [`src/data/repository/branch_sync/branch_dependency_repository.ts`](../src/data/repository/branch_sync/branch_dependency_repository.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/infrastructure/composition/push_single_action_capability_port_binding.ts`](../src/infrastructure/composition/push_single_action_capability_port_binding.ts) · [`src/infrastructure/branch_sync_workspace_adapter.ts`](../src/infrastructure/branch_sync_workspace_adapter.ts) · [`src/prompts/branch_sync_conflicts.ts`](../src/prompts/branch_sync_conflicts.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`src/infrastructure/branch_sync_sdd_catalog.ts`](../src/infrastructure/branch_sync_sdd_catalog.ts) · [`src/domain/branch_sync_validation.ts`](../src/domain/branch_sync_validation.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/infrastructure/branch_sync_communication_budget.ts`](../src/infrastructure/branch_sync_communication_budget.ts) · [`src/application/usecases/actions/retire_branch_sync_use_case.ts`](../src/application/usecases/actions/retire_branch_sync_use_case.ts) · [`src/application/ports/issue_completion_ports.ts`](../src/application/ports/issue_completion_ports.ts) · [`src/application/usecases/pull_request_workflow.ts`](../src/application/usecases/pull_request_workflow.ts) · [`src/application/usecases/pull_request_workflow_context.ts`](../src/application/usecases/pull_request_workflow_context.ts) · [`src/infrastructure/composition/pull_request_use_case_composition_root.ts`](../src/infrastructure/composition/pull_request_use_case_composition_root.ts) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) · [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`scripts/validate-workflow-contract.cjs`](../scripts/validate-workflow-contract.cjs) · [`src/application/ports/reply_request_read_ports.ts`](../src/application/ports/reply_request_read_ports.ts) · [`src/domain/reply_request_digest.ts`](../src/domain/reply_request_digest.ts) · [`src/infrastructure/composition/reply_request_composition_root.ts`](../src/infrastructure/composition/reply_request_composition_root.ts) · [`src/data/repository/reply_request_repository.ts`](../src/data/repository/reply_request_repository.ts) · [`src/application/policies/verification_runtime_policy.ts`](../src/application/policies/verification_runtime_policy.ts) · [`src/infrastructure/verification_runtime_manifest.ts`](../src/infrastructure/verification_runtime_manifest.ts) · [`src/infrastructure/verification_runtime_preflight.ts`](../src/infrastructure/verification_runtime_preflight.ts) · [`src/infrastructure/untrusted_command_environment.ts`](../src/infrastructure/untrusted_command_environment.ts) · [`src/application/ports/git_ports.ts`](../src/application/ports/git_ports.ts) · [`src/application/ports/bugbot_git_ports.ts`](../src/application/ports/bugbot_git_ports.ts) · [`src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts`](../src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts) · [`src/application/usecases/steps/commit/bugbot/verify_command_runner.ts`](../src/application/usecases/steps/commit/bugbot/verify_command_runner.ts) · [`src/domain/closed_issue_reconciliation.ts`](../src/domain/closed_issue_reconciliation.ts) · [`src/application/errors/closed_issue_reconciliation_error.ts`](../src/application/errors/closed_issue_reconciliation_error.ts) diff --git a/specs/catalog.json b/specs/catalog.json index 95fed7d33..05cff3d73 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -1937,7 +1937,8 @@ "lastVerified": "2026-10-20", "specs": [ "specs/branch-synchronization-and-conflict-recovery.md", - "specs/repository-verification-runtime-prerequisites.md" + "specs/repository-verification-runtime-prerequisites.md", + "specs/repository-verification-setup-hooks.md" ], "workflows": [ ".github/workflows/copilot_branch_sync.yml", diff --git a/specs/repository-verification-setup-hooks.md b/specs/repository-verification-setup-hooks.md new file mode 100644 index 000000000..76116edc5 --- /dev/null +++ b/specs/repository-verification-setup-hooks.md @@ -0,0 +1,349 @@ +# Repository-Owned Verification Setup Hooks + +- Status: Draft +- Date: 2026-10-21 +- Catalog capability ID: branch-synchronization +- Last verified: not applicable (prospective specification) +- Owners: Copilot maintainers +- Scope: Define an optional repository-owned preparation helper for authorized operations that run repository verification, including branch synchronization and automated fixes. +- Related issues/PRs: [Issue #647](https://github.com/vypdev/copilot/issues/647); [verification runtime prerequisites](https://github.com/vypdev/copilot/blob/master/docs/security-operations/operations/verification.mdx#repository-verification-prerequisites) +- Required review gates: product UX, architecture, testing, documentation, security/operations +- Open decisions blocking readiness: None; this companion records the resolved contract for implementation review. + +## 1. Executive summary + +Consumers may add a reviewed helper at `.github/copilot/verification-setup` on their trusted main/production branch. Copilot captures an immutable snapshot and invokes it in the credential-free environment immediately before each configured verification command, using the candidate tree being checked. The helper prepares only that repository’s required runtimes and dependencies. Without an enabled helper, existing prerequisite checks and verification behavior remain in force. Any enabled-helper validation, execution, timeout, or result failure blocks verification and commit/push. + +```text +authorized operation -> candidate tree ready -> trusted helper snapshot prepares isolated verifier -> preflight and verification -> existing commit/push policy +``` + +The safety rule is that candidate changes cannot replace the trusted helper, and setup success is never reused for a different command or candidate tree. + +## 2. Problem, current behavior, and evidence + +### 2.1 Problem + +Repository maintainers need project-specific runtimes and dependencies in the same job and isolated environment that executes Copilot verification. Generated workflows belong to Copilot; editing them for each repository creates update drift. A separate successful CI job or preparation job cannot provision the verifier’s environment. Synchronization may produce a temporary merge, and an agent may change manifests, so setup must match the actual candidate checked. + +### 2.2 Current behavior + +1. Copilot’s verification contract runs configured repository commands subject to existing authorization and prerequisite checks. +2. There is no specified repository-owned helper contract wired through all verification routes. +3. In the reported `vypdev/homeassistant-gateway` run, `python3 -m pytest` failed because `/usr/bin/python3` lacked `pytest`; the temporary merge was aborted without push. This establishes that run’s missing dependency, not a general runner or repository-access failure. + +### 2.3 Evidence + +- Code/workflow/documentation references: the catalog lists `.github/workflows/copilot_branch_sync.yml`, setup workflow templates for issue, issue-comment, pull-request-comment and branch-sync, and verification policy/preflight and shared commit verification code. These paths are current evidence, not a promise about future execution support. +- Incident: [consumer issue #19](https://github.com/vypdev/homeassistant-gateway/issues/19), [command comment](https://github.com/vypdev/homeassistant-gateway/issues/19#issuecomment-6311691282), [failed run](https://github.com/vypdev/homeassistant-gateway/actions/runs/39570398824), [job](https://github.com/vypdev/homeassistant-gateway/actions/runs/39570398824/job/119122814347). +- External primary sources: none required for this contract. +- Unknowns: implementation-specific workflow syntax and helper result representation must be settled consistently with repository conventions; the fixed observable limits below are resolved. + +### 2.4 Retrospective classification (as-built baselines only) + +Not applicable: this is a prospective specification and reconstructs no implemented baseline. + +## 3. Actors, surfaces, and terminology + +| Actor | Goal | Entry point | Visible surfaces | +|---|---|---|---| +| Consumer maintainer | Prepare project tools without editing generated workflows | Trusted branch helper file | Repository, verification logs, documentation | +| Issue/PR/commit actor | Request authorized implementation or autofix | Existing issue, comment, review, or commit route | Issue/PR comments and run result | +| Operator | Understand blocked verification and recovery | Workflow execution | Job summary and bounded logs | +| Copilot | Run setup and verification safely | Sync or shared commit-verification boundary | Check/run status | + +“Trusted helper snapshot” means immutable regular-file bytes and SHA captured from the configured trusted main/production branch at composition/admission. “Candidate tree” means the exact workspace revision, including temporary synchronization merge or agent edits, whose manifests verification will check. “Preparation” means one helper invocation immediately before a single configured verification command and its runtime preflight. + +## 4. Goals + +### 4.1 Goals + +1. Provide one documented optional repository-owned setup hook, invoked consistently for every authorized route that actually runs verification. +2. Prepare dependencies for the exact candidate tree in the same isolated execution environment as preflight and verification. +3. Preserve generated workflow ownership, verification policy, authorization and fail-closed commit/push behavior. + +### 4.2 Non-goals + +1. Globally provisioning consumer dependencies on shared runners or editing generated event workflows per consumer. +2. Weakening verification, removing project checks, automatic setup retries, or claiming a universal OS sandbox for maintainer code. +3. Releasing Copilot as part of this feature. + +### 4.3 Fixed product/safety invariants + +1. The helper is optional; absence preserves compatible existing behavior and prerequisite diagnostics. +2. An enabled but missing or invalid helper fails closed when verification is requested. +3. Only the trusted snapshot executes; candidate/model edits cannot replace it. +4. Setup failure, timeout, invalid output, or failed verification prevents commit/push. Sync uses existing abort/recovery behavior. +5. No setup runs for read-only queries, dry runs, aligned/no-op branches, or empty verification command lists. +6. No PAT, model/cloud credential, persisted Git write credential, agent auth-store variable/path, or agent credential profile is available to the helper. + +## 5. Current versus proposed product journey + +| Stage | Current | Proposed | User/operator effect | +|---|---|---|---| +| Configure | No stable managed hook contract | Add or update helper on trusted branch | Consumer owns setup implementation; Copilot owns generated workflows | +| Admit operation | Existing route authorization | Capture helper bytes/SHA before workspace mutation | Provenance is inspectable and candidate cannot substitute code | +| Prepare candidate | Project tools may be absent | Invoke snapshot after merge/fix changes, per command | Dependencies match current manifests | +| Verify | Existing preflight and command policy | Same policy, after setup and preflight | Existing checks remain authoritative | +| Failure | Existing verification failure behavior | Sanitized setup stage/cause; no commit/push | Clear recovery without exposing helper output | + +```mermaid +flowchart LR + A[Authorized route] --> B[Capture trusted helper SHA] + B --> C[Merge or agent candidate changes] + C --> D[Clean isolated environment] + D --> E[Run helper for this candidate and command] + E --> F[Runtime preflight and verification] + F --> G[Existing commit/push gate] + E -->|failure or timeout| H[Block commit/push; existing recovery] +``` + +Text equivalent: authorized route captures trusted helper, prepares the changed candidate in isolation, verifies it, then applies the existing commit/push gate; setup failure blocks that gate. + +## 6. Functional behavior and state model + +### 6.1 Happy path + +1. Admission preserves current actor-modification authorization, same-repository/native-head/workspace checks and fork rejection. +2. Before any agent workspace mutation, composition captures the regular-file helper from the configured trusted main/production branch SHA and retains its bytes/SHA privately. +3. Only when a route reaches actual verification does Copilot execute the snapshot from the candidate cwd after merge/fix changes. +4. Each configured command gets a fresh helper invocation, then runtime preflight and that command in the same isolated environment. Setup readiness is not reused. +5. On success, existing verification and commit/push rules continue unchanged. + +### 6.2 Alternative paths + +- No helper present and hook disabled: retain existing prerequisites and compatible behavior. +- Enabled helper absent, malformed, unsafe, or wrong type: fail closed when verification is requested. +- Snapshot acquisition failure on a route that never verifies: defer the error; read-only acquisition does not cause setup execution. +- Setup command failure or timeout: stop before verifier and commit/push; synchronization aborts through existing recovery. +- Empty command list, no-op/aligned branch, dry run or read-only request: do not invoke setup. + +### 6.3 State machine + +| State | Entered when | User-visible meaning | Allowed next states | Recovery/owner | +|---|---|---|---|---| +| Not requested | Route does not verify | No project preparation needed | Complete | Existing route owner | +| Snapshot captured | Trusted regular file pinned | Helper provenance fixed | Preparing, failed on later validation | Composition boundary | +| Preparing | Verification command is about to run | Project tools are being prepared | Ready, blocked | Setup runner | +| Ready | Setup and output validation succeed | This candidate/command may be checked | Verifying | Verification orchestration | +| Blocked | Setup/preflight/verification fails | No commit/push will occur | Recovered by existing operation flow | Operator/operation owner | +| Complete | Existing operation finishes | Existing result is final | Terminal | Existing route owner | + +Duplicate events follow existing route idempotency. A new candidate or command is a new preparation; stale readiness cannot be reused. Cancellation terminates setup and prevents downstream verification/commit. Partial synchronization remains governed by existing merge recovery. + +## 7. User-facing configuration + +| Input | Type | Recommended default | Allowed values/range | Scope/persistence | +|---|---|---|---|---| +| `.github/copilot/verification-setup` | Optional repository file | Absent/disabled | Regular executable helper in the trusted branch snapshot; fixed path and invocation contract | Repository-owned; snapshot SHA/bytes per admitted operation | +| Helper timeout | Fixed duration | 300 seconds | Not configurable | Per invocation | +| Captured output ceiling | Fixed limit | 64 KiB | Not configurable | Per invocation | +| Result file ceiling | Fixed limit | 16 KiB | Not configurable | Per invocation | + +The helper contract is a bounded process invocation with candidate cwd and a narrow documented input/output interface. It MAY install tools and dependencies needed by that candidate, using declared runtime versions and lockfiles. It MUST NOT require shell interpolation. Node execution uses argument arrays. Provision pnpm explicitly; implicit Corepack downloads are not a supported prerequisite. Helper-owned workspace caches must validate lockfiles. Private-profile tools may be recreated for each command. + +Only approved output paths are accepted: existing directories contained in the workspace, private profile, or trusted tool root. Reject unknown environment fields, unsafe links, and paths. Invalid cross-field/output combinations fail closed. Helper stdout/stderr is never emitted raw; diagnostics expose bounded stage, cause, pinned path/SHA and exit/status facts only. + +Recommended setup: use the repository’s declared Python version and lockfile to install test dependencies, and the declared Node/pnpm versions and lockfile for frontend gates. A meaningful alternative is a JVM toolchain and its project lock/dependency declaration. No arbitrary ref, URL, secret, timeout, or environment override is configurable. Updating Copilot preserves consumer helper contents; first adoption requires review onto the trusted source branch before activation. + +## 8. Clean Architecture + +### 8.1 Responsibilities and dependency direction + +| Layer/boundary | Owns | Must not own/import | +|---|---|---| +| Domain/pure policy | Whether verification is requested; fixed limits and eligibility rules | GitHub SDK, process APIs | +| Application | Snapshot lifecycle and per-command setup/verify ordering | Concrete process/provider DTOs | +| Adapters/data | Trusted branch file retrieval and immutable bytes/SHA mapping | Authorization or product policy | +| Infrastructure/composition | Isolated environment creation, process execution, cleanup and route wiring | Decisions about whether a route is authorized | +| Entrypoints | Convert issue/PR/comment/review/commit/sync events to use-case input | Duplicated verification orchestration | +| Presentation | Sanitized status and diagnostic model | Mutations, secret handling, retry policy | + +```mermaid +flowchart LR + E[Workflow and CLI entrypoints] --> A[Application verification orchestration] + A --> D[Pure eligibility and limits policy] + A --> P[Snapshot and isolated process ports] + I[Git and process adapters] --> P + A --> V[Sanitized result model] + V --> R[Job summary and existing status renderer] +``` + +### 8.2 Contracts, state, and trust boundaries + +- Pure decisions: operation requires verification, helper enabled/valid, and fixed limits; no-op/read-only exclusions. +- Application contracts: immutable trusted snapshot metadata, candidate identity, command identity, bounded setup result, and verification outcome. +- Semantic ports: read trusted regular file at pinned ref; execute bounded helper in isolated environment; clean environment; report sanitized outcome. +- Durable state/schema ownership: no new durable user state required; snapshot bytes/SHA remain private to the operation and are not sourced again from candidate files. +- Concurrency/idempotency strategy: per-operation snapshot, per-candidate/per-command invocation; no shared readiness cache and no retry. +- Trusted and untrusted inputs: trusted source branch snapshot is executable; event payloads, candidate tree, helper output and model edits are untrusted. Existing authorization and fork checks remain mandatory. +- Provider error mapping: distinguish acquisition, validation, setup exit, timeout, result validation, preflight and verification; expose bounded sanitized cause only. + +### 8.3 Executable architecture constraints + +Add contract checks proving source and distributed workflow templates route all verification paths through the shared preparation boundary, setup variable planning is consistent, and entrypoints do not duplicate process orchestration. Tests must structurally parse workflows/contracts rather than grep prose. Enforce application-to-semantic-port dependency direction and prevent domain imports of provider/process packages using the repository’s architecture checks. + +## 9. UI/UX and content contract + +### 9.1 Information hierarchy + +Show current verification status, completed setup/verification facts, next transition, whether action is required, commit/push impact, and links before technical details. Setup output and secrets never appear in user-facing content. + +### 9.2 Representative primary view + +Blocked before irreversible effects: + +```markdown +# Verification could not continue + +> **Current status:** Repository setup failed before verification. +> +> **Action required:** Review `.github/copilot/verification-setup` on the trusted branch and rerun the operation. + +## Progress + +- [x] Candidate workspace prepared +- [ ] Project verification not run +- [ ] Commit and push withheld + +## What happens next + +Correct the setup helper or its declared dependencies, then retry the authorized operation. + +
+Technical details + +Setup stage: dependency preparation. Trusted helper SHA: ``. Exit status: ``. + +
+``` + +### 9.3 Issue, PR, and comment behavior + +Use existing operation titles, durable markers, update behavior and notification budget. Setup is a step within verification, not a new issue/comment lifecycle. Existing issue/PR/comment/review/commit and sync surfaces link to their run. No new labels or checks are required; existing checks remain supplemental state. A blocked result must never imply a commit/push occurred. + +### 9.4 Accessibility, localization, and responsive behavior + +Use the existing supported locale source and fallback; keep helper path and stage names stable while surrounding prose follows existing localization. The textual flow above is the diagram equivalent. Use concise headings, descriptive links, narrow-screen readable tables, theme-independent status text, and sanitize untrusted helper content, Markdown, mentions, commands, markers and URLs. Do not render raw helper output. + +## 10. Failure, recovery, and cleanup + +| Failure/partial state | User impact | Retained facts | Automatic retry | Required action | Cleanup | +|---|---|---|---|---|---| +| Enabled helper missing/invalid | Verification blocked | Trusted SHA/path and validation stage | No | Fix helper on trusted branch; rerun | Remove private snapshot after operation | +| Snapshot acquisition failure | If verification requested, operation blocks | Acquisition stage and route identity | No | Inspect trusted branch access/configuration | Discard partial snapshot | +| Helper exits nonzero | No verifier or commit/push | Exit status and pinned SHA | No | Fix helper/dependency declaration | Clean private profile and temporary outputs | +| Timeout at 300 seconds | No verifier or commit/push | Timeout stage and pinned SHA | No | Reduce setup work or correct lock/install behavior | Terminate process; clean outputs | +| Invalid/oversized output or unsafe path | No verifier or commit/push | Validation cause, bounded status | No | Correct helper contract | Remove rejected result and private state | +| Verification fails after setup | Existing verification failure | Setup succeeded for exact candidate/command | No new retry | Fix candidate/project checks | Existing operation cleanup; sync recovery | +| Temporary merge cannot verify | Sync does not push | Merge/recovery facts | Existing behavior only | Follow sync recovery guidance | Existing merge abort/cleanup | + +Errors follow impact -> cause -> action -> retained state. No automatic retry is added. Setup process cleanup and credential-profile cleanup occur on success, failure, timeout and cancellation. + +## 11. Security, permissions, and privacy + +1. Preserve actor modification authorization, same-repository/native-head/workspace checks and fork rejection on every admitted route. +2. Capture immutable regular-file bytes and SHA from the configured trusted main/production branch before workspace mutation. Candidate/model changes cannot replace it. New helper adoption requires review on that trusted source branch. +3. Before setup, clean agent runtime credential profiles and use a fresh private HOME plus the existing allowlisted verification environment. Exclude PAT, model/cloud credentials, persisted Git write credentials, agent auth-store variables and paths. +4. Reject unknown environment fields, unsafe links and output paths outside existing approved workspace/private-profile/trusted-tool-root directories. Bound time and output as specified. Do not claim a universal OS sandbox against malicious maintainer code; retain the trusted-runner boundary. +5. Never log raw helper output, secrets, or credential-bearing environment. Diagnostics identify stage/cause, fixed path, pinned SHA and bounded exit/status only. + +## 12. Observability and operational UX + +- User-facing state: whether setup was skipped, prepared, or blocked; whether verification and commit/push ran. +- Job Summary/operator evidence: operation correlation, route, candidate identity, helper pinned SHA, stage and bounded outcome; no raw output. +- Logs, metrics, and correlation: structured stage timing and result, correlated with existing run/operation identifiers; do not record environment values. +- Pending external dependency versus workflow failure: setup timeout/exit is a workflow verification failure; trusted source acquisition is reported separately. +- Rate-limit/retry behavior: no setup retry; existing provider retry policies remain unchanged. +- Noise/comment budget: update existing operation status; do not add per-command comments. + +## 13. Compatibility, migration, rollout, and rollback + +- Existing in-flight operations retain their captured policy and do not acquire a helper mid-operation. +- No schema migration is required. Existing repositories without a helper retain compatible behavior and prerequisite checks. +- The helper is opt-in at its fixed path; invalid enabled configuration fails closed. Generated workflow updates must preserve the consumer-owned helper. +- Roll out through setup CLI/template wiring and documentation, then verify controlled local same-managed-branch synchronization. Do not launch a release as part of this work. +- Rollback removes hook invocation while preserving existing verification policy. Never undo a completed external commit/push as part of rollback; this design blocks those effects on failure. + +## 14. Testing strategy and numeric budget + +Minimum distinct cases, risk-derived from trust, freshness, route coverage, and irreversible commit/push gating: + +| Area | Minimum distinct cases | Behaviors/risks covered | +|---|---:|---| +| Domain/configuration/pure planning | 6 | Enabled/absent/invalid helper; verify eligibility; no-op exclusions; fixed limits | +| State/application/idempotency/races | 6 | Snapshot timing; candidate changes; per-command refresh; cancellation; duplicate route; no readiness reuse | +| Adapters/provider contracts | 4 | Regular-file snapshot/SHA; acquisition failure; process arguments; bounded result mapping | +| Workflows/setup | 8 | Issue, PR, general/review comments, commit, autofix and sync boundaries; source/template parity; update idempotency | +| UI/UX/localization/sanitization | 4 | Blocked and success states; links/action; localization fallback; output sanitization | +| Integration/security/migration | 8 | Python pytest prepared/unprepared; Node/pnpm; changed manifests; credential isolation; unsafe path; timeout/failure; no-helper compatibility; controlled sync | +| **Total** | **36** | Distinct cases; no double counting | + +Keep repository-wide coverage thresholds in force and meet them in changed modules. New pure eligibility/limits policy requires at least 90% branch coverage. Use table-driven distinct behavior rows, deterministic clocks/IDs/process fakes, no real waits or live services, structural workflow contract checks, and semantic UI assertions alongside golden fixtures. Manual UX evidence must show readable blocked/success results on a narrow view and inspectable run navigation. Controlled acceptance must demonstrate the original branch-sync verification with the same managed branch and no manual generated-workflow edits; no release. + +## 15. Documentation and discoverability + +| Audience | Artifact/page | Required content | Validation/navigation | +|---|---|---|---| +| User | `docs/security-operations/operations/verification.mdx` | Normal journey, optional helper, Python/Node examples, defaults and no-helper behavior | Link and example contract check | +| Setup owner | Verification guide section | Fixed path/interface, trusted branch, runtimes/lockfiles, pnpm provisioning, approved outputs | Fixture-backed contract test | +| Operator | `docs/security-operations/operations/troubleshooting.mdx` | Failure stages, sanitized diagnostics, recovery, timeout and cleanup | Navigation/link validation | +| Contributor | `docs/development/architecture.mdx` and companion SDD | Ports, snapshot trust, route boundary and workflow checks | Architecture/workflow contract checks | + +Document credential boundaries, update preservation, migration/rollback, and the fact that a separate CI job does not provision the verifier. Keep examples synchronized with helper contract fixtures. + +## 16. Acceptance + +1. Given an authorized verification operation and a valid trusted helper, when a candidate is ready, then the pinned helper runs in the isolated verifier environment before preflight and the configured command. +2. Given a temporary sync merge or changed manifests, when each verification command starts, then setup refreshes against that exact candidate and does not reuse prior readiness. +3. Given no helper and the hook is disabled, when verification runs, then existing prerequisite behavior remains compatible; given an enabled missing/invalid helper, verification fails closed. +4. Given setup exits unsuccessfully, exceeds 300 seconds, or emits invalid/oversized output, when the operation reaches verification, then verification and commit/push are withheld and the diagnostic is sanitized and actionable. +5. Given setup or verification failure during synchronization, when recovery runs, then the temporary merge follows existing abort/recovery behavior and no push occurs. +6. Given a fork or untrusted event, when admission is evaluated, then helper execution is rejected and no credentials or agent auth-store state is exposed. +7. Given a read-only query, dry run, aligned/no-op branch, or empty command list, when the route completes, then setup is not invoked. +8. Given a consumer updates Copilot templates, when setup CLI/templates are refreshed, then the consumer helper contents remain intact and all verification routes reach the shared boundary. +9. Given the `homeassistant-gateway` Python command, when the trusted helper installs declared locked test dependencies, then pytest preflight and configured verification can run; without preparation the missing dependency blocks commit/push. +10. Given controlled same-managed-branch synchronization, when the candidate is merged and verified, then the run succeeds without manual edits to generated Copilot workflows. No release is part of acceptance. + +## 17. Requirements traceability + +| Requirement | Policy/use case/adapter/presentation | Test or evidence | Documentation | +|---|---|---|---| +| Optional hook and no-op exclusions | Eligibility policy and shared verification orchestration | Pure planning and route tests | Verification guide | +| Trusted immutable snapshot | Composition and snapshot port | SHA/source mutation and fork tests | Setup/security guidance | +| Per-candidate preparation | Verification orchestration | Changed-manifest and sync merge cases | Helper contract | +| Credential/output isolation | Isolated process adapter | Credential, path and sanitization tests | Security and troubleshooting | +| Fixed failure limits and commit gate | Application result policy | Timeout/failure/no-push cases | Troubleshooting | +| All routes and template preservation | Workflow/setup variable planning | Structural route parity/idempotency checks | Contributor guide | +| Understandable status | Sanitized presentation model | Semantic UI/localization fixtures and manual UX evidence | Operator guide | + +## 18. Implementation sequence + +1. Define helper interface, fixed path, snapshot/result contracts and architecture checks. +2. Implement pure eligibility, limit and output-validation policies with focused tests. +3. Add application snapshot lifecycle and per-command setup/verification orchestration. +4. Add trusted-file and isolated-process adapters with credential cleanup and bounded execution. +5. Wire sync and shared commit-verification boundaries across source and distributed templates and setup CLI planning. +6. Add sanitized status, fixtures, documentation, troubleshooting, and update-preservation behavior. +7. Complete route/security/integration coverage and controlled local synchronization acceptance; validate specs/catalog artifacts. No release. + +## 19. Definition of Done + +- [ ] Every normative requirement maps to acceptance and traceability. +- [ ] Helper path, invocation, limits, trusted SHA and output contract are implemented and validated. +- [ ] All actual verification routes use the shared hook; read-only/no-op/dry-run paths do not. +- [ ] Snapshot, candidate freshness, credentials, unsafe paths, timeout, output bounds and fail-closed commit gate pass. +- [ ] Numeric test budget of at least 36 distinct cases and coverage requirements pass. +- [ ] UI states, sanitized diagnostics, navigation, accessibility and localization are reviewed. +- [ ] User, setup-owner, operator and contributor documentation is discoverable and examples match fixtures. +- [ ] Setup/update preserves consumer helper contents; generated workflow contract checks pass. +- [ ] Controlled same-managed-branch sync acceptance passes without manual workflow edits; no release is launched. +- [ ] `specs/catalog.json` evidence and `specs/CATALOG.md` are current and `pnpm run validate:specifications` passes. +- [ ] No readiness-blocking decision remains unresolved. + +## 20. References and decisions + +- Primary sources: [Issue #647](https://github.com/vypdev/copilot/issues/647); [repository verification prerequisites](https://github.com/vypdev/copilot/blob/master/docs/security-operations/operations/verification.mdx#repository-verification-prerequisites); [consumer issue #19](https://github.com/vypdev/homeassistant-gateway/issues/19); [failed run](https://github.com/vypdev/homeassistant-gateway/actions/runs/39570398824). +- Related specs/ADRs/issues/PRs: owning specifications are `specs/branch-synchronization-and-conflict-recovery.md` and `specs/repository-verification-runtime-prerequisites.md`; this companion is `specs/repository-verification-setup-hooks.md` and does not replace either owner. +- Decisions and rejected alternatives: fixed optional repository helper and shared managed invocation boundary; rejected editing generated workflows per consumer, global dependency installation, separate preparation jobs, weakening verification, automatic retries, and contributor-created helper activation. +- Follow-up work explicitly outside this spec: implementation admission, Action-managed branch, PR review, release, and any release operation. \ No newline at end of file From 45335e87408769cb0924cc4876c6a10ad37f4837 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 21 Oct 2026 15:36:18 +0200 Subject: [PATCH 2/8] feat: prepare repository verification with trusted setup hooks --- .copilot/AGENT_GUIDE.md | 2 + .copilot/setup-manifest.json | 2 +- .github/workflows/copilot_commit.yml | 1 + .github/workflows/copilot_issue.yml | 1 + .github/workflows/copilot_issue_comment.yml | 1 + .github/workflows/copilot_pull_request.yml | 1 + .../copilot_pull_request_comment.yml | 1 + .../copilot_pull_request_review_state.yml | 1 + action.yml | 3 + build/cli/index.js | 344 +++++++++++++++++- build/github_action/index.js | 339 ++++++++++++++++- docs/configuration.mdx | 1 + docs/development/architecture.mdx | 18 + .../operations/troubleshooting.mdx | 24 ++ .../operations/verification.mdx | 122 ++++++- scripts/coverage-budgets.json | 24 ++ scripts/validate-workflow-contract.cjs | 4 +- setup/workflows/copilot_commit.yml | 2 + setup/workflows/copilot_issue.yml | 2 + setup/workflows/copilot_issue_comment.yml | 2 + setup/workflows/copilot_pull_request.yml | 2 + .../copilot_pull_request_comment.yml | 2 + .../copilot_pull_request_review_state.yml | 2 + specs/CATALOG.md | 14 +- ...h-synchronization-and-conflict-recovery.md | 4 + specs/catalog.json | 23 +- ...tory-verification-runtime-prerequisites.md | 4 + specs/repository-verification-setup-hooks.md | 75 ++-- src/actions/github_action_execution.ts | 1 + .../local_action_configuration_sections.ts | 1 + src/actions/local_action_execution.ts | 3 +- src/application/contracts/input_keys.ts | 1 + .../verification_setup_policy.test.ts | 39 ++ .../repository_agent_guidance_policy.ts | 2 + .../policies/setup_configuration_defaults.ts | 1 + .../policies/setup_configuration_plan.ts | 2 + .../setup_configuration_validation.ts | 3 + .../setup_organization_variable_bindings.ts | 1 + .../policies/verification_setup_policy.ts | 44 +++ .../ports/verification_setup_ports.ts | 9 + .../commit/bugbot/verify_command_runner.ts | 3 +- src/cli/setup_config_file.ts | 2 +- src/data/model/execution.ts | 2 + src/data/model/execution_components.ts | 1 + src/domain/setup.ts | 2 + .../verification_setup_configuration.test.ts | 64 ++++ .../verification_setup_runner.test.ts | 189 ++++++++++ .../verification_setup_snapshot.test.ts | 100 +++++ .../__tests__/verification_setup_sync.test.ts | 104 ++++++ .../main_run_route_composition_root.test.ts | 16 + .../main_run_route_composition_root.ts | 14 +- src/infrastructure/git_commit_adapter.ts | 8 +- .../untrusted_command_environment.ts | 4 +- .../verification_setup_process.ts | 39 ++ .../verification_setup_result.ts | 42 +++ .../verification_setup_runner.ts | 32 ++ .../verification_setup_snapshot.ts | 46 +++ .../validate_workflow_contract.test.ts | 2 +- src/utils/__tests__/setup_files.test.ts | 5 +- 59 files changed, 1727 insertions(+), 76 deletions(-) create mode 100644 src/application/policies/__tests__/verification_setup_policy.test.ts create mode 100644 src/application/policies/verification_setup_policy.ts create mode 100644 src/application/ports/verification_setup_ports.ts create mode 100644 src/infrastructure/__tests__/verification_setup_configuration.test.ts create mode 100644 src/infrastructure/__tests__/verification_setup_runner.test.ts create mode 100644 src/infrastructure/__tests__/verification_setup_snapshot.test.ts create mode 100644 src/infrastructure/__tests__/verification_setup_sync.test.ts create mode 100644 src/infrastructure/verification_setup_process.ts create mode 100644 src/infrastructure/verification_setup_result.ts create mode 100644 src/infrastructure/verification_setup_runner.ts create mode 100644 src/infrastructure/verification_setup_snapshot.ts diff --git a/.copilot/AGENT_GUIDE.md b/.copilot/AGENT_GUIDE.md index a963ae107..8b5d3e477 100644 --- a/.copilot/AGENT_GUIDE.md +++ b/.copilot/AGENT_GUIDE.md @@ -51,4 +51,6 @@ The setup runner profile selects placement; it does not install the project's la Declare reviewed version prefixes in `.copilot/verification-prerequisites.json` (schemaVersion 1, runtimes such as java 17, node 24, pnpm 10). Copilot checks declared versions in the credential-free verification environment before each configured verification command; a missing or incompatible runtime blocks publication. Without this optional declaration, existing verification behavior remains and no language is inferred. Java provisioning may use `actions/setup-java`; `JAVA_HOME` and `PATH` survive isolation, while agent credentials and tool-option injection variables do not. Do not add an unreviewed bootstrap or weaken verification to work around a missing runtime. Read [verification prerequisites and same-job setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites). +The optional repository-owned setup helper prepares each candidate before verification without editing generated workflows. Review `.copilot/verification-setup.mjs` onto trusted main before enabling `VERIFICATION_SETUP=true` (Action input `verification-setup`, disabled by default). Copilot executes captured trusted bytes before each command in a fresh credential-free profile; candidate changes cannot replace them. Setup failure blocks verification and commit/push. Read [repository-owned setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup). + For the full contributor flow, read [repository collaboration](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-collaboration.mdx). For drift and recovery, read [guidance troubleshooting](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-guidance-troubleshooting.mdx). diff --git a/.copilot/setup-manifest.json b/.copilot/setup-manifest.json index b94212fbe..d15350be3 100644 --- a/.copilot/setup-manifest.json +++ b/.copilot/setup-manifest.json @@ -12,7 +12,7 @@ }, ".copilot/AGENT_GUIDE.md": { "role": "guide", - "sha256": "07ef187d584f4da868a6cebe6b6d7c7654b057af703e0d10ede11f768ba64674" + "sha256": "26fad3d7f671c3f839a7b62195c0caae5119631d2e0ef848143998c3cc2b7433" }, ".copilot/repository-profile.json": { "role": "profile", diff --git a/.github/workflows/copilot_commit.yml b/.github/workflows/copilot_commit.yml index f1a263b89..dbdfdd087 100644 --- a/.github/workflows/copilot_commit.yml +++ b/.github/workflows/copilot_commit.yml @@ -51,6 +51,7 @@ jobs: issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }} issue-managed-branches: true pre-branch-sdd: true + verification-setup: ${{ vars.VERIFICATION_SETUP || 'false' }} debug: ${{ vars.DEBUG }} bugbot-severity: ${{ vars.BUGBOT_SEVERITY || 'low' }} bugbot-comment-limit: ${{ vars.BUGBOT_COMMENT_LIMIT || '20' }} diff --git a/.github/workflows/copilot_issue.yml b/.github/workflows/copilot_issue.yml index 789458199..beef7c2a3 100644 --- a/.github/workflows/copilot_issue.yml +++ b/.github/workflows/copilot_issue.yml @@ -26,6 +26,7 @@ jobs: issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }} issue-managed-branches: true pre-branch-sdd: true + verification-setup: ${{ vars.VERIFICATION_SETUP || 'false' }} ai-ignore-files: build/*,specs/CATALOG.md bugbot-dry-run: ${{ vars.BUGBOT_DRY_RUN || 'false' }} bugbot-effort: ${{ vars.BUGBOT_EFFORT || 'smart' }} diff --git a/.github/workflows/copilot_issue_comment.yml b/.github/workflows/copilot_issue_comment.yml index 8fde2a2dc..d02893b7d 100644 --- a/.github/workflows/copilot_issue_comment.yml +++ b/.github/workflows/copilot_issue_comment.yml @@ -50,6 +50,7 @@ jobs: issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }} issue-managed-branches: true pre-branch-sdd: true + verification-setup: ${{ vars.VERIFICATION_SETUP || 'false' }} ai-ignore-files: build/*,specs/CATALOG.md debug: ${{ vars.DEBUG }} agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} diff --git a/.github/workflows/copilot_pull_request.yml b/.github/workflows/copilot_pull_request.yml index d63827d47..928453a51 100644 --- a/.github/workflows/copilot_pull_request.yml +++ b/.github/workflows/copilot_pull_request.yml @@ -48,6 +48,7 @@ jobs: issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }} issue-managed-branches: true pre-branch-sdd: true + verification-setup: ${{ vars.VERIFICATION_SETUP || 'false' }} ai-ignore-files: build/*,specs/CATALOG.md bugbot-severity: ${{ vars.BUGBOT_SEVERITY || 'low' }} bugbot-comment-limit: ${{ vars.BUGBOT_COMMENT_LIMIT || '20' }} diff --git a/.github/workflows/copilot_pull_request_comment.yml b/.github/workflows/copilot_pull_request_comment.yml index 0b2fdd06d..a81024a92 100644 --- a/.github/workflows/copilot_pull_request_comment.yml +++ b/.github/workflows/copilot_pull_request_comment.yml @@ -50,6 +50,7 @@ jobs: issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }} issue-managed-branches: true pre-branch-sdd: true + verification-setup: ${{ vars.VERIFICATION_SETUP || 'false' }} ai-ignore-files: build/*,specs/CATALOG.md debug: ${{ vars.DEBUG }} agent-provider: ${{ vars.AGENT_PROVIDER || 'codex' }} diff --git a/.github/workflows/copilot_pull_request_review_state.yml b/.github/workflows/copilot_pull_request_review_state.yml index 4d77d0bda..eb5879069 100644 --- a/.github/workflows/copilot_pull_request_review_state.yml +++ b/.github/workflows/copilot_pull_request_review_state.yml @@ -33,6 +33,7 @@ jobs: issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }} issue-managed-branches: true pre-branch-sdd: true + verification-setup: ${{ vars.VERIFICATION_SETUP || 'false' }} ai-ignore-files: build/*,specs/CATALOG.md bugbot-severity: ${{ vars.BUGBOT_SEVERITY || 'low' }} bugbot-comment-limit: ${{ vars.BUGBOT_COMMENT_LIMIT || '20' }} diff --git a/action.yml b/action.yml index 8c30c4230..ce75110fc 100644 --- a/action.yml +++ b/action.yml @@ -53,6 +53,9 @@ inputs: pre-branch-sdd: description: "Require a validated SDD first commit for started feature and contract-change issues. Requires issue-managed-branches." default: "false" + verification-setup: + description: "Run the trusted main-branch .copilot/verification-setup.mjs before each repository verification command. Disabled by default." + default: "false" issue-workflow-profile: description: "JSON profile of enabled issue workflows." default: "" diff --git a/build/cli/index.js b/build/cli/index.js index 10462212d..18bfa96dd 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -37179,6 +37179,7 @@ function readLocalWorkflowConfiguration(additionalParams, actionInputs) { commitPrefixBuilder: read(input_keys_1.INPUT_KEYS.COMMIT_PREFIX_TRANSFORMS) || 'replace-slash', issueManagedBranches: (0, input_boolean_policy_1.parseIssueWorkflowBoolean)(read(input_keys_1.INPUT_KEYS.ISSUE_MANAGED_BRANCHES), input_keys_1.INPUT_KEYS.ISSUE_MANAGED_BRANCHES, true), preBranchSdd: (0, input_boolean_policy_1.parseIssueWorkflowBoolean)(read(input_keys_1.INPUT_KEYS.PRE_BRANCH_SDD), input_keys_1.INPUT_KEYS.PRE_BRANCH_SDD, false), + verificationSetup: (0, input_boolean_policy_1.parseIssueWorkflowBoolean)(read(input_keys_1.INPUT_KEYS.VERIFICATION_SETUP), input_keys_1.INPUT_KEYS.VERIFICATION_SETUP, false), reopenIssueOnPush: (0, input_boolean_policy_1.isEnabledInput)(read(input_keys_1.INPUT_KEYS.REOPEN_ISSUE_ON_PUSH)), issueDesiredAssigneesCount: (0, input_number_policy_1.parseIntegerInput)(read(input_keys_1.INPUT_KEYS.DESIRED_ASSIGNEES_COUNT), 0), pullRequestDesiredAssigneesCount: (0, input_number_policy_1.parseIntegerInput)(read(input_keys_1.INPUT_KEYS.PULL_REQUEST_DESIRED_ASSIGNEES_COUNT), 0), @@ -37212,7 +37213,7 @@ const configuration_builders_1 = __nccwpck_require__(19094); const branches_builder_1 = __nccwpck_require__(30085); const size_threshold_builder_1 = __nccwpck_require__(39757); function buildLocalActionExecution(configuration, additionalParams) { - const { debug, singleAction, singleActionIssue, singleActionVersion, singleActionTitle, singleActionChangelog, singleActionMessage, singleActionCommentId, singleActionCommentMode, singleActionOperationId, inactivityThresholdHours, commitPrefixBuilder, issueManagedBranches, preBranchSdd, reopenIssueOnPush, issueDesiredAssigneesCount, pullRequestDesiredAssigneesCount, pullRequestDesiredReviewersCount, titleEmoji, branchManagementEmoji, token, agentModel, aiPullRequestDescriptionMode, aiMembersOnly, aiIgnoreFiles, aiIncludeReasoning, bugbotSeverity, bugbotCommentLimit, bugbotFixVerifyCommands, bugbotReviewConfiguration, agentTasks, bugLabel, bugfixLabel, hotfixLabel, enhancementLabel, featureLabel, releaseLabel, questionLabel, helpLabel, deployLabel, deployedLabel, docsLabel, documentationLabel, choreLabel, maintenanceLabel, priorityHighLabel, priorityMediumLabel, priorityLowLabel, priorityNoneLabel, sizeXxlLabel, sizeXlLabel, sizeLLabel, sizeMLabel, sizeSLabel, sizeXsLabel, lifecycle, issueTypeTask, issueTypeTaskDescription, issueTypeTaskColor, issueTypeBug, issueTypeBugDescription, issueTypeBugColor, issueTypeFeature, issueTypeFeatureDescription, issueTypeFeatureColor, issueTypeDocumentation, issueTypeDocumentationDescription, issueTypeDocumentationColor, issueTypeMaintenance, issueTypeMaintenanceDescription, issueTypeMaintenanceColor, issueTypeHotfix, issueTypeHotfixDescription, issueTypeHotfixColor, issueTypeRelease, issueTypeReleaseDescription, issueTypeReleaseColor, issueTypeQuestion, issueTypeQuestionDescription, issueTypeQuestionColor, issueTypeHelp, issueTypeHelpDescription, issueTypeHelpColor, repositoryLocale, issueLocale, pullRequestLocale, sizeXxlThresholdLines, sizeXxlThresholdFiles, sizeXxlThresholdCommits, sizeXlThresholdLines, sizeXlThresholdFiles, sizeXlThresholdCommits, sizeLThresholdLines, sizeLThresholdFiles, sizeLThresholdCommits, sizeMThresholdLines, sizeMThresholdFiles, sizeMThresholdCommits, sizeSThresholdLines, sizeSThresholdFiles, sizeSThresholdCommits, sizeXsThresholdLines, sizeXsThresholdFiles, sizeXsThresholdCommits, mainBranch, developmentBranch, featureTree, bugfixTree, hotfixTree, releaseTree, docsTree, choreTree, releaseWorkflow, hotfixWorkflow, projects, projectColumnIssueCreated, projectColumnPullRequestCreated, projectColumnIssueInProgress, projectColumnPullRequestInProgress, welcomeTitle, welcomeMessages, deployment, } = configuration; + const { debug, singleAction, singleActionIssue, singleActionVersion, singleActionTitle, singleActionChangelog, singleActionMessage, singleActionCommentId, singleActionCommentMode, singleActionOperationId, inactivityThresholdHours, commitPrefixBuilder, issueManagedBranches, preBranchSdd, verificationSetup, reopenIssueOnPush, issueDesiredAssigneesCount, pullRequestDesiredAssigneesCount, pullRequestDesiredReviewersCount, titleEmoji, branchManagementEmoji, token, agentModel, aiPullRequestDescriptionMode, aiMembersOnly, aiIgnoreFiles, aiIncludeReasoning, bugbotSeverity, bugbotCommentLimit, bugbotFixVerifyCommands, bugbotReviewConfiguration, agentTasks, bugLabel, bugfixLabel, hotfixLabel, enhancementLabel, featureLabel, releaseLabel, questionLabel, helpLabel, deployLabel, deployedLabel, docsLabel, documentationLabel, choreLabel, maintenanceLabel, priorityHighLabel, priorityMediumLabel, priorityLowLabel, priorityNoneLabel, sizeXxlLabel, sizeXlLabel, sizeLLabel, sizeMLabel, sizeSLabel, sizeXsLabel, lifecycle, issueTypeTask, issueTypeTaskDescription, issueTypeTaskColor, issueTypeBug, issueTypeBugDescription, issueTypeBugColor, issueTypeFeature, issueTypeFeatureDescription, issueTypeFeatureColor, issueTypeDocumentation, issueTypeDocumentationDescription, issueTypeDocumentationColor, issueTypeMaintenance, issueTypeMaintenanceDescription, issueTypeMaintenanceColor, issueTypeHotfix, issueTypeHotfixDescription, issueTypeHotfixColor, issueTypeRelease, issueTypeReleaseDescription, issueTypeReleaseColor, issueTypeQuestion, issueTypeQuestionDescription, issueTypeQuestionColor, issueTypeHelp, issueTypeHelpDescription, issueTypeHelpColor, repositoryLocale, issueLocale, pullRequestLocale, sizeXxlThresholdLines, sizeXxlThresholdFiles, sizeXxlThresholdCommits, sizeXlThresholdLines, sizeXlThresholdFiles, sizeXlThresholdCommits, sizeLThresholdLines, sizeLThresholdFiles, sizeLThresholdCommits, sizeMThresholdLines, sizeMThresholdFiles, sizeMThresholdCommits, sizeSThresholdLines, sizeSThresholdFiles, sizeSThresholdCommits, sizeXsThresholdLines, sizeXsThresholdFiles, sizeXsThresholdCommits, mainBranch, developmentBranch, featureTree, bugfixTree, hotfixTree, releaseTree, docsTree, choreTree, releaseWorkflow, hotfixWorkflow, projects, projectColumnIssueCreated, projectColumnPullRequestCreated, projectColumnIssueInProgress, projectColumnPullRequestInProgress, welcomeTitle, welcomeMessages, deployment, } = configuration; return (0, execution_builder_1.buildExecution)({ debug, inactivityThresholdHours, @@ -37220,6 +37221,7 @@ function buildLocalActionExecution(configuration, additionalParams) { commitPrefixBuilder, issue: (0, configuration_builders_1.buildIssue)(issueManagedBranches, reopenIssueOnPush, issueDesiredAssigneesCount, additionalParams), preBranchSdd, + verificationSetup, pullRequest: (0, configuration_builders_1.buildPullRequest)(pullRequestDesiredAssigneesCount, pullRequestDesiredReviewersCount, additionalParams), emoji: (0, configuration_builders_1.buildEmoji)(titleEmoji, branchManagementEmoji), tokens: (0, configuration_builders_1.buildTokens)(token), @@ -37982,6 +37984,7 @@ exports.INPUT_KEYS = { // Issue ISSUE_MANAGED_BRANCHES: 'issue-managed-branches', PRE_BRANCH_SDD: 'pre-branch-sdd', + VERIFICATION_SETUP: 'verification-setup', REOPEN_ISSUE_ON_PUSH: 'reopen-issue-on-push', DESIRED_ASSIGNEES_COUNT: 'desired-assignees-count', // Pull Request @@ -46211,6 +46214,8 @@ The setup runner profile selects placement; it does not install the project's la Declare reviewed version prefixes in \`.copilot/verification-prerequisites.json\` (schemaVersion 1, runtimes such as java 17, node 24, pnpm 10). Copilot checks declared versions in the credential-free verification environment before each configured verification command; a missing or incompatible runtime blocks publication. Without this optional declaration, existing verification behavior remains and no language is inferred. Java provisioning may use \`actions/setup-java\`; \`JAVA_HOME\` and \`PATH\` survive isolation, while agent credentials and tool-option injection variables do not. Do not add an unreviewed bootstrap or weaken verification to work around a missing runtime. Read [verification prerequisites and same-job setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites). +The optional repository-owned setup helper prepares each candidate before verification without editing generated workflows. Review \`.copilot/verification-setup.mjs\` onto trusted main before enabling \`VERIFICATION_SETUP=true\` (Action input \`verification-setup\`, disabled by default). Copilot executes captured trusted bytes before each command in a fresh credential-free profile; candidate changes cannot replace them. Setup failure blocks verification and commit/push. Read [repository-owned setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup). + For the full contributor flow, read [repository collaboration](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-collaboration.mdx). For drift and recovery, read [guidance troubleshooting](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-guidance-troubleshooting.mdx). `; } @@ -47928,6 +47933,7 @@ function createDefaultSetupConfiguration() { choreTree: 'chore', issueManagedBranches: true, preBranchSdd: false, + verificationSetup: false, reopenIssueOnPush: true, desiredAssigneesCount: 1, desiredReviewersCount: 1, @@ -48195,6 +48201,7 @@ function buildSetupRepositoryVariables(configuration) { add('CHORE_TREE', repository.choreTree); add('ISSUE_MANAGED_BRANCHES', repository.issueManagedBranches); add('PRE_BRANCH_SDD', repository.preBranchSdd); + add('VERIFICATION_SETUP', repository.verificationSetup ?? false); add('REOPEN_ISSUE_ON_PUSH', repository.reopenIssueOnPush); add('DESIRED_ASSIGNEES_COUNT', repository.desiredAssigneesCount); add('DESIRED_REVIEWERS_COUNT', repository.desiredReviewersCount); @@ -48257,6 +48264,7 @@ function buildSetupActionInputs(configuration) { 'chore-tree': repository.choreTree, 'issue-managed-branches': String(repository.issueManagedBranches), 'pre-branch-sdd': String(repository.preBranchSdd), + 'verification-setup': String(repository.verificationSetup ?? false), 'reopen-issue-on-push': String(repository.reopenIssueOnPush), 'desired-assignees-count': String(repository.desiredAssigneesCount), 'desired-reviewers-count': String(repository.desiredReviewersCount), @@ -48702,6 +48710,9 @@ function validateSetupConfiguration(configuration, options = {}) { || typeof configuration.repository.preBranchSdd !== 'boolean') { errors.push('issue-managed-branches and pre-branch-sdd must be boolean values.'); } + if (configuration.repository.verificationSetup !== undefined && typeof configuration.repository.verificationSetup !== 'boolean') { + errors.push('verification-setup must be a boolean value.'); + } if (configuration.repository.preBranchSdd && !configuration.repository.issueManagedBranches) { errors.push('pre-branch-sdd requires issue-managed-branches.'); } @@ -49933,6 +49944,7 @@ const repositoryFields = [ ['HOTFIX_TREE', 'hotfixTree'], ['RELEASE_TREE', 'releaseTree'], ['DOCS_TREE', 'docsTree'], ['CHORE_TREE', 'choreTree'], ['ISSUE_MANAGED_BRANCHES', 'issueManagedBranches'], ['PRE_BRANCH_SDD', 'preBranchSdd'], + ['VERIFICATION_SETUP', 'verificationSetup'], ['REOPEN_ISSUE_ON_PUSH', 'reopenIssueOnPush'], ['DESIRED_ASSIGNEES_COUNT', 'desiredAssigneesCount'], ['DESIRED_REVIEWERS_COUNT', 'desiredReviewersCount'], ['INACTIVITY_THRESHOLD_HOURS', 'inactivityThresholdHours'], ['REPOSITORY_LOCALE', 'repositoryLocale'], ['ISSUES_LOCALE', 'issueLocale'], @@ -53328,6 +53340,54 @@ function verificationRuntimeMatches(required, observed) { } +/***/ }), + +/***/ 65326: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.VerificationSetupError = exports.VERIFICATION_SETUP_HELP = exports.VERIFICATION_SETUP_LIMITS = exports.VERIFICATION_SETUP_PATH = void 0; +exports.parseVerificationSetupResult = parseVerificationSetupResult; +exports.VERIFICATION_SETUP_PATH = '.copilot/verification-setup.mjs'; +exports.VERIFICATION_SETUP_LIMITS = Object.freeze({ timeoutMs: 300000, outputBytes: 65536, resultBytes: 16384, scriptBytes: 65536 }); +exports.VERIFICATION_SETUP_HELP = 'https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup'; +/** Closed facts only. Provider/helper output and raw exception causes never cross this boundary. */ +class VerificationSetupError extends Error { + constructor(reason, sourceSha, exitCode) { + const sha = sourceSha && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(sourceSha) ? sourceSha : 'unavailable'; + const exit = Number.isInteger(exitCode) && exitCode >= 0 && exitCode <= 255 ? ` Exit status: ${exitCode}.` : ''; + super(`Repository setup blocked verification (${reason}). Review ${exports.VERIFICATION_SETUP_PATH} on the trusted main branch and rerun. Trusted source SHA: ${sha}.${exit} Verification, commit and push are withheld. Help: ${exports.VERIFICATION_SETUP_HELP}`); + this.reason = reason; + this.exitCode = exitCode; + this.name = 'VerificationSetupError'; + } +} +exports.VerificationSetupError = VerificationSetupError; +function record(value) { + return !!value && typeof value === 'object' && !Array.isArray(value); +} +function pathString(value) { + return typeof value === 'string' && value.length > 0 && value.length <= 2048 + && ![...value].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127); +} +/** Filesystem containment is checked by the adapter, after this closed schema is parsed. */ +function parseVerificationSetupResult(value) { + if (!record(value) || value.schemaVersion !== 1 || Object.keys(value).some(key => !['schemaVersion', 'path', 'environment'].includes(key))) { + throw new VerificationSetupError('result-invalid'); + } + const paths = value.path === undefined ? [] : value.path; + const environment = value.environment === undefined ? {} : value.environment; + if (!Array.isArray(paths) || paths.length > 8 || !paths.every(pathString) || new Set(paths).size !== paths.length + || !record(environment) || Object.keys(environment).some(key => key !== 'JAVA_HOME') + || ('JAVA_HOME' in environment && !pathString(environment.JAVA_HOME))) { + throw new VerificationSetupError('result-invalid'); + } + return Object.freeze({ path: Object.freeze([...paths]), ...(environment.JAVA_HOME ? { javaHome: environment.JAVA_HOME } : {}) }); +} + + /***/ }), /***/ 43193: @@ -53533,6 +53593,18 @@ function bindSddEvidenceReadPorts(ports) { } +/***/ }), + +/***/ 26509: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DISABLED_VERIFICATION_SETUP = void 0; +exports.DISABLED_VERIFICATION_SETUP = Object.freeze({ kind: 'disabled' }); + + /***/ }), /***/ 84387: @@ -68320,6 +68392,7 @@ exports.runVerifyCommands = runVerifyCommands; const logging_ports_1 = __nccwpck_require__(6152); const verify_command_policy_1 = __nccwpck_require__(96031); const verification_runtime_policy_1 = __nccwpck_require__(89347); +const verification_setup_policy_1 = __nccwpck_require__(65326); async function runVerifyCommands(commands, execute) { for (const command of commands) { const result = await executeVerifyCommand(command, execute); @@ -68339,7 +68412,7 @@ async function executeVerifyCommand(command, execute) { : { success: false, failedCommand: formatCommandForDiagnostics(parsed) }; } catch (error) { - if (error instanceof verification_runtime_policy_1.VerificationPrerequisiteError) { + if (error instanceof verification_runtime_policy_1.VerificationPrerequisiteError || error instanceof verification_setup_policy_1.VerificationSetupError) { return { success: false, failedCommand: formatCommandForDiagnostics(parsed), error: `${error.message} Verification command: ${formatCommandForDiagnostics(parsed)}.` }; } @@ -75110,7 +75183,7 @@ const REPOSITORY_STRING_KEYS = new Set([ 'orchestrationPresentationMode', 'orchestrationCommentMode', ]); -const REPOSITORY_BOOLEAN_KEYS = new Set(['issueManagedBranches', 'preBranchSdd', 'reopenIssueOnPush', 'orchestrationDiagrams']); +const REPOSITORY_BOOLEAN_KEYS = new Set(['issueManagedBranches', 'preBranchSdd', 'verificationSetup', 'reopenIssueOnPush', 'orchestrationDiagrams']); const REPOSITORY_NUMBER_KEYS = new Set(['desiredAssigneesCount', 'desiredReviewersCount', 'inactivityThresholdHours']); const REPOSITORY_STRUCTURED_KEYS = new Set(['mergeQueueCheckAttestations']); const AI_STRING_KEYS = new Set(['ignoreFiles', 'pullRequestDescriptionMode', 'bugbotSeverity', 'bugbotFixVerifyCommands', 'bugbotEffort', 'bugbotOrganizationRules', 'provisioningMode']); @@ -79084,6 +79157,7 @@ class Execution { this.currentIssueWorkflowAdmission = components.issueWorkflowAdmission; this.currentConfiguration.issueWorkflowProfileDigest = components.issueWorkflowProfileDigest; this.preBranchSdd = components.preBranchSdd ?? false; + this.verificationSetup = components.verificationSetup ?? false; } } exports.Execution = Execution; @@ -95869,12 +95943,14 @@ function createDeploymentOrchestrationUseCase(issueDescriptionQueryPort, publica }); } const copilot_history_port_factory_1 = __nccwpck_require__(12321); -function createIssueCommentUseCaseCompositionRoot(binding) { +const verification_setup_snapshot_1 = __nccwpck_require__(62148); +const verification_setup_ports_1 = __nccwpck_require__(26509); +function createIssueCommentUseCaseCompositionRoot(binding, setup = verification_setup_ports_1.DISABLED_VERIFICATION_SETUP) { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(binding); const findings = (0, agent_capability_composition_root_1.createFindingsQueryPort)(); const language = (0, agent_capability_composition_root_1.createLanguageQueryPort)(); const fixer = (0, agent_capability_composition_root_1.createFixerQueryPort)(); - const gitCommit = new git_commit_adapter_1.GitCommitAdapter(); + const gitCommit = new git_commit_adapter_1.GitCommitAdapter(undefined, undefined, setup); const authenticatedUser = (0, authenticated_user_composition_root_1.createAuthenticatedUserCompositionRoot)(); const bugbotGit = new bound_bugbot_git_mutation_adapter_1.BoundBugbotGitMutationAdapter(gitCommit, authenticatedUser, binding.token); const pullRequestDescription = new update_pull_request_description_use_case_1.UpdatePullRequestDescriptionUseCase((0, lifecycle_capability_port_binding_1.bindPullRequestDescription)(new pull_request_lifecycle_repository_1.PullRequestLifecycleRepository((0, github_pull_request_client_factory_1.createPullRequestLifecycleClient)()), binding, new pull_request_changes_repository_1.PullRequestChangesRepository((0, github_pull_request_client_factory_1.createPullRequestChangesClient)())), (0, shared_capability_port_binding_1.bindIssueDescriptionQuery)((0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), binding), (0, shared_capability_port_binding_1.bindOrganizationMembers)((0, organization_members_composition_root_1.createOrganizationMembersCompositionRoot)(), binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()); @@ -95885,12 +95961,12 @@ function createIssueCommentUseCaseCompositionRoot(binding) { catalogResolver: new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language), }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, (0, issue_use_case_composition_root_1.createIssueUseCaseCompositionRoot)(binding), rememberBranchPorts(binding, bugbot.scm.context), (0, pull_request_status_composition_root_1.createPullRequestStatusReadPorts)(binding, bugbot.scm.context), (0, copilot_history_port_factory_1.createCopilotHistoryReadPort)(bugbot.scm.context, process.env.GITHUB_SERVER_URL ?? 'https://github.com'), (0, sdd_evidence_composition_root_1.createSddEvidenceQueryPort)(binding, bugbot.scm.context), (0, sdd_diff_composition_root_1.createSddDiffQueryPort)(binding, bugbot.scm.context), (0, issue_completion_composition_root_2.createNativeStatusReadPort)(binding), (0, github_runtime_doctor_composition_root_1.createGithubRuntimeDoctorPorts)(binding, bugbot.scm.context)); } -function createPullRequestReviewCommentUseCaseCompositionRoot(binding) { +function createPullRequestReviewCommentUseCaseCompositionRoot(binding, setup = verification_setup_ports_1.DISABLED_VERIFICATION_SETUP) { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(binding); const findings = (0, agent_capability_composition_root_1.createFindingsQueryPort)(); const language = (0, agent_capability_composition_root_1.createLanguageQueryPort)(); const fixer = (0, agent_capability_composition_root_1.createFixerQueryPort)(); - const gitCommit = new git_commit_adapter_1.GitCommitAdapter(); + const gitCommit = new git_commit_adapter_1.GitCommitAdapter(undefined, undefined, setup); const authenticatedUser = (0, authenticated_user_composition_root_1.createAuthenticatedUserCompositionRoot)(); const bugbotGit = new bound_bugbot_git_mutation_adapter_1.BoundBugbotGitMutationAdapter(gitCommit, authenticatedUser, binding.token); const pullRequestDescription = new update_pull_request_description_use_case_1.UpdatePullRequestDescriptionUseCase((0, lifecycle_capability_port_binding_1.bindPullRequestDescription)(new pull_request_lifecycle_repository_1.PullRequestLifecycleRepository((0, github_pull_request_client_factory_1.createPullRequestLifecycleClient)()), binding, new pull_request_changes_repository_1.PullRequestChangesRepository((0, github_pull_request_client_factory_1.createPullRequestChangesClient)())), (0, shared_capability_port_binding_1.bindIssueDescriptionQuery)((0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), binding), (0, shared_capability_port_binding_1.bindOrganizationMembers)((0, organization_members_composition_root_1.createOrganizationMembersCompositionRoot)(), binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()); @@ -95917,7 +95993,7 @@ function createMainRunRouteCompositionRoot(projectBoardCommandPort, surface, set return singleAction.invoke(execution); }, "issue-comment": async (execution) => { - issueComment ?? (issueComment = createIssueCommentUseCaseCompositionRoot(bugbotBinding(execution))); + issueComment ?? (issueComment = createIssueCommentUseCaseCompositionRoot(bugbotBinding(execution), (0, verification_setup_snapshot_1.captureVerificationSetup)(execution.verificationSetup, execution.branches.main))); return issueComment.invoke(execution); }, issue: async (execution) => { @@ -95925,7 +96001,7 @@ function createMainRunRouteCompositionRoot(projectBoardCommandPort, surface, set return issue.invoke(execution); }, "pull-request-review-comment": async (execution) => { - pullRequestReviewComment ?? (pullRequestReviewComment = createPullRequestReviewCommentUseCaseCompositionRoot(bugbotBinding(execution))); + pullRequestReviewComment ?? (pullRequestReviewComment = createPullRequestReviewCommentUseCaseCompositionRoot(bugbotBinding(execution), (0, verification_setup_snapshot_1.captureVerificationSetup)(execution.verificationSetup, execution.branches.main))); return pullRequestReviewComment.invoke(execution); }, "pull-request": async (execution) => { @@ -97038,6 +97114,8 @@ const git_authentication_environment_1 = __nccwpck_require__(16535); const untrusted_command_environment_1 = __nccwpck_require__(2304); const git_hook_isolation_1 = __nccwpck_require__(39764); const verification_runtime_preflight_1 = __nccwpck_require__(23100); +const verification_setup_ports_1 = __nccwpck_require__(26509); +const verification_setup_runner_1 = __nccwpck_require__(11709); class GitCommitAdapter { constructor(executeCommand = (program, args, options) => options ? exec.exec(program, args, { @@ -97047,9 +97125,10 @@ class GitCommitAdapter { ...(options.stdout ? { listeners: { stdout: options.stdout } } : {}), ...(options.env ? { env: options.env } : {}), }) - : exec.exec(program, args), checkPrerequisites = verification_runtime_preflight_1.assertVerificationRuntimePrerequisites) { + : exec.exec(program, args), checkPrerequisites = verification_runtime_preflight_1.assertVerificationRuntimePrerequisites, verificationSetup = verification_setup_ports_1.DISABLED_VERIFICATION_SETUP) { this.executeCommand = executeCommand; this.checkPrerequisites = checkPrerequisites; + this.verificationSetup = verificationSetup; } async execute(program, args, options) { if (options?.verifyRuntimePrerequisites && !options.untrusted) @@ -97060,8 +97139,10 @@ class GitCommitAdapter { throw new Error('Untrusted command execution does not accept a caller-supplied environment.'); const runtime = (0, untrusted_command_environment_1.prepareUntrustedCommandEnvironment)(); try { - if (options.verifyRuntimePrerequisites) + if (options.verifyRuntimePrerequisites) { + await (0, verification_setup_runner_1.prepareRepositoryVerification)(this.verificationSetup, runtime.environment); this.checkPrerequisites(runtime.environment); + } return await this.executeCommand(program, args, { ...(options.stdout ? { stdout: options.stdout } : {}), env: runtime.environment, @@ -102574,7 +102655,7 @@ const ALLOWED_VARIABLES = [ * credentials or paths to local agent authentication stores. */ function prepareUntrustedCommandEnvironment(source = process.env, platform = process.platform) { - const runtimeHome = (0, agent_private_install_root_1.createPrivateVerificationRuntimeRoot)(); + const runtimeHome = (0, node_fs_1.realpathSync)((0, agent_private_install_root_1.createPrivateVerificationRuntimeRoot)()); const environment = { HOME: runtimeHome }; for (const variable of ALLOWED_VARIABLES) { const value = source[variable]; @@ -102728,6 +102809,245 @@ function checkRuntime(requirement, environment, run, platform, job) { } +/***/ }), + +/***/ 43966: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.runVerificationSetupProcess = runVerificationSetupProcess; +const node_child_process_1 = __nccwpck_require__(17718); +const agent_trusted_system_tools_1 = __nccwpck_require__(76049); +const verification_setup_policy_1 = __nccwpck_require__(65326); +/** All output is counted and discarded. Fixed production limits; short injected limits are for deterministic tests. */ +function runVerificationSetupProcess(node, script, cwd, env, sourceSha, limits = verification_setup_policy_1.VERIFICATION_SETUP_LIMITS) { + return new Promise((resolve, reject) => { + const child = (0, node_child_process_1.spawn)(node, [script], { cwd, env, shell: false, windowsHide: true, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] }); + let failure; + let outputBytes = 0; + const terminate = () => { + if (!child.pid) + return; + try { + if (process.platform === 'win32') + (0, node_child_process_1.spawnSync)((0, agent_trusted_system_tools_1.trustedWindowsSystemTool)('taskkill.exe'), ['/PID', String(child.pid), '/T', '/F'], { env, windowsHide: true, timeout: 5000, stdio: 'ignore', shell: false }); + else + process.kill(-child.pid, 'SIGKILL'); + } + catch { + child.kill('SIGKILL'); + } + }; + const timer = setTimeout(() => { failure ?? (failure = 'timeout'); terminate(); }, limits.timeoutMs); + const cancel = () => { failure ?? (failure = 'cancelled'); terminate(); }; + process.once('SIGINT', cancel); + process.once('SIGTERM', cancel); + const capture = (data) => { + outputBytes += data.length; + if (outputBytes > limits.outputBytes) { + failure ?? (failure = 'output-limit'); + terminate(); + } + }; + child.stdout.on('data', capture); + child.stderr.on('data', capture); + child.on('error', () => { failure ?? (failure = 'process-failed'); }); + child.on('close', code => { + clearTimeout(timer); + process.removeListener('SIGINT', cancel); + process.removeListener('SIGTERM', cancel); + terminate(); + if (failure || code !== 0) + reject(new verification_setup_policy_1.VerificationSetupError(failure ?? 'process-failed', sourceSha, code ?? undefined)); + else + resolve(); + }); + }); +} + + +/***/ }), + +/***/ 23358: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.applyVerificationSetupResult = applyVerificationSetupResult; +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const verification_setup_policy_1 = __nccwpck_require__(65326); +function contained(root, path) { + const remainder = (0, node_path_1.relative)(root, path); + return remainder === '' || (remainder !== '..' && !remainder.startsWith(`..${node_path_1.sep}`) && !(0, node_path_1.isAbsolute)(remainder)); +} +/** Accept only existing contained directories, checking both lexical paths and symlink targets. */ +function approvedDirectory(value, roots) { + if (!(0, node_path_1.isAbsolute)(value) || value.includes(node_path_1.delimiter) || value.split(/[\\/]/u).includes('..') + || !roots.some(root => contained(root, value))) + throw new verification_setup_policy_1.VerificationSetupError('result-invalid'); + const actual = (0, node_fs_1.realpathSync)(value); + if (!roots.some(root => contained(root, actual)) || !(0, node_fs_1.lstatSync)(actual).isDirectory()) + throw new verification_setup_policy_1.VerificationSetupError('result-invalid'); + return actual; +} +function applyVerificationSetupResult(file, environment, roots) { + let descriptor; + try { + let before; + try { + before = (0, node_fs_1.lstatSync)(file, { bigint: true }); + } + catch (error) { + if (error.code === 'ENOENT') + return; + throw error; + } + if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.size > BigInt(verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.resultBytes)) + throw new Error(); + descriptor = (0, node_fs_1.openSync)(file, node_fs_1.constants.O_RDONLY | (node_fs_1.constants.O_NOFOLLOW ?? 0)); + const opened = (0, node_fs_1.fstatSync)(descriptor, { bigint: true }); + if (!opened.isFile() || opened.nlink !== 1n || opened.dev !== before.dev || opened.ino !== before.ino + || opened.size !== before.size || opened.mtimeNs !== before.mtimeNs) + throw new Error(); + const bytes = Buffer.alloc(verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.resultBytes + 1); + const size = (0, node_fs_1.readSync)(descriptor, bytes, 0, bytes.length, 0); + if (size > verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.resultBytes) + throw new Error(); + const after = (0, node_fs_1.fstatSync)(descriptor, { bigint: true }); + if (after.size !== opened.size || after.mtimeNs !== opened.mtimeNs) + throw new Error(); + const result = (0, verification_setup_policy_1.parseVerificationSetupResult)(JSON.parse(bytes.subarray(0, size).toString('utf8'))); + const paths = result.path.map(value => approvedDirectory(value, roots)); + const javaHome = result.javaHome ? approvedDirectory(result.javaHome, roots) : undefined; + if (paths.length) + environment.PATH = [...paths, ...(environment.PATH ? [environment.PATH] : [])].join(node_path_1.delimiter); + if (javaHome) + environment.JAVA_HOME = javaHome; + } + catch { + throw new verification_setup_policy_1.VerificationSetupError('result-invalid'); + } + finally { + if (descriptor !== undefined) + (0, node_fs_1.closeSync)(descriptor); + } +} + + +/***/ }), + +/***/ 11709: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.prepareRepositoryVerification = prepareRepositoryVerification; +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const verification_setup_policy_1 = __nccwpck_require__(65326); +const verification_setup_result_1 = __nccwpck_require__(23358); +const verification_setup_process_1 = __nccwpck_require__(43966); +const logging_ports_1 = __nccwpck_require__(6152); +const verification_setup_policy_2 = __nccwpck_require__(65326); +/** Called once per verification command, on its fresh private profile, before runtime probes. */ +async function prepareRepositoryVerification(setup, environment) { + if (setup.kind === 'disabled') + return; + if (setup.kind === 'blocked') + throw new verification_setup_policy_1.VerificationSetupError(setup.reason, setup.sourceSha); + try { + if ((0, node_fs_1.realpathSync)(process.cwd()) !== setup.workspace) + throw new verification_setup_policy_1.VerificationSetupError('workspace-changed', setup.sourceSha); + const profile = (0, node_fs_1.realpathSync)(environment.HOME); + const script = (0, node_path_1.join)(profile, 'verification-setup.mjs'); + const result = (0, node_path_1.join)(profile, 'verification-setup-result.json'); + (0, node_fs_1.writeFileSync)(script, setup.script, { mode: 0o600, flag: 'wx' }); + await (0, verification_setup_process_1.runVerificationSetupProcess)(setup.node, script, setup.workspace, { + ...environment, + COPILOT_VERIFICATION_SETUP_PROTOCOL: '1', + COPILOT_VERIFICATION_SETUP_RESULT: result, + COPILOT_VERIFICATION_SETUP_SOURCE_SHA: setup.sourceSha, + }, setup.sourceSha); + (0, verification_setup_result_1.applyVerificationSetupResult)(result, environment, [setup.workspace, profile, ...(setup.toolRoot ? [setup.toolRoot] : [])]); + (0, logging_ports_1.logInfo)(`Repository setup prepared (${verification_setup_policy_2.VERIFICATION_SETUP_PATH}; trusted source SHA: ${setup.sourceSha}).`); + } + catch (error) { + throw new verification_setup_policy_1.VerificationSetupError(error instanceof verification_setup_policy_1.VerificationSetupError ? error.reason : 'process-failed', setup.sourceSha, error instanceof verification_setup_policy_1.VerificationSetupError ? error.exitCode : undefined); + } +} + + +/***/ }), + +/***/ 62148: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.captureVerificationSetup = captureVerificationSetup; +const node_child_process_1 = __nccwpck_require__(17718); +const node_crypto_1 = __nccwpck_require__(6005); +const node_fs_1 = __nccwpck_require__(87561); +const verification_setup_ports_1 = __nccwpck_require__(26509); +const verification_setup_policy_1 = __nccwpck_require__(65326); +const untrusted_command_environment_1 = __nccwpck_require__(2304); +/** No network, agent, worktree-file reads, or credentials. Acquisition errors are deferred until verification. */ +function captureVerificationSetup(enabled, mainBranch, root = process.cwd()) { + if (!enabled) + return verification_setup_ports_1.DISABLED_VERIFICATION_SETUP; + let sourceSha; + let runtime; + let snapshot; + try { + runtime = (0, untrusted_command_environment_1.prepareUntrustedCommandEnvironment)(); + const environment = runtime.environment; + if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/u.test(mainBranch) || mainBranch.includes('..') + || mainBranch.includes('//') || mainBranch.endsWith('/') || mainBranch.endsWith('.lock')) + throw new Error(); + const workspace = (0, node_fs_1.realpathSync)(root); + const git = (...args) => { + const result = (0, node_child_process_1.spawnSync)('git', ['--no-replace-objects', ...args], { cwd: workspace, env: environment, + shell: false, windowsHide: true, timeout: 5000, maxBuffer: verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.scriptBytes + 4096 }); + if (result.error || result.status !== 0) + throw new Error(); + return result.stdout; + }; + const candidateSha = git('rev-parse', '--verify', `refs/remotes/origin/${mainBranch}^{commit}`).toString('utf8').trim(); + if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(candidateSha)) + throw new Error(); + sourceSha = candidateSha; + const entry = git('ls-tree', '-z', sourceSha, '--', verification_setup_policy_1.VERIFICATION_SETUP_PATH).toString('utf8'); + const match = /^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64})\t\.copilot\/verification-setup\.mjs\0$/u.exec(entry); + if (!match) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + const bytes = git('cat-file', 'blob', match[2]); + const digest = (0, node_crypto_1.createHash)(match[2].length === 40 ? 'sha1' : 'sha256').update(`blob ${bytes.length}\0`).update(bytes).digest('hex'); + const script = bytes.toString('utf8'); + if (!bytes.length || bytes.length > verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.scriptBytes || digest !== match[2] + || !Buffer.from(script).equals(bytes) || script.includes('\0')) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + const toolRoot = environment.RUNNER_TOOL_CACHE ? (0, node_fs_1.realpathSync)(environment.RUNNER_TOOL_CACHE) : undefined; + snapshot = Object.freeze({ kind: 'ready', script, sourceSha, workspace, node: process.execPath, ...(toolRoot ? { toolRoot } : {}) }); + } + catch (error) { + snapshot = Object.freeze({ kind: 'blocked', reason: error instanceof verification_setup_policy_1.VerificationSetupError ? error.reason : 'source-unavailable', ...(sourceSha ? { sourceSha } : {}) }); + } + try { + runtime?.cleanup(); + } + catch { + snapshot = Object.freeze({ kind: 'blocked', reason: 'source-unavailable', ...(sourceSha ? { sourceSha } : {}) }); + } + return snapshot; +} + + /***/ }), /***/ 92540: diff --git a/build/github_action/index.js b/build/github_action/index.js index 66263e0b3..ab796a6f3 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -37531,6 +37531,7 @@ async function buildGithubActionExecution(input) { commitPrefixBuilder: getCommitPrefixBuilder(getInput), issue: (0, configuration_builders_1.buildIssue)((0, input_boolean_policy_1.parseIssueWorkflowBoolean)(getInput(input_keys_1.INPUT_KEYS.ISSUE_MANAGED_BRANCHES), input_keys_1.INPUT_KEYS.ISSUE_MANAGED_BRANCHES, true), (0, input_boolean_policy_1.isEnabledInput)(getInput(input_keys_1.INPUT_KEYS.REOPEN_ISSUE_ON_PUSH)), (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.DESIRED_ASSIGNEES_COUNT), 0), eventInputs), preBranchSdd: (0, input_boolean_policy_1.parseIssueWorkflowBoolean)(getInput(input_keys_1.INPUT_KEYS.PRE_BRANCH_SDD), input_keys_1.INPUT_KEYS.PRE_BRANCH_SDD, false), + verificationSetup: (0, input_boolean_policy_1.parseIssueWorkflowBoolean)(getInput(input_keys_1.INPUT_KEYS.VERIFICATION_SETUP), input_keys_1.INPUT_KEYS.VERIFICATION_SETUP, false), pullRequest: (0, configuration_builders_1.buildPullRequest)((0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.PULL_REQUEST_DESIRED_ASSIGNEES_COUNT), 0), (0, input_number_policy_1.parseIntegerInput)(getInput(input_keys_1.INPUT_KEYS.PULL_REQUEST_DESIRED_REVIEWERS_COUNT), 0), eventInputs), emoji: (0, configuration_builders_1.buildEmoji)(getInput(input_keys_1.INPUT_KEYS.EMOJI_LABELED_TITLE) === 'true', getInput(input_keys_1.INPUT_KEYS.BRANCH_MANAGEMENT_EMOJI)), tokens: (0, configuration_builders_1.buildTokens)(token), @@ -38746,6 +38747,7 @@ exports.INPUT_KEYS = { // Issue ISSUE_MANAGED_BRANCHES: 'issue-managed-branches', PRE_BRANCH_SDD: 'pre-branch-sdd', + VERIFICATION_SETUP: 'verification-setup', REOPEN_ISSUE_ON_PUSH: 'reopen-issue-on-push', DESIRED_ASSIGNEES_COUNT: 'desired-assignees-count', // Pull Request @@ -47753,6 +47755,8 @@ The setup runner profile selects placement; it does not install the project's la Declare reviewed version prefixes in \`.copilot/verification-prerequisites.json\` (schemaVersion 1, runtimes such as java 17, node 24, pnpm 10). Copilot checks declared versions in the credential-free verification environment before each configured verification command; a missing or incompatible runtime blocks publication. Without this optional declaration, existing verification behavior remains and no language is inferred. Java provisioning may use \`actions/setup-java\`; \`JAVA_HOME\` and \`PATH\` survive isolation, while agent credentials and tool-option injection variables do not. Do not add an unreviewed bootstrap or weaken verification to work around a missing runtime. Read [verification prerequisites and same-job setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites). +The optional repository-owned setup helper prepares each candidate before verification without editing generated workflows. Review \`.copilot/verification-setup.mjs\` onto trusted main before enabling \`VERIFICATION_SETUP=true\` (Action input \`verification-setup\`, disabled by default). Copilot executes captured trusted bytes before each command in a fresh credential-free profile; candidate changes cannot replace them. Setup failure blocks verification and commit/push. Read [repository-owned setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup). + For the full contributor flow, read [repository collaboration](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-collaboration.mdx). For drift and recovery, read [guidance troubleshooting](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-guidance-troubleshooting.mdx). `; } @@ -49168,6 +49172,7 @@ function createDefaultSetupConfiguration() { choreTree: 'chore', issueManagedBranches: true, preBranchSdd: false, + verificationSetup: false, reopenIssueOnPush: true, desiredAssigneesCount: 1, desiredReviewersCount: 1, @@ -49435,6 +49440,7 @@ function buildSetupRepositoryVariables(configuration) { add('CHORE_TREE', repository.choreTree); add('ISSUE_MANAGED_BRANCHES', repository.issueManagedBranches); add('PRE_BRANCH_SDD', repository.preBranchSdd); + add('VERIFICATION_SETUP', repository.verificationSetup ?? false); add('REOPEN_ISSUE_ON_PUSH', repository.reopenIssueOnPush); add('DESIRED_ASSIGNEES_COUNT', repository.desiredAssigneesCount); add('DESIRED_REVIEWERS_COUNT', repository.desiredReviewersCount); @@ -49497,6 +49503,7 @@ function buildSetupActionInputs(configuration) { 'chore-tree': repository.choreTree, 'issue-managed-branches': String(repository.issueManagedBranches), 'pre-branch-sdd': String(repository.preBranchSdd), + 'verification-setup': String(repository.verificationSetup ?? false), 'reopen-issue-on-push': String(repository.reopenIssueOnPush), 'desired-assignees-count': String(repository.desiredAssigneesCount), 'desired-reviewers-count': String(repository.desiredReviewersCount), @@ -49942,6 +49949,9 @@ function validateSetupConfiguration(configuration, options = {}) { || typeof configuration.repository.preBranchSdd !== 'boolean') { errors.push('issue-managed-branches and pre-branch-sdd must be boolean values.'); } + if (configuration.repository.verificationSetup !== undefined && typeof configuration.repository.verificationSetup !== 'boolean') { + errors.push('verification-setup must be a boolean value.'); + } if (configuration.repository.preBranchSdd && !configuration.repository.issueManagedBranches) { errors.push('pre-branch-sdd requires issue-managed-branches.'); } @@ -50344,6 +50354,7 @@ const repositoryFields = [ ['HOTFIX_TREE', 'hotfixTree'], ['RELEASE_TREE', 'releaseTree'], ['DOCS_TREE', 'docsTree'], ['CHORE_TREE', 'choreTree'], ['ISSUE_MANAGED_BRANCHES', 'issueManagedBranches'], ['PRE_BRANCH_SDD', 'preBranchSdd'], + ['VERIFICATION_SETUP', 'verificationSetup'], ['REOPEN_ISSUE_ON_PUSH', 'reopenIssueOnPush'], ['DESIRED_ASSIGNEES_COUNT', 'desiredAssigneesCount'], ['DESIRED_REVIEWERS_COUNT', 'desiredReviewersCount'], ['INACTIVITY_THRESHOLD_HOURS', 'inactivityThresholdHours'], ['REPOSITORY_LOCALE', 'repositoryLocale'], ['ISSUES_LOCALE', 'issueLocale'], @@ -51074,6 +51085,54 @@ function verificationRuntimeMatches(required, observed) { } +/***/ }), + +/***/ 65326: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.VerificationSetupError = exports.VERIFICATION_SETUP_HELP = exports.VERIFICATION_SETUP_LIMITS = exports.VERIFICATION_SETUP_PATH = void 0; +exports.parseVerificationSetupResult = parseVerificationSetupResult; +exports.VERIFICATION_SETUP_PATH = '.copilot/verification-setup.mjs'; +exports.VERIFICATION_SETUP_LIMITS = Object.freeze({ timeoutMs: 300000, outputBytes: 65536, resultBytes: 16384, scriptBytes: 65536 }); +exports.VERIFICATION_SETUP_HELP = 'https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup'; +/** Closed facts only. Provider/helper output and raw exception causes never cross this boundary. */ +class VerificationSetupError extends Error { + constructor(reason, sourceSha, exitCode) { + const sha = sourceSha && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(sourceSha) ? sourceSha : 'unavailable'; + const exit = Number.isInteger(exitCode) && exitCode >= 0 && exitCode <= 255 ? ` Exit status: ${exitCode}.` : ''; + super(`Repository setup blocked verification (${reason}). Review ${exports.VERIFICATION_SETUP_PATH} on the trusted main branch and rerun. Trusted source SHA: ${sha}.${exit} Verification, commit and push are withheld. Help: ${exports.VERIFICATION_SETUP_HELP}`); + this.reason = reason; + this.exitCode = exitCode; + this.name = 'VerificationSetupError'; + } +} +exports.VerificationSetupError = VerificationSetupError; +function record(value) { + return !!value && typeof value === 'object' && !Array.isArray(value); +} +function pathString(value) { + return typeof value === 'string' && value.length > 0 && value.length <= 2048 + && ![...value].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127); +} +/** Filesystem containment is checked by the adapter, after this closed schema is parsed. */ +function parseVerificationSetupResult(value) { + if (!record(value) || value.schemaVersion !== 1 || Object.keys(value).some(key => !['schemaVersion', 'path', 'environment'].includes(key))) { + throw new VerificationSetupError('result-invalid'); + } + const paths = value.path === undefined ? [] : value.path; + const environment = value.environment === undefined ? {} : value.environment; + if (!Array.isArray(paths) || paths.length > 8 || !paths.every(pathString) || new Set(paths).size !== paths.length + || !record(environment) || Object.keys(environment).some(key => key !== 'JAVA_HOME') + || ('JAVA_HOME' in environment && !pathString(environment.JAVA_HOME))) { + throw new VerificationSetupError('result-invalid'); + } + return Object.freeze({ path: Object.freeze([...paths]), ...(environment.JAVA_HOME ? { javaHome: environment.JAVA_HOME } : {}) }); +} + + /***/ }), /***/ 43193: @@ -51279,6 +51338,18 @@ function bindSddEvidenceReadPorts(ports) { } +/***/ }), + +/***/ 26509: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.DISABLED_VERIFICATION_SETUP = void 0; +exports.DISABLED_VERIFICATION_SETUP = Object.freeze({ kind: 'disabled' }); + + /***/ }), /***/ 84387: @@ -63549,6 +63620,7 @@ exports.runVerifyCommands = runVerifyCommands; const logging_ports_1 = __nccwpck_require__(6152); const verify_command_policy_1 = __nccwpck_require__(96031); const verification_runtime_policy_1 = __nccwpck_require__(89347); +const verification_setup_policy_1 = __nccwpck_require__(65326); async function runVerifyCommands(commands, execute) { for (const command of commands) { const result = await executeVerifyCommand(command, execute); @@ -63568,7 +63640,7 @@ async function executeVerifyCommand(command, execute) { : { success: false, failedCommand: formatCommandForDiagnostics(parsed) }; } catch (error) { - if (error instanceof verification_runtime_policy_1.VerificationPrerequisiteError) { + if (error instanceof verification_runtime_policy_1.VerificationPrerequisiteError || error instanceof verification_setup_policy_1.VerificationSetupError) { return { success: false, failedCommand: formatCommandForDiagnostics(parsed), error: `${error.message} Verification command: ${formatCommandForDiagnostics(parsed)}.` }; } @@ -69508,6 +69580,7 @@ class Execution { this.currentIssueWorkflowAdmission = components.issueWorkflowAdmission; this.currentConfiguration.issueWorkflowProfileDigest = components.issueWorkflowProfileDigest; this.preBranchSdd = components.preBranchSdd ?? false; + this.verificationSetup = components.verificationSetup ?? false; } } exports.Execution = Execution; @@ -86429,12 +86502,14 @@ function createDeploymentOrchestrationUseCase(issueDescriptionQueryPort, publica }); } const copilot_history_port_factory_1 = __nccwpck_require__(12321); -function createIssueCommentUseCaseCompositionRoot(binding) { +const verification_setup_snapshot_1 = __nccwpck_require__(62148); +const verification_setup_ports_1 = __nccwpck_require__(26509); +function createIssueCommentUseCaseCompositionRoot(binding, setup = verification_setup_ports_1.DISABLED_VERIFICATION_SETUP) { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(binding); const findings = (0, agent_capability_composition_root_1.createFindingsQueryPort)(); const language = (0, agent_capability_composition_root_1.createLanguageQueryPort)(); const fixer = (0, agent_capability_composition_root_1.createFixerQueryPort)(); - const gitCommit = new git_commit_adapter_1.GitCommitAdapter(); + const gitCommit = new git_commit_adapter_1.GitCommitAdapter(undefined, undefined, setup); const authenticatedUser = (0, authenticated_user_composition_root_1.createAuthenticatedUserCompositionRoot)(); const bugbotGit = new bound_bugbot_git_mutation_adapter_1.BoundBugbotGitMutationAdapter(gitCommit, authenticatedUser, binding.token); const pullRequestDescription = new update_pull_request_description_use_case_1.UpdatePullRequestDescriptionUseCase((0, lifecycle_capability_port_binding_1.bindPullRequestDescription)(new pull_request_lifecycle_repository_1.PullRequestLifecycleRepository((0, github_pull_request_client_factory_1.createPullRequestLifecycleClient)()), binding, new pull_request_changes_repository_1.PullRequestChangesRepository((0, github_pull_request_client_factory_1.createPullRequestChangesClient)())), (0, shared_capability_port_binding_1.bindIssueDescriptionQuery)((0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), binding), (0, shared_capability_port_binding_1.bindOrganizationMembers)((0, organization_members_composition_root_1.createOrganizationMembersCompositionRoot)(), binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()); @@ -86445,12 +86520,12 @@ function createIssueCommentUseCaseCompositionRoot(binding) { catalogResolver: new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language), }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, (0, issue_use_case_composition_root_1.createIssueUseCaseCompositionRoot)(binding), rememberBranchPorts(binding, bugbot.scm.context), (0, pull_request_status_composition_root_1.createPullRequestStatusReadPorts)(binding, bugbot.scm.context), (0, copilot_history_port_factory_1.createCopilotHistoryReadPort)(bugbot.scm.context, process.env.GITHUB_SERVER_URL ?? 'https://github.com'), (0, sdd_evidence_composition_root_1.createSddEvidenceQueryPort)(binding, bugbot.scm.context), (0, sdd_diff_composition_root_1.createSddDiffQueryPort)(binding, bugbot.scm.context), (0, issue_completion_composition_root_2.createNativeStatusReadPort)(binding), (0, github_runtime_doctor_composition_root_1.createGithubRuntimeDoctorPorts)(binding, bugbot.scm.context)); } -function createPullRequestReviewCommentUseCaseCompositionRoot(binding) { +function createPullRequestReviewCommentUseCaseCompositionRoot(binding, setup = verification_setup_ports_1.DISABLED_VERIFICATION_SETUP) { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(binding); const findings = (0, agent_capability_composition_root_1.createFindingsQueryPort)(); const language = (0, agent_capability_composition_root_1.createLanguageQueryPort)(); const fixer = (0, agent_capability_composition_root_1.createFixerQueryPort)(); - const gitCommit = new git_commit_adapter_1.GitCommitAdapter(); + const gitCommit = new git_commit_adapter_1.GitCommitAdapter(undefined, undefined, setup); const authenticatedUser = (0, authenticated_user_composition_root_1.createAuthenticatedUserCompositionRoot)(); const bugbotGit = new bound_bugbot_git_mutation_adapter_1.BoundBugbotGitMutationAdapter(gitCommit, authenticatedUser, binding.token); const pullRequestDescription = new update_pull_request_description_use_case_1.UpdatePullRequestDescriptionUseCase((0, lifecycle_capability_port_binding_1.bindPullRequestDescription)(new pull_request_lifecycle_repository_1.PullRequestLifecycleRepository((0, github_pull_request_client_factory_1.createPullRequestLifecycleClient)()), binding, new pull_request_changes_repository_1.PullRequestChangesRepository((0, github_pull_request_client_factory_1.createPullRequestChangesClient)())), (0, shared_capability_port_binding_1.bindIssueDescriptionQuery)((0, issue_content_composition_root_1.createIssueContentCompositionRoot)(), binding), (0, shared_capability_port_binding_1.bindOrganizationMembers)((0, organization_members_composition_root_1.createOrganizationMembersCompositionRoot)(), binding), (0, agent_capability_composition_root_1.createFindingsQueryPort)()); @@ -86477,7 +86552,7 @@ function createMainRunRouteCompositionRoot(projectBoardCommandPort, surface, set return singleAction.invoke(execution); }, "issue-comment": async (execution) => { - issueComment ?? (issueComment = createIssueCommentUseCaseCompositionRoot(bugbotBinding(execution))); + issueComment ?? (issueComment = createIssueCommentUseCaseCompositionRoot(bugbotBinding(execution), (0, verification_setup_snapshot_1.captureVerificationSetup)(execution.verificationSetup, execution.branches.main))); return issueComment.invoke(execution); }, issue: async (execution) => { @@ -86485,7 +86560,7 @@ function createMainRunRouteCompositionRoot(projectBoardCommandPort, surface, set return issue.invoke(execution); }, "pull-request-review-comment": async (execution) => { - pullRequestReviewComment ?? (pullRequestReviewComment = createPullRequestReviewCommentUseCaseCompositionRoot(bugbotBinding(execution))); + pullRequestReviewComment ?? (pullRequestReviewComment = createPullRequestReviewCommentUseCaseCompositionRoot(bugbotBinding(execution), (0, verification_setup_snapshot_1.captureVerificationSetup)(execution.verificationSetup, execution.branches.main))); return pullRequestReviewComment.invoke(execution); }, "pull-request": async (execution) => { @@ -87427,6 +87502,8 @@ const git_authentication_environment_1 = __nccwpck_require__(16535); const untrusted_command_environment_1 = __nccwpck_require__(2304); const git_hook_isolation_1 = __nccwpck_require__(39764); const verification_runtime_preflight_1 = __nccwpck_require__(23100); +const verification_setup_ports_1 = __nccwpck_require__(26509); +const verification_setup_runner_1 = __nccwpck_require__(11709); class GitCommitAdapter { constructor(executeCommand = (program, args, options) => options ? exec.exec(program, args, { @@ -87436,9 +87513,10 @@ class GitCommitAdapter { ...(options.stdout ? { listeners: { stdout: options.stdout } } : {}), ...(options.env ? { env: options.env } : {}), }) - : exec.exec(program, args), checkPrerequisites = verification_runtime_preflight_1.assertVerificationRuntimePrerequisites) { + : exec.exec(program, args), checkPrerequisites = verification_runtime_preflight_1.assertVerificationRuntimePrerequisites, verificationSetup = verification_setup_ports_1.DISABLED_VERIFICATION_SETUP) { this.executeCommand = executeCommand; this.checkPrerequisites = checkPrerequisites; + this.verificationSetup = verificationSetup; } async execute(program, args, options) { if (options?.verifyRuntimePrerequisites && !options.untrusted) @@ -87449,8 +87527,10 @@ class GitCommitAdapter { throw new Error('Untrusted command execution does not accept a caller-supplied environment.'); const runtime = (0, untrusted_command_environment_1.prepareUntrustedCommandEnvironment)(); try { - if (options.verifyRuntimePrerequisites) + if (options.verifyRuntimePrerequisites) { + await (0, verification_setup_runner_1.prepareRepositoryVerification)(this.verificationSetup, runtime.environment); this.checkPrerequisites(runtime.environment); + } return await this.executeCommand(program, args, { ...(options.stdout ? { stdout: options.stdout } : {}), env: runtime.environment, @@ -88797,7 +88877,7 @@ const ALLOWED_VARIABLES = [ * credentials or paths to local agent authentication stores. */ function prepareUntrustedCommandEnvironment(source = process.env, platform = process.platform) { - const runtimeHome = (0, agent_private_install_root_1.createPrivateVerificationRuntimeRoot)(); + const runtimeHome = (0, node_fs_1.realpathSync)((0, agent_private_install_root_1.createPrivateVerificationRuntimeRoot)()); const environment = { HOME: runtimeHome }; for (const variable of ALLOWED_VARIABLES) { const value = source[variable]; @@ -88951,6 +89031,245 @@ function checkRuntime(requirement, environment, run, platform, job) { } +/***/ }), + +/***/ 43966: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.runVerificationSetupProcess = runVerificationSetupProcess; +const node_child_process_1 = __nccwpck_require__(17718); +const agent_trusted_system_tools_1 = __nccwpck_require__(76049); +const verification_setup_policy_1 = __nccwpck_require__(65326); +/** All output is counted and discarded. Fixed production limits; short injected limits are for deterministic tests. */ +function runVerificationSetupProcess(node, script, cwd, env, sourceSha, limits = verification_setup_policy_1.VERIFICATION_SETUP_LIMITS) { + return new Promise((resolve, reject) => { + const child = (0, node_child_process_1.spawn)(node, [script], { cwd, env, shell: false, windowsHide: true, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] }); + let failure; + let outputBytes = 0; + const terminate = () => { + if (!child.pid) + return; + try { + if (process.platform === 'win32') + (0, node_child_process_1.spawnSync)((0, agent_trusted_system_tools_1.trustedWindowsSystemTool)('taskkill.exe'), ['/PID', String(child.pid), '/T', '/F'], { env, windowsHide: true, timeout: 5000, stdio: 'ignore', shell: false }); + else + process.kill(-child.pid, 'SIGKILL'); + } + catch { + child.kill('SIGKILL'); + } + }; + const timer = setTimeout(() => { failure ?? (failure = 'timeout'); terminate(); }, limits.timeoutMs); + const cancel = () => { failure ?? (failure = 'cancelled'); terminate(); }; + process.once('SIGINT', cancel); + process.once('SIGTERM', cancel); + const capture = (data) => { + outputBytes += data.length; + if (outputBytes > limits.outputBytes) { + failure ?? (failure = 'output-limit'); + terminate(); + } + }; + child.stdout.on('data', capture); + child.stderr.on('data', capture); + child.on('error', () => { failure ?? (failure = 'process-failed'); }); + child.on('close', code => { + clearTimeout(timer); + process.removeListener('SIGINT', cancel); + process.removeListener('SIGTERM', cancel); + terminate(); + if (failure || code !== 0) + reject(new verification_setup_policy_1.VerificationSetupError(failure ?? 'process-failed', sourceSha, code ?? undefined)); + else + resolve(); + }); + }); +} + + +/***/ }), + +/***/ 23358: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.applyVerificationSetupResult = applyVerificationSetupResult; +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const verification_setup_policy_1 = __nccwpck_require__(65326); +function contained(root, path) { + const remainder = (0, node_path_1.relative)(root, path); + return remainder === '' || (remainder !== '..' && !remainder.startsWith(`..${node_path_1.sep}`) && !(0, node_path_1.isAbsolute)(remainder)); +} +/** Accept only existing contained directories, checking both lexical paths and symlink targets. */ +function approvedDirectory(value, roots) { + if (!(0, node_path_1.isAbsolute)(value) || value.includes(node_path_1.delimiter) || value.split(/[\\/]/u).includes('..') + || !roots.some(root => contained(root, value))) + throw new verification_setup_policy_1.VerificationSetupError('result-invalid'); + const actual = (0, node_fs_1.realpathSync)(value); + if (!roots.some(root => contained(root, actual)) || !(0, node_fs_1.lstatSync)(actual).isDirectory()) + throw new verification_setup_policy_1.VerificationSetupError('result-invalid'); + return actual; +} +function applyVerificationSetupResult(file, environment, roots) { + let descriptor; + try { + let before; + try { + before = (0, node_fs_1.lstatSync)(file, { bigint: true }); + } + catch (error) { + if (error.code === 'ENOENT') + return; + throw error; + } + if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.size > BigInt(verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.resultBytes)) + throw new Error(); + descriptor = (0, node_fs_1.openSync)(file, node_fs_1.constants.O_RDONLY | (node_fs_1.constants.O_NOFOLLOW ?? 0)); + const opened = (0, node_fs_1.fstatSync)(descriptor, { bigint: true }); + if (!opened.isFile() || opened.nlink !== 1n || opened.dev !== before.dev || opened.ino !== before.ino + || opened.size !== before.size || opened.mtimeNs !== before.mtimeNs) + throw new Error(); + const bytes = Buffer.alloc(verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.resultBytes + 1); + const size = (0, node_fs_1.readSync)(descriptor, bytes, 0, bytes.length, 0); + if (size > verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.resultBytes) + throw new Error(); + const after = (0, node_fs_1.fstatSync)(descriptor, { bigint: true }); + if (after.size !== opened.size || after.mtimeNs !== opened.mtimeNs) + throw new Error(); + const result = (0, verification_setup_policy_1.parseVerificationSetupResult)(JSON.parse(bytes.subarray(0, size).toString('utf8'))); + const paths = result.path.map(value => approvedDirectory(value, roots)); + const javaHome = result.javaHome ? approvedDirectory(result.javaHome, roots) : undefined; + if (paths.length) + environment.PATH = [...paths, ...(environment.PATH ? [environment.PATH] : [])].join(node_path_1.delimiter); + if (javaHome) + environment.JAVA_HOME = javaHome; + } + catch { + throw new verification_setup_policy_1.VerificationSetupError('result-invalid'); + } + finally { + if (descriptor !== undefined) + (0, node_fs_1.closeSync)(descriptor); + } +} + + +/***/ }), + +/***/ 11709: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.prepareRepositoryVerification = prepareRepositoryVerification; +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const verification_setup_policy_1 = __nccwpck_require__(65326); +const verification_setup_result_1 = __nccwpck_require__(23358); +const verification_setup_process_1 = __nccwpck_require__(43966); +const logging_ports_1 = __nccwpck_require__(6152); +const verification_setup_policy_2 = __nccwpck_require__(65326); +/** Called once per verification command, on its fresh private profile, before runtime probes. */ +async function prepareRepositoryVerification(setup, environment) { + if (setup.kind === 'disabled') + return; + if (setup.kind === 'blocked') + throw new verification_setup_policy_1.VerificationSetupError(setup.reason, setup.sourceSha); + try { + if ((0, node_fs_1.realpathSync)(process.cwd()) !== setup.workspace) + throw new verification_setup_policy_1.VerificationSetupError('workspace-changed', setup.sourceSha); + const profile = (0, node_fs_1.realpathSync)(environment.HOME); + const script = (0, node_path_1.join)(profile, 'verification-setup.mjs'); + const result = (0, node_path_1.join)(profile, 'verification-setup-result.json'); + (0, node_fs_1.writeFileSync)(script, setup.script, { mode: 0o600, flag: 'wx' }); + await (0, verification_setup_process_1.runVerificationSetupProcess)(setup.node, script, setup.workspace, { + ...environment, + COPILOT_VERIFICATION_SETUP_PROTOCOL: '1', + COPILOT_VERIFICATION_SETUP_RESULT: result, + COPILOT_VERIFICATION_SETUP_SOURCE_SHA: setup.sourceSha, + }, setup.sourceSha); + (0, verification_setup_result_1.applyVerificationSetupResult)(result, environment, [setup.workspace, profile, ...(setup.toolRoot ? [setup.toolRoot] : [])]); + (0, logging_ports_1.logInfo)(`Repository setup prepared (${verification_setup_policy_2.VERIFICATION_SETUP_PATH}; trusted source SHA: ${setup.sourceSha}).`); + } + catch (error) { + throw new verification_setup_policy_1.VerificationSetupError(error instanceof verification_setup_policy_1.VerificationSetupError ? error.reason : 'process-failed', setup.sourceSha, error instanceof verification_setup_policy_1.VerificationSetupError ? error.exitCode : undefined); + } +} + + +/***/ }), + +/***/ 62148: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.captureVerificationSetup = captureVerificationSetup; +const node_child_process_1 = __nccwpck_require__(17718); +const node_crypto_1 = __nccwpck_require__(6005); +const node_fs_1 = __nccwpck_require__(87561); +const verification_setup_ports_1 = __nccwpck_require__(26509); +const verification_setup_policy_1 = __nccwpck_require__(65326); +const untrusted_command_environment_1 = __nccwpck_require__(2304); +/** No network, agent, worktree-file reads, or credentials. Acquisition errors are deferred until verification. */ +function captureVerificationSetup(enabled, mainBranch, root = process.cwd()) { + if (!enabled) + return verification_setup_ports_1.DISABLED_VERIFICATION_SETUP; + let sourceSha; + let runtime; + let snapshot; + try { + runtime = (0, untrusted_command_environment_1.prepareUntrustedCommandEnvironment)(); + const environment = runtime.environment; + if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/u.test(mainBranch) || mainBranch.includes('..') + || mainBranch.includes('//') || mainBranch.endsWith('/') || mainBranch.endsWith('.lock')) + throw new Error(); + const workspace = (0, node_fs_1.realpathSync)(root); + const git = (...args) => { + const result = (0, node_child_process_1.spawnSync)('git', ['--no-replace-objects', ...args], { cwd: workspace, env: environment, + shell: false, windowsHide: true, timeout: 5000, maxBuffer: verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.scriptBytes + 4096 }); + if (result.error || result.status !== 0) + throw new Error(); + return result.stdout; + }; + const candidateSha = git('rev-parse', '--verify', `refs/remotes/origin/${mainBranch}^{commit}`).toString('utf8').trim(); + if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(candidateSha)) + throw new Error(); + sourceSha = candidateSha; + const entry = git('ls-tree', '-z', sourceSha, '--', verification_setup_policy_1.VERIFICATION_SETUP_PATH).toString('utf8'); + const match = /^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64})\t\.copilot\/verification-setup\.mjs\0$/u.exec(entry); + if (!match) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + const bytes = git('cat-file', 'blob', match[2]); + const digest = (0, node_crypto_1.createHash)(match[2].length === 40 ? 'sha1' : 'sha256').update(`blob ${bytes.length}\0`).update(bytes).digest('hex'); + const script = bytes.toString('utf8'); + if (!bytes.length || bytes.length > verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.scriptBytes || digest !== match[2] + || !Buffer.from(script).equals(bytes) || script.includes('\0')) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + const toolRoot = environment.RUNNER_TOOL_CACHE ? (0, node_fs_1.realpathSync)(environment.RUNNER_TOOL_CACHE) : undefined; + snapshot = Object.freeze({ kind: 'ready', script, sourceSha, workspace, node: process.execPath, ...(toolRoot ? { toolRoot } : {}) }); + } + catch (error) { + snapshot = Object.freeze({ kind: 'blocked', reason: error instanceof verification_setup_policy_1.VerificationSetupError ? error.reason : 'source-unavailable', ...(sourceSha ? { sourceSha } : {}) }); + } + try { + runtime?.cleanup(); + } + catch { + snapshot = Object.freeze({ kind: 'blocked', reason: 'source-unavailable', ...(sourceSha ? { sourceSha } : {}) }); + } + return snapshot; +} + + /***/ }), /***/ 92540: diff --git a/docs/configuration.mdx b/docs/configuration.mdx index a987ad8aa..83793eb09 100644 --- a/docs/configuration.mdx +++ b/docs/configuration.mdx @@ -63,6 +63,7 @@ PR bot approval uses the versioned `pullRequestApproval` setup object and `PR_AP ## Branch Management - `issue-managed-branches`: Create and verify a linked branch after `in-progress` (default: "true") + - `verification-setup`: Run the trusted main-branch repository setup helper before each configured verification command (default: "false"). See [repository-owned setup](/security-operations/operations/verification#repository-owned-setup). - `pre-branch-sdd`: Require a validated SDD before implementation for eligible issues (default: "false") - `branch-management-emoji`: Emoji for branched issues (default: "🧑‍💻") diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index bc828dca3..bac0ffe85 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -633,3 +633,21 @@ The trusted credential health adapter is GET-only and pins packaged workflow content, default SHA, run identity, freshness and unchanged Secret metadata. It never inherits the legacy bootstrap or dispatch adapter. Architecture/cycle, SDK contract, replay, drift and presentation tests enforce these boundaries. + +## Repository verification setup + +The optional [repository-owned helper](/security-operations/operations/verification#repository-owned-setup) +is captured once at comment-route composition from the configured trusted main +commit. The immutable semantic contract contains only regular-file bytes, source +SHA and trusted execution roots. Acquisition failures are deferred; actor, fork, +native-head and workspace guards still decide whether verification is reached. + +Both branch synchronization and shared automated commit preflight request the same +`untrusted + verifyRuntimePrerequisites` Git-port intent. Its adapter creates a fresh +private environment, runs the snapshot, validates the bounded closed result, probes +runtime prerequisites, executes the command and cleans up. The pure policy owns +schema/limits/sanitized diagnostics; infrastructure owns Git, filesystem, process +timeouts and cancellation. Agent credential profile cleanup precedes this phase. +No entrypoint duplicates the process runner, and no command/candidate reuses setup +readiness. Structural workflow/configuration tests keep all six source/distributed +event workflows consistent; local real-Git tests enforce the same-ref push gate. diff --git a/docs/security-operations/operations/troubleshooting.mdx b/docs/security-operations/operations/troubleshooting.mdx index d8f38bd1a..989d29d92 100644 --- a/docs/security-operations/operations/troubleshooting.mdx +++ b/docs/security-operations/operations/troubleshooting.mdx @@ -583,3 +583,27 @@ successful skip. A version field alone does not prove that the stored branch or operation facts are valid. Authorization and temporary GitHub read failures retain their own error categories; follow their permission/provider recovery instead of deleting configuration. + +## Repository setup blocks verification + +A `Repository setup blocked verification` detail means the helper did not authorize +this command environment. Verification and commit/push are withheld; branch sync +uses its existing merge abort. Inspect the closed cause and pinned trusted SHA in +the current run, then correct `.copilot/verification-setup.mjs` on `main-branch`: + +| Cause | Recovery | +| --- | --- | +| `source-unavailable` | Make the configured trusted origin ref available in the full checkout. | +| `helper-invalid` | Review a regular standalone Node ESM helper, at most 64 KiB, onto trusted main. | +| `workspace-changed` | Retry from the expected candidate checkout. | +| `process-failed` | Correct the helper or its locked dependency installation; inspect only private operator diagnostics. | +| `timeout` / `output-limit` | Fit setup within 300 seconds and 64 KiB combined output; no limit override or automatic retry exists. | +| `cancelled` | Retry the authorized operation after cancellation is resolved. | +| `result-invalid` | Use protocol v1, at most 16 KiB, only approved existing PATH directories and JAVA_HOME. | + +The raw helper output is intentionally withheld. A helper committed only on the +candidate branch does not replace the captured main version. A missing helper with +`VERIFICATION_SETUP=true` blocks checks; the disabled default preserves legacy +behavior. See [repository-owned setup](/security-operations/operations/verification#repository-owned-setup) +for Python/pnpm examples, credential boundaries and update preservation. A separate +passing CI job does not prepare this command environment. diff --git a/docs/security-operations/operations/verification.mdx b/docs/security-operations/operations/verification.mdx index 394eafca2..07c344ebf 100644 --- a/docs/security-operations/operations/verification.mdx +++ b/docs/security-operations/operations/verification.mdx @@ -32,8 +32,10 @@ The source workflow validator accepts the setup's literal `runs-on: {group: vypd The setup's runner profile chooses **where** a job runs. It does not install a consumer project's language runtimes or dependencies. A passing application CI job does not prepare the separate `issue_comment`, PR-comment or automated-fix -job. Each job that runs `bugbot-fix-verify-commands` must prepare its own tools -before Copilot, including for `/copilot sync-branch --no-agent`. +job. Each job that runs `bugbot-fix-verify-commands` must prepare its own tools, +including for `/copilot sync-branch --no-agent`. Use the [repository-owned setup +helper](#repository-owned-setup) to prepare the candidate without editing generated +workflows, or retain your reviewed same-job bootstrap. For a Kotlin/JVM project, commit a reviewed `.copilot/verification-prerequisites.json`: @@ -117,6 +119,122 @@ commit or push; automated fixes also withhold their commit and push. Correct the declaration or bootstrap, then retry the original operation. Dry runs and already-aligned branches do not execute verification or prove runtime readiness. +## Repository-owned setup + +To prepare project dependencies without editing generated workflows: + +1. Review a standalone `.copilot/verification-setup.mjs` onto your configured + `main-branch` (normally production). Candidate-branch helpers cannot activate + themselves. Keep runtime versions and dependency lockfiles under review. +2. Set Repository Variable `VERIFICATION_SETUP` to `true`. Generated source and + consumer event workflows forward it as the boolean `verification-setup` Action + input. For setup config files, use `repository: { verificationSetup: true }`. + The default is `false`; paths, commands, arguments and limits are fixed. +3. Retry the authorized modification or `/copilot sync-branch --no-agent`. + Copilot runs the trusted snapshot on the actual candidate after merge/fix + changes, immediately before **each** configured command and its runtime probes. + +Setup captures immutable regular-file bytes and the source commit SHA before an +agent can modify the workspace. The configured `origin/` ref must be +available locally; generated comment workflows use a full checkout. The snapshot +runs with the Action's Node executable, `shell: false`, no user arguments and cwd +at the candidate root. It is copied into a private directory, so relative module +imports do not load siblings from your repository. Use Node built-ins and explicit +subprocess argument arrays. The standalone helper is limited to 64 KiB. + +The helper receives a fresh private HOME and the existing allowlisted verification +environment, with these additional non-secret protocol fields: + +| Variable | Value | +| --- | --- | +| `COPILOT_VERIFICATION_SETUP_PROTOCOL` | `1` | +| `COPILOT_VERIFICATION_SETUP_SOURCE_SHA` | Pinned trusted source commit | +| `COPILOT_VERIFICATION_SETUP_RESULT` | Optional private JSON output-file path | + +No PAT, model/cloud credentials, Git write credentials, agent auth-store variables +or prior agent credential profile are supplied. Agent profiles are removed before +verification begins. This retains the trusted-runner boundary; it is not an OS +sandbox against malicious maintainer code. Review installers as executable code. +Do not rely on authenticated caches, ambient user-home settings or a separate CI job. + +A helper may finish without a result, leaving the environment unchanged. To add +prepared tools, write this closed result format to the private result path: + +```json +{"schemaVersion":1,"path":["/absolute/approved/tools/bin"],"environment":{"JAVA_HOME":"/absolute/approved/jdk"}} +``` + +`path` and `environment` are optional. Only up to eight unique absolute PATH +directories (2,048 characters each) and `JAVA_HOME` are supported. Every directory +must already exist inside the canonical candidate workspace, private HOME or +captured trusted `RUNNER_TOOL_CACHE`. Escaping symlinks, traversal, control +characters, PATH delimiter injection, null values and unknown fields are rejected. +The result must be a regular, nonlinked JSON file of at most 16 KiB. No arbitrary +environment variables are accepted. stdout/stderr never set policy and are never +published raw. Protocol metadata is not passed to the verification command. + +For Python on a Linux/macOS runner with the declared Python version installed, +this helper creates a fresh private virtual environment and installs the candidate's +reviewed requirements lockfile, including pytest. The lockfile must pin all +transitive dependencies and hashes. The command can remain `python3 -m pytest`: + +```javascript +import { execFileSync } from 'node:child_process'; +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const venv = join(process.env.HOME, 'venv'); +execFileSync('python3', ['-m', 'venv', venv], { stdio: 'inherit' }); +const bin = join(venv, 'bin'); +execFileSync(join(bin, 'python3'), [ + '-m', 'pip', 'install', '--require-hashes', '-r', 'requirements-test.lock', +], { stdio: 'inherit' }); +writeFileSync(process.env.COPILOT_VERIFICATION_SETUP_RESULT, + JSON.stringify({ schemaVersion: 1, path: [bin] })); +``` + +For Node/pnpm on a Linux/macOS runner, explicitly provision the pinned pnpm version +instead of relying on an implicit Corepack download. This alternative requires +`npm` on the reviewed runner and uses the candidate's `pnpm-lock.yaml`: + +```javascript +import { execFileSync } from 'node:child_process'; +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const tools = join(process.env.HOME, 'tools'); +execFileSync('npm', ['install', '--prefix', tools, '--ignore-scripts', + '--no-audit', '--no-fund', 'pnpm@10.12.4'], { stdio: 'inherit' }); +execFileSync(process.execPath, [join(tools, 'node_modules/pnpm/bin/pnpm.cjs'), + 'install', '--frozen-lockfile', '--ignore-scripts'], { stdio: 'inherit' }); +writeFileSync(process.env.COPILOT_VERIFICATION_SETUP_RESULT, + JSON.stringify({ schemaVersion: 1, path: [join(tools, 'node_modules/.bin')] })); +``` + +These are alternative project helpers, not universal installers. Adapt your +platform's executable names and reviewed runtime versions. Windows Python uses +`Scripts` and `python.exe`; invoke package-manager scripts through Node rather +than passing `.cmd` names to shell-free Node subprocesses. Keep generated caches +ignored, never modify tracked source/lockfiles during verification, and validate +lockfiles before reusing any workspace cache. Private tools are recreated for each +command; there is no readiness cache across commands or candidates. + +Each invocation has a fixed **300-second timeout**, **64 KiB combined stdout/stderr +ceiling**, and **16 KiB result ceiling**. There are no overrides or automatic +retries. Setup failure, timeout, cancellation or invalid output stops preflight and +verification, withholds commit/push and follows existing sync abort behavior. The +existing maximum of twenty verification commands remains. Diagnostics contain +only a closed cause, fixed helper path, pinned SHA and safe exit status; success +logs the path and source SHA. Existing operation comments and run navigation carry +the result without per-command comments. + +Read-only questions/status, dry runs, already-aligned branches and empty command +lists never execute the helper. Acquisition failures are deferred until actual +verification. With the input disabled, existing prerequisites and behavior remain. +Updating Copilot preserves this consumer-owned file. To roll back the opt-in, set +`VERIFICATION_SETUP` to `false`; keep the same verification commands and provision +their dependencies through your reviewed runner/bootstrap. + ## Reused credential evidence The reuse journey performs GET-only inspection of trusted installed health diff --git a/scripts/coverage-budgets.json b/scripts/coverage-budgets.json index 4336d03af..acfff75f8 100644 --- a/scripts/coverage-budgets.json +++ b/scripts/coverage-budgets.json @@ -900,6 +900,30 @@ } ], "successMessage": "approval evidence diagnostics coverage: PASS (95% lines/statements, 90% branches/functions)" + }, + { + "name": "Verification setup", + "missingEntryLabel": "verification setup", + "rules": [ + { + "files": [ + "src/application/policies/verification_setup_policy.ts" + ], + "mode": "each", + "thresholdProfile": "default" + }, + { + "files": [ + "src/infrastructure/verification_setup_snapshot.ts", + "src/infrastructure/verification_setup_result.ts", + "src/infrastructure/verification_setup_process.ts", + "src/infrastructure/verification_setup_runner.ts" + ], + "mode": "aggregate", + "thresholdProfile": "default" + } + ], + "successMessage": "verification setup coverage: PASS (95% lines/statements, 90% branches/functions)" } ] } diff --git a/scripts/validate-workflow-contract.cjs b/scripts/validate-workflow-contract.cjs index 64a69df77..010d93940 100644 --- a/scripts/validate-workflow-contract.cjs +++ b/scripts/validate-workflow-contract.cjs @@ -1387,16 +1387,18 @@ function assertIssueBranchConfigurationInputs(file, workflow) { ? { 'issue-managed-branches': "${{ vars.ISSUE_MANAGED_BRANCHES || 'true' }}", 'pre-branch-sdd': "${{ vars.PRE_BRANCH_SDD || 'false' }}", + 'verification-setup': "${{ vars.VERIFICATION_SETUP || 'false' }}", } : { 'issue-managed-branches': branches.issueManagedBranches, 'pre-branch-sdd': branches.preBranchSdd, + 'verification-setup': "${{ vars.VERIFICATION_SETUP || 'false' }}", }; const actionSteps = Object.values(workflow.jobs ?? {}).flatMap(job => job?.steps ?? []) .filter(isCopilotAction); if (actionSteps.length === 0 || actionSteps.some(step => Object.entries(expected).some(([input, value]) => step.with?.[input] !== value))) { - throw new Error(`${relativeFile} must pass consistent issue-managed-branches and pre-branch-sdd inputs to every Copilot callback.`); + throw new Error(`${relativeFile} must pass consistent issue-managed-branches, pre-branch-sdd and verification-setup inputs to every Copilot callback.`); } } diff --git a/setup/workflows/copilot_commit.yml b/setup/workflows/copilot_commit.yml index e289a0932..bd3dd0718 100644 --- a/setup/workflows/copilot_commit.yml +++ b/setup/workflows/copilot_commit.yml @@ -43,6 +43,7 @@ jobs: # Before Copilot, provision project runtimes/dependencies in this same job when # repository verification is configured. A separate CI job does not prepare it. # Declare reviewed versions in .copilot/verification-prerequisites.json. + # Optional trusted-main setup: VERIFICATION_SETUP=true and .copilot/verification-setup.mjs. # https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites - uses: vypdev/copilot@v3 with: @@ -62,6 +63,7 @@ jobs: chore-tree: ${{ vars.CHORE_TREE || 'chore' }} issue-managed-branches: ${{ vars.ISSUE_MANAGED_BRANCHES || 'true' }} pre-branch-sdd: ${{ vars.PRE_BRANCH_SDD || 'false' }} + verification-setup: ${{ vars.VERIFICATION_SETUP || 'false' }} reopen-issue-on-push: ${{ vars.REOPEN_ISSUE_ON_PUSH || 'true' }} desired-assignees-count: ${{ vars.DESIRED_ASSIGNEES_COUNT || '1' }} desired-reviewers-count: ${{ vars.DESIRED_REVIEWERS_COUNT || '1' }} diff --git a/setup/workflows/copilot_issue.yml b/setup/workflows/copilot_issue.yml index 6955abe07..06b17345e 100644 --- a/setup/workflows/copilot_issue.yml +++ b/setup/workflows/copilot_issue.yml @@ -21,6 +21,7 @@ jobs: # Before Copilot, provision project runtimes/dependencies in this same job when # repository verification is configured. A separate CI job does not prepare it. # Declare reviewed versions in .copilot/verification-prerequisites.json. + # Optional trusted-main setup: VERIFICATION_SETUP=true and .copilot/verification-setup.mjs. # https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites - uses: vypdev/copilot@v3 with: @@ -39,6 +40,7 @@ jobs: chore-tree: ${{ vars.CHORE_TREE || 'chore' }} issue-managed-branches: ${{ vars.ISSUE_MANAGED_BRANCHES || 'true' }} pre-branch-sdd: ${{ vars.PRE_BRANCH_SDD || 'false' }} + verification-setup: ${{ vars.VERIFICATION_SETUP || 'false' }} issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }} reopen-issue-on-push: ${{ vars.REOPEN_ISSUE_ON_PUSH || 'true' }} desired-assignees-count: ${{ vars.DESIRED_ASSIGNEES_COUNT || '1' }} diff --git a/setup/workflows/copilot_issue_comment.yml b/setup/workflows/copilot_issue_comment.yml index d977a5eba..08cfa87de 100644 --- a/setup/workflows/copilot_issue_comment.yml +++ b/setup/workflows/copilot_issue_comment.yml @@ -23,6 +23,7 @@ jobs: # Before Copilot, provision project runtimes/dependencies in this same job when # repository verification is configured. A separate CI job does not prepare it. # Declare reviewed versions in .copilot/verification-prerequisites.json. + # Optional trusted-main setup: VERIFICATION_SETUP=true and .copilot/verification-setup.mjs. # https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites - uses: vypdev/copilot@v3 with: @@ -41,6 +42,7 @@ jobs: chore-tree: ${{ vars.CHORE_TREE || 'chore' }} issue-managed-branches: ${{ vars.ISSUE_MANAGED_BRANCHES || 'true' }} pre-branch-sdd: ${{ vars.PRE_BRANCH_SDD || 'false' }} + verification-setup: ${{ vars.VERIFICATION_SETUP || 'false' }} issue-workflow-profile: ${{ vars.COPILOT_ISSUE_WORKFLOW_PROFILE || '' }} reopen-issue-on-push: ${{ vars.REOPEN_ISSUE_ON_PUSH || 'true' }} desired-assignees-count: ${{ vars.DESIRED_ASSIGNEES_COUNT || '1' }} diff --git a/setup/workflows/copilot_pull_request.yml b/setup/workflows/copilot_pull_request.yml index f6dd20108..76e3078fd 100644 --- a/setup/workflows/copilot_pull_request.yml +++ b/setup/workflows/copilot_pull_request.yml @@ -39,6 +39,7 @@ jobs: # Before Copilot, provision project runtimes/dependencies in this same job when # repository verification is configured. A separate CI job does not prepare it. # Declare reviewed versions in .copilot/verification-prerequisites.json. + # Optional trusted-main setup: VERIFICATION_SETUP=true and .copilot/verification-setup.mjs. # https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites - uses: vypdev/copilot@v3 with: @@ -58,6 +59,7 @@ jobs: chore-tree: ${{ vars.CHORE_TREE || 'chore' }} issue-managed-branches: ${{ vars.ISSUE_MANAGED_BRANCHES || 'true' }} pre-branch-sdd: ${{ vars.PRE_BRANCH_SDD || 'false' }} + verification-setup: ${{ vars.VERIFICATION_SETUP || 'false' }} reopen-issue-on-push: ${{ vars.REOPEN_ISSUE_ON_PUSH || 'true' }} desired-assignees-count: ${{ vars.DESIRED_ASSIGNEES_COUNT || '1' }} desired-reviewers-count: ${{ vars.DESIRED_REVIEWERS_COUNT || '1' }} diff --git a/setup/workflows/copilot_pull_request_comment.yml b/setup/workflows/copilot_pull_request_comment.yml index b9e2bae5b..b95c3634e 100644 --- a/setup/workflows/copilot_pull_request_comment.yml +++ b/setup/workflows/copilot_pull_request_comment.yml @@ -23,6 +23,7 @@ jobs: # Before Copilot, provision project runtimes/dependencies in this same job when # repository verification is configured. A separate CI job does not prepare it. # Declare reviewed versions in .copilot/verification-prerequisites.json. + # Optional trusted-main setup: VERIFICATION_SETUP=true and .copilot/verification-setup.mjs. # https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites - uses: vypdev/copilot@v3 with: @@ -42,6 +43,7 @@ jobs: chore-tree: ${{ vars.CHORE_TREE || 'chore' }} issue-managed-branches: ${{ vars.ISSUE_MANAGED_BRANCHES || 'true' }} pre-branch-sdd: ${{ vars.PRE_BRANCH_SDD || 'false' }} + verification-setup: ${{ vars.VERIFICATION_SETUP || 'false' }} reopen-issue-on-push: ${{ vars.REOPEN_ISSUE_ON_PUSH || 'true' }} desired-assignees-count: ${{ vars.DESIRED_ASSIGNEES_COUNT || '1' }} desired-reviewers-count: ${{ vars.DESIRED_REVIEWERS_COUNT || '1' }} diff --git a/setup/workflows/copilot_pull_request_review_state.yml b/setup/workflows/copilot_pull_request_review_state.yml index ab7cfce09..e74e89338 100644 --- a/setup/workflows/copilot_pull_request_review_state.yml +++ b/setup/workflows/copilot_pull_request_review_state.yml @@ -27,6 +27,7 @@ jobs: # Before Copilot, provision project runtimes/dependencies in this same job when # repository verification is configured. A separate CI job does not prepare it. # Declare reviewed versions in .copilot/verification-prerequisites.json. + # Optional trusted-main setup: VERIFICATION_SETUP=true and .copilot/verification-setup.mjs. # https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites - uses: vypdev/copilot@v3 with: @@ -46,6 +47,7 @@ jobs: chore-tree: ${{ vars.CHORE_TREE || 'chore' }} issue-managed-branches: ${{ vars.ISSUE_MANAGED_BRANCHES || 'true' }} pre-branch-sdd: ${{ vars.PRE_BRANCH_SDD || 'false' }} + verification-setup: ${{ vars.VERIFICATION_SETUP || 'false' }} reopen-issue-on-push: ${{ vars.REOPEN_ISSUE_ON_PUSH || 'true' }} desired-assignees-count: ${{ vars.DESIRED_ASSIGNEES_COUNT || '1' }} desired-reviewers-count: ${{ vars.DESIRED_REVIEWERS_COUNT || '1' }} diff --git a/specs/CATALOG.md b/specs/CATALOG.md index ec95a641c..ca0a3b74f 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -16,7 +16,7 @@ debt or convert unknown historic intent into a design decision. | `bugbot-review-state-reconciliation` | Implemented | Reconcile review snapshots, findings, threads, comments, and check conclusions | [Bugbot review-state reconciliation](./bugbot-review-state-reconciliation.md) | 59 paths · 2026-10-11 | | `execution-lifecycle` | Implemented | Shared GitHub Action lifecycle from event admission through durable user-facing results | [Execution admission, queueing, routing, and result publication](./execution-admission-queue-and-publication.md) + 3 companion | 104 paths · 2026-10-21 | | `architecture-quality-hardening` | Implemented | Close verified concurrency, error-contract, context-coupling, fan-out, setup/doctor, and provider-policy risks in dependency order | [Architecture quality and scalability hardening](./architecture-quality-and-scalability-hardening.md) + 1 companion | 72 paths · 2026-10-13 | -| `setup-and-doctor` | Implemented | Plan, validate, provision, and audit a repository installation without exposing credentials | [Setup, configuration, credentials, and doctor](./setup-configuration-credentials-and-doctor.md) + 5 companion | 326 paths · 2026-10-20 | +| `setup-and-doctor` | Implemented | Plan, validate, provision, and audit a repository installation without exposing credentials | [Setup, configuration, credentials, and doctor](./setup-configuration-credentials-and-doctor.md) + 5 companion | 328 paths · 2026-10-20 | | `local-web-setup-assistant` | Proposed | Offer a packaged, loopback-only Svelte setup interface over the existing engine with four-language guidance, English CLI help, basic/custom first-run paths, editable question and plan review, source-labelled GitHub facts, assisted CI and Project evidence, role-separated PATs, and truthful itemized outcomes; inspect existing configuration and apply independent, scope-aware runtime adjustments with permission, drift and read-back verification | [Local web setup assistant](./local-web-setup-assistant.md) + 1 companion | 238 paths · 2026-10-21 | | `guided-bot-pat-onboarding` | Proposed | Guide creation of the persistent workflow PAT, verify bot identity and grants, recover explicitly from wrong-account input within the session, and install the approved Actions Secret | [Guided bot PAT onboarding](./guided-bot-pat-onboarding.md) | 39 paths · 2026-10-07 | | `temporary-setup-operator-authorization` | Proposed | Guide creation of the one-run operator PAT through GitHub's official form, verify final setup access, and report user-owned deletion accurately | [Assisted setup PAT creation](./temporary-setup-operator-authorization.md) | 36 paths · 2026-10-18 | @@ -24,7 +24,7 @@ debt or convert unknown historic intent into a design decision. | `managed-issue-lifecycle` | As-built baseline | Convert typed issues into traceable work branches, project state, and lifecycle state | [Managed issue and branch lifecycle](./managed-issue-and-branch-lifecycle.md) | 71 paths · 2026-10-20 | | `comment-automation` | Implemented | Admit only explicit commands or exact mentions, then route them while protecting repository mutations | [Comment automation and authorization](./comment-automation-and-authorization.md) | 79 paths · 2026-10-11 | | `bugbot-analysis-and-autofix` | Implemented | Select one canonical PR, exhaustively analyze its bounded diff partitions, publish stable findings atomically, and apply authorized verified fixes | [Bugbot analysis, finding publication, and autofix](./bugbot-analysis-publication-and-autofix.md) + 2 companion | 109 paths · 2026-10-13 | -| `branch-synchronization` | Implemented | Observe parent drift with one localized status card and transition-only notifications, then safely merge a parent branch into a linked working branch | [Branch synchronization and conflict recovery](./branch-synchronization-and-conflict-recovery.md) + 2 companion | 90 paths · 2026-10-20 | +| `branch-synchronization` | Implemented | Observe parent drift with one localized status card and transition-only notifications, then safely merge a parent branch into a linked working branch | [Branch synchronization and conflict recovery](./branch-synchronization-and-conflict-recovery.md) + 2 companion | 101 paths · 2026-10-21 | | `pull-request-lifecycle` | Implemented | Enrich linked and unlinked pull requests with safe issue linkage, projects, metadata, reviewers, concise descriptions, and distinct workflow evidence | [Pull request lifecycle and enrichment](./pull-request-lifecycle-and-enrichment.md) | 77 paths · 2026-10-20 | | `agent-runtime` | Implemented | Resolve, provision, authenticate, authorize, and execute only the agent roles reachable by a run | [Agent runtime, provider, model, and role routing](./agent-runtime-provider-and-model-routing.md) + 1 companion | 99 paths · 2026-10-17 | | `cli-and-single-actions` | As-built baseline | Expose bounded local commands and workflow-dispatched operations through the shared application core | [CLI and single-action execution](./cli-and-single-action-execution.md) | 44 paths · 2026-10-19 | @@ -107,8 +107,8 @@ debt or convert unknown historic intent into a design decision. - Specifications: [`specs/setup-configuration-credentials-and-doctor.md`](./setup-configuration-credentials-and-doctor.md) · [`specs/setup-doctor-architecture-hardening.md`](./setup-doctor-architecture-hardening.md) · [`specs/setup-pat-permission-guidance-and-verification.md`](./setup-pat-permission-guidance-and-verification.md) · [`specs/github-runtime-doctor-command.md`](./github-runtime-doctor-command.md) · [`specs/organization-configuration-reuse-and-batch-setup.md`](./organization-configuration-reuse-and-batch-setup.md) · [`specs/setup-runner-profiles-and-installation-parity.md`](./setup-runner-profiles-and-installation-parity.md) - Workflows: [`setup/workflows/agent-cli-provisioning.yml`](../setup/workflows/agent-cli-provisioning.yml) · [`setup/workflows/copilot_credential_health.yml`](../setup/workflows/copilot_credential_health.yml) · [`.github/workflows/copilot_issue_comment.yml`](../.github/workflows/copilot_issue_comment.yml) · [`.github/workflows/copilot_pull_request_comment.yml`](../.github/workflows/copilot_pull_request_comment.yml) · [`.github/workflows/agent-cli-provisioning.yml`](../.github/workflows/agent-cli-provisioning.yml) · [`.github/workflows/copilot_credential_health.yml`](../.github/workflows/copilot_credential_health.yml) · [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.yml) · [`.github/workflows/copilot_branch_sync.yml`](../.github/workflows/copilot_branch_sync.yml) · [`setup/workflows/copilot_commit.yml`](../setup/workflows/copilot_commit.yml) · [`setup/workflows/copilot_branch_sync.yml`](../setup/workflows/copilot_branch_sync.yml) · [`setup/workflows/copilot_issue_comment.yml`](../setup/workflows/copilot_issue_comment.yml) · [`setup/workflows/copilot_pull_request_comment.yml`](../setup/workflows/copilot_pull_request_comment.yml) - Entrypoints: [`src/cli/commands/setup.ts`](../src/cli/commands/setup.ts) · [`src/cli/commands/doctor.ts`](../src/cli/commands/doctor.ts) · [`src/application/usecases/issue_comment_use_case.ts`](../src/application/usecases/issue_comment_use_case.ts) · [`src/application/usecases/pull_request_review_comment_use_case.ts`](../src/application/usecases/pull_request_review_comment_use_case.ts) -- Core code: [`src/domain/setup.ts`](../src/domain/setup.ts) · [`src/domain/setup_questionnaire.ts`](../src/domain/setup_questionnaire.ts) · [`src/domain/setup_token_permissions.ts`](../src/domain/setup_token_permissions.ts) · [`src/application/ports/setup_terminal_ports.ts`](../src/application/ports/setup_terminal_ports.ts) · [`src/application/ports/setup_wizard_ports.ts`](../src/application/ports/setup_wizard_ports.ts) · [`src/application/ports/branch_tag_ports.ts`](../src/application/ports/branch_tag_ports.ts) · [`src/application/policies/setup_issue_workflow_policy.ts`](../src/application/policies/setup_issue_workflow_policy.ts) · [`src/data/repository/release/repository_version_tags_query_repository.ts`](../src/data/repository/release/repository_version_tags_query_repository.ts) · [`src/application/ports/setup_token_permission_ports.ts`](../src/application/ports/setup_token_permission_ports.ts) · [`src/application/policies/setup_token_permission_evidence_policy.ts`](../src/application/policies/setup_token_permission_evidence_policy.ts) · [`src/application/policies/setup_token_permission_policy.ts`](../src/application/policies/setup_token_permission_policy.ts) · [`src/application/policies/setup_questionnaire_policy.ts`](../src/application/policies/setup_questionnaire_policy.ts) · [`src/application/policies/setup_configuration_plan.ts`](../src/application/policies/setup_configuration_plan.ts) · [`src/application/policies/merge_setup_overrides_policy.ts`](../src/application/policies/merge_setup_overrides_policy.ts) · [`src/application/policies/setup_configuration_storage_policy.ts`](../src/application/policies/setup_configuration_storage_policy.ts) · [`src/application/policies/setup_doctor_message_catalog.ts`](../src/application/policies/setup_doctor_message_catalog.ts) · [`src/application/policies/setup_doctor_report_policy.ts`](../src/application/policies/setup_doctor_report_policy.ts) · [`src/application/policies/setup_journey_policy.ts`](../src/application/policies/setup_journey_policy.ts) · [`src/application/policies/setup_permission_summary_policy.ts`](../src/application/policies/setup_permission_summary_policy.ts) · [`src/application/usecases/setup/setup_wizard_use_case.ts`](../src/application/usecases/setup/setup_wizard_use_case.ts) · [`src/application/usecases/setup/setup_journey_use_case.ts`](../src/application/usecases/setup/setup_journey_use_case.ts) · [`src/application/usecases/setup/setup_session_coordinator.ts`](../src/application/usecases/setup/setup_session_coordinator.ts) · [`src/application/usecases/setup/setup_questionnaire_controller.ts`](../src/application/usecases/setup/setup_questionnaire_controller.ts) · [`src/application/usecases/setup/setup_credentials_use_case.ts`](../src/application/usecases/setup/setup_credentials_use_case.ts) · [`src/application/usecases/setup/setup_token_permissions_use_case.ts`](../src/application/usecases/setup/setup_token_permissions_use_case.ts) · [`src/application/usecases/setup/doctor_use_case.ts`](../src/application/usecases/setup/doctor_use_case.ts) · [`src/application/usecases/setup/merge_queue_readiness_use_case.ts`](../src/application/usecases/setup/merge_queue_readiness_use_case.ts) · [`src/application/usecases/actions/initial_setup_workflow.ts`](../src/application/usecases/actions/initial_setup_workflow.ts) · [`src/application/usecases/actions/setup_resource_provisioning.ts`](../src/application/usecases/actions/setup_resource_provisioning.ts) · [`src/application/ports/message_catalog_ports.ts`](../src/application/ports/message_catalog_ports.ts) · [`src/application/usecases/localization/resolve_message_catalog_use_case.ts`](../src/application/usecases/localization/resolve_message_catalog_use_case.ts) · [`src/application/policies/setup_configuration_validation.ts`](../src/application/policies/setup_configuration_validation.ts) · [`src/infrastructure/setup_workspace_adapter.ts`](../src/infrastructure/setup_workspace_adapter.ts) · [`src/data/repository/repository_variables_repository.ts`](../src/data/repository/repository_variables_repository.ts) · [`src/infrastructure/github/ports/github_repository_variables_protocol.ts`](../src/infrastructure/github/ports/github_repository_variables_protocol.ts) · [`src/infrastructure/setup_remote_credential_health_adapter.ts`](../src/infrastructure/setup_remote_credential_health_adapter.ts) · [`src/infrastructure/setup_credential_validation_adapter.ts`](../src/infrastructure/setup_credential_validation_adapter.ts) · [`src/infrastructure/setup_token_permission_query_adapter.ts`](../src/infrastructure/setup_token_permission_query_adapter.ts) · [`src/infrastructure/setup_permission_actions_probe.ts`](../src/infrastructure/setup_permission_actions_probe.ts) · [`src/infrastructure/setup_permission_issue_probe.ts`](../src/infrastructure/setup_permission_issue_probe.ts) · [`src/infrastructure/setup_permission_issue_cleanup.ts`](../src/infrastructure/setup_permission_issue_cleanup.ts) · [`src/infrastructure/setup_permission_probe_journal.ts`](../src/infrastructure/setup_permission_probe_journal.ts) · [`src/infrastructure/setup_permission_probe_transaction.ts`](../src/infrastructure/setup_permission_probe_transaction.ts) · [`src/infrastructure/setup_permission_resource_probes.ts`](../src/infrastructure/setup_permission_resource_probes.ts) · [`src/infrastructure/setup_projects_probe_page_policy.ts`](../src/infrastructure/setup_projects_probe_page_policy.ts) · [`src/cli/setup_terminal_driver.ts`](../src/cli/setup_terminal_driver.ts) · [`src/cli/setup_question_renderer.ts`](../src/cli/setup_question_renderer.ts) · [`src/cli/setup_journey_presenter.ts`](../src/cli/setup_journey_presenter.ts) · [`src/cli/setup_plan_presenter.ts`](../src/cli/setup_plan_presenter.ts) · [`src/cli/setup_doctor_presenter.ts`](../src/cli/setup_doctor_presenter.ts) · [`src/cli/setup_prompt_rendering.ts`](../src/cli/setup_prompt_rendering.ts) · [`src/cli/setup_credential_prompt_adapter.ts`](../src/cli/setup_credential_prompt_adapter.ts) · [`src/cli/setup_credential_collection.ts`](../src/cli/setup_credential_collection.ts) · [`src/cli/commands/setup_execution.ts`](../src/cli/commands/setup_execution.ts) · [`src/cli/setup_token_permission_presenter.ts`](../src/cli/setup_token_permission_presenter.ts) · [`src/infrastructure/composition/setup_credentials_composition_root.ts`](../src/infrastructure/composition/setup_credentials_composition_root.ts) · [`src/infrastructure/composition/setup_token_permissions_composition_root.ts`](../src/infrastructure/composition/setup_token_permissions_composition_root.ts) · [`src/infrastructure/composition/setup_doctor_composition_root.ts`](../src/infrastructure/composition/setup_doctor_composition_root.ts) · [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`scripts/documentation_pat_exception_policy.cjs`](../scripts/documentation_pat_exception_policy.cjs) · [`scripts/validate-documentation-contract.cjs`](../scripts/validate-documentation-contract.cjs) · [`src/infrastructure/http_deadline.ts`](../src/infrastructure/http_deadline.ts) · [`src/infrastructure/setup_permission_denial.ts`](../src/infrastructure/setup_permission_denial.ts) · [`src/infrastructure/setup_permission_read_probe.ts`](../src/infrastructure/setup_permission_read_probe.ts) · [`src/infrastructure/setup_permission_read_evidence.ts`](../src/infrastructure/setup_permission_read_evidence.ts) · [`src/infrastructure/setup_permission_read_shape.ts`](../src/infrastructure/setup_permission_read_shape.ts) · [`src/infrastructure/setup_permission_projects_read.ts`](../src/infrastructure/setup_permission_projects_read.ts) · [`src/infrastructure/setup_permission_projects_access.ts`](../src/infrastructure/setup_permission_projects_access.ts) · [`src/infrastructure/setup_permission_workflow_cleanup.ts`](../src/infrastructure/setup_permission_workflow_cleanup.ts) · [`src/infrastructure/setup_permission_project_cleanup.ts`](../src/infrastructure/setup_permission_project_cleanup.ts) · [`src/infrastructure/setup_permission_probe_http.ts`](../src/infrastructure/setup_permission_probe_http.ts) · [`src/infrastructure/setup_permission_pull_request_probe.ts`](../src/infrastructure/setup_permission_pull_request_probe.ts) · [`src/infrastructure/git_authentication_environment.ts`](../src/infrastructure/git_authentication_environment.ts) · [`src/actions/github_action_input.ts`](../src/actions/github_action_input.ts) · [`src/infrastructure/setup_github_identity_query_adapter.ts`](../src/infrastructure/setup_github_identity_query_adapter.ts) · [`src/infrastructure/github_secret_encryption.ts`](../src/infrastructure/github_secret_encryption.ts) · [`src/infrastructure/setup_permission_organization_owner.ts`](../src/infrastructure/setup_permission_organization_owner.ts) · [`src/data/repository/github/github_actions_resource_inspector.ts`](../src/data/repository/github/github_actions_resource_inspector.ts) · [`src/data/repository/github/github_actions_resource_commands.ts`](../src/data/repository/github/github_actions_resource_commands.ts) · [`src/data/repository/github/github_actions_resource_collection.ts`](../src/data/repository/github/github_actions_resource_collection.ts) · [`src/application/usecases/setup/resolve_setup_workflow_pat_conflict_use_case.ts`](../src/application/usecases/setup/resolve_setup_workflow_pat_conflict_use_case.ts) · [`src/cli/setup_workflow_pat_storage_copy.ts`](../src/cli/setup_workflow_pat_storage_copy.ts) · [`src/cli/commands/setup_policy.ts`](../src/cli/commands/setup_policy.ts) · [`src/application/usecases/execution/execution_issue_number_policy.ts`](../src/application/usecases/execution/execution_issue_number_policy.ts) · [`src/application/usecases/execution/setup_execution_workflow.ts`](../src/application/usecases/execution/setup_execution_workflow.ts) · [`src/domain/github_runtime_doctor.ts`](../src/domain/github_runtime_doctor.ts) · [`src/domain/copilot_command.ts`](../src/domain/copilot_command.ts) · [`src/application/usecases/comment_automation_command_workflow.ts`](../src/application/usecases/comment_automation_command_workflow.ts) · [`src/application/policies/copilot_interaction_policy.ts`](../src/application/policies/copilot_interaction_policy.ts) · [`src/application/policies/publication_message_catalog.ts`](../src/application/policies/publication_message_catalog.ts) · [`src/application/ports/github_runtime_doctor_ports.ts`](../src/application/ports/github_runtime_doctor_ports.ts) · [`src/application/policies/github_runtime_doctor_policy.ts`](../src/application/policies/github_runtime_doctor_policy.ts) · [`src/application/policies/doctor_workflow_condition_policy.ts`](../src/application/policies/doctor_workflow_condition_policy.ts) · [`src/application/policies/github_runtime_doctor_presentation_policy.ts`](../src/application/policies/github_runtime_doctor_presentation_policy.ts) · [`src/application/usecases/read_copilot_doctor_workflow.ts`](../src/application/usecases/read_copilot_doctor_workflow.ts) · [`src/data/repository/github/runtime_doctor_repository.ts`](../src/data/repository/github/runtime_doctor_repository.ts) · [`src/infrastructure/composition/github_runtime_doctor_composition_root.ts`](../src/infrastructure/composition/github_runtime_doctor_composition_root.ts) · [`src/application/policies/agent_activity_policy.ts`](../src/application/policies/agent_activity_policy.ts) · [`src/application/policies/doctor_workflow_matrix_policy.ts`](../src/application/policies/doctor_workflow_matrix_policy.ts) · [`src/application/contracts/setup_batch_view.ts`](../src/application/contracts/setup_batch_view.ts) · [`src/application/policies/issue_resource_input_policy.ts`](../src/application/policies/issue_resource_input_policy.ts) · [`src/application/policies/setup_batch_access_policy.ts`](../src/application/policies/setup_batch_access_policy.ts) · [`src/application/policies/setup_batch_policy.ts`](../src/application/policies/setup_batch_policy.ts) · [`src/application/policies/setup_batch_retry_policy.ts`](../src/application/policies/setup_batch_retry_policy.ts) · [`src/application/policies/setup_batch_selection_policy.ts`](../src/application/policies/setup_batch_selection_policy.ts) · [`src/application/policies/setup_batch_view_policy.ts`](../src/application/policies/setup_batch_view_policy.ts) · [`src/application/policies/setup_credential_binding_policy.ts`](../src/application/policies/setup_credential_binding_policy.ts) · [`src/application/policies/setup_organization_import_policy.ts`](../src/application/policies/setup_organization_import_policy.ts) · [`src/application/policies/setup_organization_variable_bindings.ts`](../src/application/policies/setup_organization_variable_bindings.ts) · [`src/application/policies/setup_organization_variable_reuse_policy.ts`](../src/application/policies/setup_organization_variable_reuse_policy.ts) · [`src/application/ports/setup_batch_ports.ts`](../src/application/ports/setup_batch_ports.ts) · [`src/application/usecases/setup/apply_setup_batch_use_case.ts`](../src/application/usecases/setup/apply_setup_batch_use_case.ts) · [`src/application/usecases/setup/configure_setup_batch_target_use_case.ts`](../src/application/usecases/setup/configure_setup_batch_target_use_case.ts) · [`src/application/usecases/setup/prepare_setup_batch_use_case.ts`](../src/application/usecases/setup/prepare_setup_batch_use_case.ts) · [`src/application/usecases/setup/select_setup_batch_targets_use_case.ts`](../src/application/usecases/setup/select_setup_batch_targets_use_case.ts) · [`src/application/usecases/setup/setup_batch_session_use_case.ts`](../src/application/usecases/setup/setup_batch_session_use_case.ts) · [`src/cli/setup_batch_adapter.ts`](../src/cli/setup_batch_adapter.ts) · [`src/cli/setup_source_adapter.ts`](../src/cli/setup_source_adapter.ts) · [`src/domain/setup_batch.ts`](../src/domain/setup_batch.ts) · [`src/domain/setup_credential_health.ts`](../src/domain/setup_credential_health.ts) · [`src/domain/setup_organization_reuse.ts`](../src/domain/setup_organization_reuse.ts) · [`src/infrastructure/composition/setup_batch_composition_root.ts`](../src/infrastructure/composition/setup_batch_composition_root.ts) · [`src/infrastructure/github/ports/github_setup_batch_protocol.ts`](../src/infrastructure/github/ports/github_setup_batch_protocol.ts) · [`src/infrastructure/github/ports/github_trusted_credential_health_protocol.ts`](../src/infrastructure/github/ports/github_trusted_credential_health_protocol.ts) · [`src/infrastructure/setup_batch_github_apply_adapter.ts`](../src/infrastructure/setup_batch_github_apply_adapter.ts) · [`src/infrastructure/setup_batch_github_read_adapter.ts`](../src/infrastructure/setup_batch_github_read_adapter.ts) · [`src/infrastructure/setup_batch_journal_adapter.ts`](../src/infrastructure/setup_batch_journal_adapter.ts) · [`src/infrastructure/setup_batch_workspace_adapter.ts`](../src/infrastructure/setup_batch_workspace_adapter.ts) · [`src/infrastructure/setup_trusted_credential_health_adapter.ts`](../src/infrastructure/setup_trusted_credential_health_adapter.ts) · [`src/application/policies/setup_remote_facts_policy.ts`](../src/application/policies/setup_remote_facts_policy.ts) · [`src/cli/setup_confirmation_adapter.ts`](../src/cli/setup_confirmation_adapter.ts) · [`src/cli/web_setup_adapters.ts`](../src/cli/web_setup_adapters.ts) · [`scripts/validate-agent-documentation.cjs`](../scripts/validate-agent-documentation.cjs) · [`src/utils/setup_workflow_upgrade.ts`](../src/utils/setup_workflow_upgrade.ts) · [`src/domain/setup_runner.ts`](../src/domain/setup_runner.ts) · [`src/application/policies/setup_runner_policy.ts`](../src/application/policies/setup_runner_policy.ts) · [`src/application/policies/setup_runner_question_policy.ts`](../src/application/policies/setup_runner_question_policy.ts) · [`src/application/policies/setup_runner_guidance_policy.ts`](../src/application/policies/setup_runner_guidance_policy.ts) · [`src/application/ports/setup_runner_ports.ts`](../src/application/ports/setup_runner_ports.ts) · [`src/infrastructure/setup_runner_discovery_adapter.ts`](../src/infrastructure/setup_runner_discovery_adapter.ts) · [`src/infrastructure/composition/setup_runner_composition_root.ts`](../src/infrastructure/composition/setup_runner_composition_root.ts) · [`src/utils/setup_installation_review.ts`](../src/utils/setup_installation_review.ts) · [`src/cli/setup_installation_review_presenter.ts`](../src/cli/setup_installation_review_presenter.ts) · [`src/application/policies/setup_runner_installation_summary_policy.ts`](../src/application/policies/setup_runner_installation_summary_policy.ts) · [`src/utils/setup_files.ts`](../src/utils/setup_files.ts) · [`src/domain/specification_catalog_path_policy.cjs`](../src/domain/specification_catalog_path_policy.cjs) · [`src/domain/sdd_prerequisite_policy.cjs`](../src/domain/sdd_prerequisite_policy.cjs) · [`src/application/contracts/setup_documentation.ts`](../src/application/contracts/setup_documentation.ts) · [`src/application/policies/setup_question_documentation_policy.ts`](../src/application/policies/setup_question_documentation_policy.ts) · [`web/src/components/SetupHelp.svelte`](../web/src/components/SetupHelp.svelte) · [`web/src/components/CompletedSetupHelp.svelte`](../web/src/components/CompletedSetupHelp.svelte) · [`src/application/policies/verification_runtime_policy.ts`](../src/application/policies/verification_runtime_policy.ts) · [`src/infrastructure/verification_runtime_manifest.ts`](../src/infrastructure/verification_runtime_manifest.ts) · [`src/infrastructure/verification_runtime_preflight.ts`](../src/infrastructure/verification_runtime_preflight.ts) · [`src/infrastructure/untrusted_command_environment.ts`](../src/infrastructure/untrusted_command_environment.ts) · [`src/application/ports/git_ports.ts`](../src/application/ports/git_ports.ts) · [`src/application/ports/bugbot_git_ports.ts`](../src/application/ports/bugbot_git_ports.ts) · [`src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts`](../src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts) · [`src/application/usecases/steps/commit/bugbot/verify_command_runner.ts`](../src/application/usecases/steps/commit/bugbot/verify_command_runner.ts) -- Tests: [`src/data/repository/__tests__/repository_version_tags_query_repository.test.ts`](../src/data/repository/__tests__/repository_version_tags_query_repository.test.ts) · [`src/cli/__tests__/setup_command_options.test.ts`](../src/cli/__tests__/setup_command_options.test.ts) · [`src/data/repository/__tests__/setup_variable_write_failures.test.ts`](../src/data/repository/__tests__/setup_variable_write_failures.test.ts) · [`src/infrastructure/composition/__tests__/initial_setup_composition_root.test.ts`](../src/infrastructure/composition/__tests__/initial_setup_composition_root.test.ts) · [`src/application/policies/__tests__/setup_questionnaire_policy.test.ts`](../src/application/policies/__tests__/setup_questionnaire_policy.test.ts) · [`src/application/policies/__tests__/setup_configuration_policy.test.ts`](../src/application/policies/__tests__/setup_configuration_policy.test.ts) · [`src/application/policies/__tests__/setup_token_permission_policy.test.ts`](../src/application/policies/__tests__/setup_token_permission_policy.test.ts) · [`src/application/policies/__tests__/setup_remote_facts_policy.test.ts`](../src/application/policies/__tests__/setup_remote_facts_policy.test.ts) · [`src/application/policies/__tests__/setup_doctor_message_catalog.test.ts`](../src/application/policies/__tests__/setup_doctor_message_catalog.test.ts) · [`src/application/policies/__tests__/setup_doctor_report_policy.test.ts`](../src/application/policies/__tests__/setup_doctor_report_policy.test.ts) · [`src/application/usecases/setup/__tests__/setup_questionnaire_controller.test.ts`](../src/application/usecases/setup/__tests__/setup_questionnaire_controller.test.ts) · [`src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts`](../src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts) · [`src/application/usecases/setup/__tests__/setup_journey_use_case.test.ts`](../src/application/usecases/setup/__tests__/setup_journey_use_case.test.ts) · [`src/application/usecases/setup/__tests__/setup_session_coordinator.test.ts`](../src/application/usecases/setup/__tests__/setup_session_coordinator.test.ts) · [`src/application/usecases/setup/__tests__/setup_credentials_use_case.test.ts`](../src/application/usecases/setup/__tests__/setup_credentials_use_case.test.ts) · [`src/application/usecases/setup/__tests__/setup_token_permissions_use_case.test.ts`](../src/application/usecases/setup/__tests__/setup_token_permissions_use_case.test.ts) · [`src/application/usecases/setup/__tests__/doctor_use_case.test.ts`](../src/application/usecases/setup/__tests__/doctor_use_case.test.ts) · [`src/application/usecases/setup/__tests__/merge_queue_readiness_use_case.test.ts`](../src/application/usecases/setup/__tests__/merge_queue_readiness_use_case.test.ts) · [`src/application/usecases/actions/__tests__/initial_setup_use_case.test.ts`](../src/application/usecases/actions/__tests__/initial_setup_use_case.test.ts) · [`src/application/usecases/actions/__tests__/setup_resource_provisioning.test.ts`](../src/application/usecases/actions/__tests__/setup_resource_provisioning.test.ts) · [`src/infrastructure/__tests__/setup_workspace_adapter.test.ts`](../src/infrastructure/__tests__/setup_workspace_adapter.test.ts) · [`src/infrastructure/__tests__/setup_remote_credential_health_adapter.test.ts`](../src/infrastructure/__tests__/setup_remote_credential_health_adapter.test.ts) · [`src/infrastructure/__tests__/setup_token_permission_query_adapter.test.ts`](../src/infrastructure/__tests__/setup_token_permission_query_adapter.test.ts) · [`src/infrastructure/__tests__/setup_permission_resource_probes.test.ts`](../src/infrastructure/__tests__/setup_permission_resource_probes.test.ts) · [`src/infrastructure/__tests__/setup_projects_probe_page_policy.test.ts`](../src/infrastructure/__tests__/setup_projects_probe_page_policy.test.ts) · [`src/infrastructure/composition/__tests__/setup_token_permissions_composition_root.test.ts`](../src/infrastructure/composition/__tests__/setup_token_permissions_composition_root.test.ts) · [`src/data/repository/__tests__/repository_variables_repository.test.ts`](../src/data/repository/__tests__/repository_variables_repository.test.ts) · [`src/cli/__tests__/setup_presenters.test.ts`](../src/cli/__tests__/setup_presenters.test.ts) · [`src/cli/__tests__/setup_journey_presenter.test.ts`](../src/cli/__tests__/setup_journey_presenter.test.ts) · [`src/cli/__tests__/setup_prompt_rendering.test.ts`](../src/cli/__tests__/setup_prompt_rendering.test.ts) · [`src/cli/__tests__/setup_token_permission_presenter.test.ts`](../src/cli/__tests__/setup_token_permission_presenter.test.ts) · [`src/__tests__/cli.test.ts`](../src/__tests__/cli.test.ts) · [`src/cli/__tests__/setup_terminal_driver.test.ts`](../src/cli/__tests__/setup_terminal_driver.test.ts) · [`src/architecture/__tests__/setup_doctor_boundaries.test.ts`](../src/architecture/__tests__/setup_doctor_boundaries.test.ts) · [`src/tooling/__tests__/documentation_pat_exception_policy.test.ts`](../src/tooling/__tests__/documentation_pat_exception_policy.test.ts) · [`src/infrastructure/__tests__/setup_pat_audit.test.ts`](../src/infrastructure/__tests__/setup_pat_audit.test.ts) · [`src/infrastructure/__tests__/setup_permission_projects_access.test.ts`](../src/infrastructure/__tests__/setup_permission_projects_access.test.ts) · [`src/infrastructure/__tests__/setup_credential_validation_adapter.test.ts`](../src/infrastructure/__tests__/setup_credential_validation_adapter.test.ts) · [`src/infrastructure/__tests__/setup_github_identity_query_adapter.test.ts`](../src/infrastructure/__tests__/setup_github_identity_query_adapter.test.ts) · [`src/infrastructure/__tests__/git_commit_adapter.test.ts`](../src/infrastructure/__tests__/git_commit_adapter.test.ts) · [`src/actions/__tests__/github_action_input.test.ts`](../src/actions/__tests__/github_action_input.test.ts) · [`src/infrastructure/__tests__/setup_permission_cleanup_contract.test.ts`](../src/infrastructure/__tests__/setup_permission_cleanup_contract.test.ts) · [`src/infrastructure/__tests__/setup_permission_actions_cleanup_timing.test.ts`](../src/infrastructure/__tests__/setup_permission_actions_cleanup_timing.test.ts) · [`src/infrastructure/__tests__/setup_permission_operator_actions.test.ts`](../src/infrastructure/__tests__/setup_permission_operator_actions.test.ts) · [`src/infrastructure/__tests__/setup_permission_journal_contract.test.ts`](../src/infrastructure/__tests__/setup_permission_journal_contract.test.ts) · [`src/infrastructure/__tests__/setup_permission_audit_transactions.test.ts`](../src/infrastructure/__tests__/setup_permission_audit_transactions.test.ts) · [`src/infrastructure/__tests__/setup_permission_write_failures.test.ts`](../src/infrastructure/__tests__/setup_permission_write_failures.test.ts) · [`src/infrastructure/__tests__/setup_permission_resource_failures.test.ts`](../src/infrastructure/__tests__/setup_permission_resource_failures.test.ts) · [`src/infrastructure/__tests__/setup_permission_boundary_failures.test.ts`](../src/infrastructure/__tests__/setup_permission_boundary_failures.test.ts) · [`src/infrastructure/__tests__/github_secret_encryption.test.ts`](../src/infrastructure/__tests__/github_secret_encryption.test.ts) · [`src/infrastructure/__tests__/setup_permission_organization_owner.test.ts`](../src/infrastructure/__tests__/setup_permission_organization_owner.test.ts) · [`src/infrastructure/__tests__/setup_pat_bootstrap_transactions.test.ts`](../src/infrastructure/__tests__/setup_pat_bootstrap_transactions.test.ts) · [`src/data/repository/__tests__/repository_actions_sdk_contract.integration.test.ts`](../src/data/repository/__tests__/repository_actions_sdk_contract.integration.test.ts) · [`src/data/repository/__tests__/repository_actions_resource_edge_cases.test.ts`](../src/data/repository/__tests__/repository_actions_resource_edge_cases.test.ts) · [`src/data/repository/__tests__/repository_selected_resource_grants.test.ts`](../src/data/repository/__tests__/repository_selected_resource_grants.test.ts) · [`src/infrastructure/__tests__/setup_permission_read_contract.test.ts`](../src/infrastructure/__tests__/setup_permission_read_contract.test.ts) · [`src/application/usecases/setup/__tests__/setup_workflow_pat_conflict.test.ts`](../src/application/usecases/setup/__tests__/setup_workflow_pat_conflict.test.ts) · [`src/application/usecases/setup/__tests__/setup_workflow_pat_credential_rotation.test.ts`](../src/application/usecases/setup/__tests__/setup_workflow_pat_credential_rotation.test.ts) · [`src/data/repository/__tests__/setup_organization_pat_inventory.test.ts`](../src/data/repository/__tests__/setup_organization_pat_inventory.test.ts) · [`src/cli/__tests__/web_setup_bot_pat_scope_conflict.test.ts`](../src/cli/__tests__/web_setup_bot_pat_scope_conflict.test.ts) · [`src/actions/__tests__/local_setup_apply_contract.test.ts`](../src/actions/__tests__/local_setup_apply_contract.test.ts) · [`src/cli/commands/__tests__/setup_policy.test.ts`](../src/cli/commands/__tests__/setup_policy.test.ts) · [`src/application/usecases/execution/__tests__/execution_issue_number_policy.test.ts`](../src/application/usecases/execution/__tests__/execution_issue_number_policy.test.ts) · [`src/application/usecases/execution/__tests__/setup_execution_workflow.test.ts`](../src/application/usecases/execution/__tests__/setup_execution_workflow.test.ts) · [`src/domain/__tests__/github_runtime_doctor.test.ts`](../src/domain/__tests__/github_runtime_doctor.test.ts) · [`src/domain/__tests__/copilot_command.test.ts`](../src/domain/__tests__/copilot_command.test.ts) · [`src/application/usecases/__tests__/comment_automation_use_case.test.ts`](../src/application/usecases/__tests__/comment_automation_use_case.test.ts) · [`src/application/policies/__tests__/github_runtime_doctor_policy.test.ts`](../src/application/policies/__tests__/github_runtime_doctor_policy.test.ts) · [`src/application/policies/__tests__/doctor_workflow_condition_policy.test.ts`](../src/application/policies/__tests__/doctor_workflow_condition_policy.test.ts) · [`src/application/policies/__tests__/github_runtime_doctor_presentation_policy.test.ts`](../src/application/policies/__tests__/github_runtime_doctor_presentation_policy.test.ts) · [`src/application/usecases/__tests__/read_copilot_doctor_workflow.test.ts`](../src/application/usecases/__tests__/read_copilot_doctor_workflow.test.ts) · [`src/application/usecases/__tests__/runtime_doctor_security_integration.test.ts`](../src/application/usecases/__tests__/runtime_doctor_security_integration.test.ts) · [`src/data/repository/github/__tests__/runtime_doctor_repository.test.ts`](../src/data/repository/github/__tests__/runtime_doctor_repository.test.ts) · [`src/infrastructure/composition/__tests__/github_runtime_doctor_composition_root.test.ts`](../src/infrastructure/composition/__tests__/github_runtime_doctor_composition_root.test.ts) · [`src/application/policies/__tests__/runtime_doctor_agent_activity.test.ts`](../src/application/policies/__tests__/runtime_doctor_agent_activity.test.ts) · [`src/application/policies/__tests__/doctor_workflow_matrix_policy.test.ts`](../src/application/policies/__tests__/doctor_workflow_matrix_policy.test.ts) · [`src/application/policies/__tests__/setup_approval_operator_permissions.test.ts`](../src/application/policies/__tests__/setup_approval_operator_permissions.test.ts) · [`src/application/usecases/setup/__tests__/audit_configured_setup_pat_use_case.test.ts`](../src/application/usecases/setup/__tests__/audit_configured_setup_pat_use_case.test.ts) · [`src/application/policies/__tests__/setup_batch_permission_policy.test.ts`](../src/application/policies/__tests__/setup_batch_permission_policy.test.ts) · [`src/application/policies/__tests__/setup_batch_policy.test.ts`](../src/application/policies/__tests__/setup_batch_policy.test.ts) · [`src/application/policies/__tests__/setup_batch_retry_policy.test.ts`](../src/application/policies/__tests__/setup_batch_retry_policy.test.ts) · [`src/application/policies/__tests__/setup_batch_selection_policy.test.ts`](../src/application/policies/__tests__/setup_batch_selection_policy.test.ts) · [`src/application/policies/__tests__/setup_organization_approval_draft.test.ts`](../src/application/policies/__tests__/setup_organization_approval_draft.test.ts) · [`src/application/policies/__tests__/setup_organization_import_policy.test.ts`](../src/application/policies/__tests__/setup_organization_import_policy.test.ts) · [`src/application/policies/__tests__/setup_organization_import_safety.test.ts`](../src/application/policies/__tests__/setup_organization_import_safety.test.ts) · [`src/application/policies/__tests__/setup_organization_variable_reuse_policy.test.ts`](../src/application/policies/__tests__/setup_organization_variable_reuse_policy.test.ts) · [`src/application/usecases/actions/__tests__/setup_organization_reuse_provisioning.test.ts`](../src/application/usecases/actions/__tests__/setup_organization_reuse_provisioning.test.ts) · [`src/application/usecases/setup/__tests__/apply_setup_batch_use_case.test.ts`](../src/application/usecases/setup/__tests__/apply_setup_batch_use_case.test.ts) · [`src/application/usecases/setup/__tests__/select_setup_batch_targets_use_case.test.ts`](../src/application/usecases/setup/__tests__/select_setup_batch_targets_use_case.test.ts) · [`src/application/usecases/setup/__tests__/setup_batch_flow.test.ts`](../src/application/usecases/setup/__tests__/setup_batch_flow.test.ts) · [`src/application/usecases/setup/__tests__/setup_organization_import_wizard.test.ts`](../src/application/usecases/setup/__tests__/setup_organization_import_wizard.test.ts) · [`src/application/usecases/setup/__tests__/setup_organization_secret_reuse.test.ts`](../src/application/usecases/setup/__tests__/setup_organization_secret_reuse.test.ts) · [`src/cli/__tests__/setup_source_adapter.test.ts`](../src/cli/__tests__/setup_source_adapter.test.ts) · [`src/cli/__tests__/web_setup_batch_components.test.ts`](../src/cli/__tests__/web_setup_batch_components.test.ts) · [`src/cli/__tests__/web_setup_batch_selection.test.ts`](../src/cli/__tests__/web_setup_batch_selection.test.ts) · [`src/infrastructure/__tests__/setup_batch_github_apply_adapter.test.ts`](../src/infrastructure/__tests__/setup_batch_github_apply_adapter.test.ts) · [`src/infrastructure/__tests__/setup_batch_github_read_adapter.test.ts`](../src/infrastructure/__tests__/setup_batch_github_read_adapter.test.ts) · [`src/infrastructure/__tests__/setup_batch_local_adapters.test.ts`](../src/infrastructure/__tests__/setup_batch_local_adapters.test.ts) · [`src/infrastructure/__tests__/setup_reuse_empty_collections.integration.test.ts`](../src/infrastructure/__tests__/setup_reuse_empty_collections.integration.test.ts) · [`src/infrastructure/__tests__/setup_trusted_credential_health_adapter.test.ts`](../src/infrastructure/__tests__/setup_trusted_credential_health_adapter.test.ts) · [`src/infrastructure/__tests__/setup_trusted_health_sdk_contract.integration.test.ts`](../src/infrastructure/__tests__/setup_trusted_health_sdk_contract.integration.test.ts) · [`src/application/policies/__tests__/setup_batch_access_and_view.test.ts`](../src/application/policies/__tests__/setup_batch_access_and_view.test.ts) · [`src/cli/__tests__/setup_batch_adapter.test.ts`](../src/cli/__tests__/setup_batch_adapter.test.ts) · [`src/infrastructure/__tests__/setup_batch_composition.test.ts`](../src/infrastructure/__tests__/setup_batch_composition.test.ts) · [`src/infrastructure/__tests__/setup_batch_guidance_compatibility.test.ts`](../src/infrastructure/__tests__/setup_batch_guidance_compatibility.test.ts) · [`src/utils/__tests__/setup_workflow_upgrade.test.ts`](../src/utils/__tests__/setup_workflow_upgrade.test.ts) · [`src/cli/__tests__/setup_pat_intent_adapter.test.ts`](../src/cli/__tests__/setup_pat_intent_adapter.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/application/policies/__tests__/setup_runner_policy.test.ts`](../src/application/policies/__tests__/setup_runner_policy.test.ts) · [`src/application/policies/__tests__/setup_runner_question_policy.test.ts`](../src/application/policies/__tests__/setup_runner_question_policy.test.ts) · [`src/infrastructure/__tests__/setup_runner_discovery_adapter.test.ts`](../src/infrastructure/__tests__/setup_runner_discovery_adapter.test.ts) · [`src/utils/__tests__/setup_runner_installation.test.ts`](../src/utils/__tests__/setup_runner_installation.test.ts) · [`src/cli/__tests__/web_setup_runner_components.test.ts`](../src/cli/__tests__/web_setup_runner_components.test.ts) · [`src/application/policies/__tests__/setup_runner_installation_summary_policy.test.ts`](../src/application/policies/__tests__/setup_runner_installation_summary_policy.test.ts) · [`src/application/usecases/setup/__tests__/setup_runner_planning.test.ts`](../src/application/usecases/setup/__tests__/setup_runner_planning.test.ts) · [`src/cli/__tests__/setup_runner_config_file.test.ts`](../src/cli/__tests__/setup_runner_config_file.test.ts) · [`src/infrastructure/__tests__/setup_runner_composition.test.ts`](../src/infrastructure/__tests__/setup_runner_composition.test.ts) · [`src/tooling/__tests__/setup_provisioning_workflow.test.ts`](../src/tooling/__tests__/setup_provisioning_workflow.test.ts) · [`src/infrastructure/__tests__/setup_sdd_prerequisites.test.ts`](../src/infrastructure/__tests__/setup_sdd_prerequisites.test.ts) · [`src/cli/__tests__/setup_documentation_navigation.test.ts`](../src/cli/__tests__/setup_documentation_navigation.test.ts) · [`src/application/policies/__tests__/setup_question_documentation_policy.test.ts`](../src/application/policies/__tests__/setup_question_documentation_policy.test.ts) · [`src/application/policies/__tests__/verification_runtime_policy.test.ts`](../src/application/policies/__tests__/verification_runtime_policy.test.ts) · [`src/infrastructure/__tests__/verification_runtime_preflight.test.ts`](../src/infrastructure/__tests__/verification_runtime_preflight.test.ts) · [`src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts`](../src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts) · [`src/infrastructure/__tests__/untrusted_command_environment.test.ts`](../src/infrastructure/__tests__/untrusted_command_environment.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts) · [`src/tooling/__tests__/verification_runtime_guidance.test.ts`](../src/tooling/__tests__/verification_runtime_guidance.test.ts) +- Core code: [`src/domain/setup.ts`](../src/domain/setup.ts) · [`src/domain/setup_questionnaire.ts`](../src/domain/setup_questionnaire.ts) · [`src/domain/setup_token_permissions.ts`](../src/domain/setup_token_permissions.ts) · [`src/application/ports/setup_terminal_ports.ts`](../src/application/ports/setup_terminal_ports.ts) · [`src/application/ports/setup_wizard_ports.ts`](../src/application/ports/setup_wizard_ports.ts) · [`src/application/ports/branch_tag_ports.ts`](../src/application/ports/branch_tag_ports.ts) · [`src/application/policies/setup_issue_workflow_policy.ts`](../src/application/policies/setup_issue_workflow_policy.ts) · [`src/data/repository/release/repository_version_tags_query_repository.ts`](../src/data/repository/release/repository_version_tags_query_repository.ts) · [`src/application/ports/setup_token_permission_ports.ts`](../src/application/ports/setup_token_permission_ports.ts) · [`src/application/policies/setup_token_permission_evidence_policy.ts`](../src/application/policies/setup_token_permission_evidence_policy.ts) · [`src/application/policies/setup_token_permission_policy.ts`](../src/application/policies/setup_token_permission_policy.ts) · [`src/application/policies/setup_questionnaire_policy.ts`](../src/application/policies/setup_questionnaire_policy.ts) · [`src/application/policies/setup_configuration_plan.ts`](../src/application/policies/setup_configuration_plan.ts) · [`src/application/policies/merge_setup_overrides_policy.ts`](../src/application/policies/merge_setup_overrides_policy.ts) · [`src/application/policies/setup_configuration_storage_policy.ts`](../src/application/policies/setup_configuration_storage_policy.ts) · [`src/application/policies/setup_doctor_message_catalog.ts`](../src/application/policies/setup_doctor_message_catalog.ts) · [`src/application/policies/setup_doctor_report_policy.ts`](../src/application/policies/setup_doctor_report_policy.ts) · [`src/application/policies/setup_journey_policy.ts`](../src/application/policies/setup_journey_policy.ts) · [`src/application/policies/setup_permission_summary_policy.ts`](../src/application/policies/setup_permission_summary_policy.ts) · [`src/application/usecases/setup/setup_wizard_use_case.ts`](../src/application/usecases/setup/setup_wizard_use_case.ts) · [`src/application/usecases/setup/setup_journey_use_case.ts`](../src/application/usecases/setup/setup_journey_use_case.ts) · [`src/application/usecases/setup/setup_session_coordinator.ts`](../src/application/usecases/setup/setup_session_coordinator.ts) · [`src/application/usecases/setup/setup_questionnaire_controller.ts`](../src/application/usecases/setup/setup_questionnaire_controller.ts) · [`src/application/usecases/setup/setup_credentials_use_case.ts`](../src/application/usecases/setup/setup_credentials_use_case.ts) · [`src/application/usecases/setup/setup_token_permissions_use_case.ts`](../src/application/usecases/setup/setup_token_permissions_use_case.ts) · [`src/application/usecases/setup/doctor_use_case.ts`](../src/application/usecases/setup/doctor_use_case.ts) · [`src/application/usecases/setup/merge_queue_readiness_use_case.ts`](../src/application/usecases/setup/merge_queue_readiness_use_case.ts) · [`src/application/usecases/actions/initial_setup_workflow.ts`](../src/application/usecases/actions/initial_setup_workflow.ts) · [`src/application/usecases/actions/setup_resource_provisioning.ts`](../src/application/usecases/actions/setup_resource_provisioning.ts) · [`src/application/ports/message_catalog_ports.ts`](../src/application/ports/message_catalog_ports.ts) · [`src/application/usecases/localization/resolve_message_catalog_use_case.ts`](../src/application/usecases/localization/resolve_message_catalog_use_case.ts) · [`src/application/policies/setup_configuration_validation.ts`](../src/application/policies/setup_configuration_validation.ts) · [`src/infrastructure/setup_workspace_adapter.ts`](../src/infrastructure/setup_workspace_adapter.ts) · [`src/data/repository/repository_variables_repository.ts`](../src/data/repository/repository_variables_repository.ts) · [`src/infrastructure/github/ports/github_repository_variables_protocol.ts`](../src/infrastructure/github/ports/github_repository_variables_protocol.ts) · [`src/infrastructure/setup_remote_credential_health_adapter.ts`](../src/infrastructure/setup_remote_credential_health_adapter.ts) · [`src/infrastructure/setup_credential_validation_adapter.ts`](../src/infrastructure/setup_credential_validation_adapter.ts) · [`src/infrastructure/setup_token_permission_query_adapter.ts`](../src/infrastructure/setup_token_permission_query_adapter.ts) · [`src/infrastructure/setup_permission_actions_probe.ts`](../src/infrastructure/setup_permission_actions_probe.ts) · [`src/infrastructure/setup_permission_issue_probe.ts`](../src/infrastructure/setup_permission_issue_probe.ts) · [`src/infrastructure/setup_permission_issue_cleanup.ts`](../src/infrastructure/setup_permission_issue_cleanup.ts) · [`src/infrastructure/setup_permission_probe_journal.ts`](../src/infrastructure/setup_permission_probe_journal.ts) · [`src/infrastructure/setup_permission_probe_transaction.ts`](../src/infrastructure/setup_permission_probe_transaction.ts) · [`src/infrastructure/setup_permission_resource_probes.ts`](../src/infrastructure/setup_permission_resource_probes.ts) · [`src/infrastructure/setup_projects_probe_page_policy.ts`](../src/infrastructure/setup_projects_probe_page_policy.ts) · [`src/cli/setup_terminal_driver.ts`](../src/cli/setup_terminal_driver.ts) · [`src/cli/setup_question_renderer.ts`](../src/cli/setup_question_renderer.ts) · [`src/cli/setup_journey_presenter.ts`](../src/cli/setup_journey_presenter.ts) · [`src/cli/setup_plan_presenter.ts`](../src/cli/setup_plan_presenter.ts) · [`src/cli/setup_doctor_presenter.ts`](../src/cli/setup_doctor_presenter.ts) · [`src/cli/setup_prompt_rendering.ts`](../src/cli/setup_prompt_rendering.ts) · [`src/cli/setup_credential_prompt_adapter.ts`](../src/cli/setup_credential_prompt_adapter.ts) · [`src/cli/setup_credential_collection.ts`](../src/cli/setup_credential_collection.ts) · [`src/cli/commands/setup_execution.ts`](../src/cli/commands/setup_execution.ts) · [`src/cli/setup_token_permission_presenter.ts`](../src/cli/setup_token_permission_presenter.ts) · [`src/infrastructure/composition/setup_credentials_composition_root.ts`](../src/infrastructure/composition/setup_credentials_composition_root.ts) · [`src/infrastructure/composition/setup_token_permissions_composition_root.ts`](../src/infrastructure/composition/setup_token_permissions_composition_root.ts) · [`src/infrastructure/composition/setup_doctor_composition_root.ts`](../src/infrastructure/composition/setup_doctor_composition_root.ts) · [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`scripts/documentation_pat_exception_policy.cjs`](../scripts/documentation_pat_exception_policy.cjs) · [`scripts/validate-documentation-contract.cjs`](../scripts/validate-documentation-contract.cjs) · [`src/infrastructure/http_deadline.ts`](../src/infrastructure/http_deadline.ts) · [`src/infrastructure/setup_permission_denial.ts`](../src/infrastructure/setup_permission_denial.ts) · [`src/infrastructure/setup_permission_read_probe.ts`](../src/infrastructure/setup_permission_read_probe.ts) · [`src/infrastructure/setup_permission_read_evidence.ts`](../src/infrastructure/setup_permission_read_evidence.ts) · [`src/infrastructure/setup_permission_read_shape.ts`](../src/infrastructure/setup_permission_read_shape.ts) · [`src/infrastructure/setup_permission_projects_read.ts`](../src/infrastructure/setup_permission_projects_read.ts) · [`src/infrastructure/setup_permission_projects_access.ts`](../src/infrastructure/setup_permission_projects_access.ts) · [`src/infrastructure/setup_permission_workflow_cleanup.ts`](../src/infrastructure/setup_permission_workflow_cleanup.ts) · [`src/infrastructure/setup_permission_project_cleanup.ts`](../src/infrastructure/setup_permission_project_cleanup.ts) · [`src/infrastructure/setup_permission_probe_http.ts`](../src/infrastructure/setup_permission_probe_http.ts) · [`src/infrastructure/setup_permission_pull_request_probe.ts`](../src/infrastructure/setup_permission_pull_request_probe.ts) · [`src/infrastructure/git_authentication_environment.ts`](../src/infrastructure/git_authentication_environment.ts) · [`src/actions/github_action_input.ts`](../src/actions/github_action_input.ts) · [`src/infrastructure/setup_github_identity_query_adapter.ts`](../src/infrastructure/setup_github_identity_query_adapter.ts) · [`src/infrastructure/github_secret_encryption.ts`](../src/infrastructure/github_secret_encryption.ts) · [`src/infrastructure/setup_permission_organization_owner.ts`](../src/infrastructure/setup_permission_organization_owner.ts) · [`src/data/repository/github/github_actions_resource_inspector.ts`](../src/data/repository/github/github_actions_resource_inspector.ts) · [`src/data/repository/github/github_actions_resource_commands.ts`](../src/data/repository/github/github_actions_resource_commands.ts) · [`src/data/repository/github/github_actions_resource_collection.ts`](../src/data/repository/github/github_actions_resource_collection.ts) · [`src/application/usecases/setup/resolve_setup_workflow_pat_conflict_use_case.ts`](../src/application/usecases/setup/resolve_setup_workflow_pat_conflict_use_case.ts) · [`src/cli/setup_workflow_pat_storage_copy.ts`](../src/cli/setup_workflow_pat_storage_copy.ts) · [`src/cli/commands/setup_policy.ts`](../src/cli/commands/setup_policy.ts) · [`src/application/usecases/execution/execution_issue_number_policy.ts`](../src/application/usecases/execution/execution_issue_number_policy.ts) · [`src/application/usecases/execution/setup_execution_workflow.ts`](../src/application/usecases/execution/setup_execution_workflow.ts) · [`src/domain/github_runtime_doctor.ts`](../src/domain/github_runtime_doctor.ts) · [`src/domain/copilot_command.ts`](../src/domain/copilot_command.ts) · [`src/application/usecases/comment_automation_command_workflow.ts`](../src/application/usecases/comment_automation_command_workflow.ts) · [`src/application/policies/copilot_interaction_policy.ts`](../src/application/policies/copilot_interaction_policy.ts) · [`src/application/policies/publication_message_catalog.ts`](../src/application/policies/publication_message_catalog.ts) · [`src/application/ports/github_runtime_doctor_ports.ts`](../src/application/ports/github_runtime_doctor_ports.ts) · [`src/application/policies/github_runtime_doctor_policy.ts`](../src/application/policies/github_runtime_doctor_policy.ts) · [`src/application/policies/doctor_workflow_condition_policy.ts`](../src/application/policies/doctor_workflow_condition_policy.ts) · [`src/application/policies/github_runtime_doctor_presentation_policy.ts`](../src/application/policies/github_runtime_doctor_presentation_policy.ts) · [`src/application/usecases/read_copilot_doctor_workflow.ts`](../src/application/usecases/read_copilot_doctor_workflow.ts) · [`src/data/repository/github/runtime_doctor_repository.ts`](../src/data/repository/github/runtime_doctor_repository.ts) · [`src/infrastructure/composition/github_runtime_doctor_composition_root.ts`](../src/infrastructure/composition/github_runtime_doctor_composition_root.ts) · [`src/application/policies/agent_activity_policy.ts`](../src/application/policies/agent_activity_policy.ts) · [`src/application/policies/doctor_workflow_matrix_policy.ts`](../src/application/policies/doctor_workflow_matrix_policy.ts) · [`src/application/contracts/setup_batch_view.ts`](../src/application/contracts/setup_batch_view.ts) · [`src/application/policies/issue_resource_input_policy.ts`](../src/application/policies/issue_resource_input_policy.ts) · [`src/application/policies/setup_batch_access_policy.ts`](../src/application/policies/setup_batch_access_policy.ts) · [`src/application/policies/setup_batch_policy.ts`](../src/application/policies/setup_batch_policy.ts) · [`src/application/policies/setup_batch_retry_policy.ts`](../src/application/policies/setup_batch_retry_policy.ts) · [`src/application/policies/setup_batch_selection_policy.ts`](../src/application/policies/setup_batch_selection_policy.ts) · [`src/application/policies/setup_batch_view_policy.ts`](../src/application/policies/setup_batch_view_policy.ts) · [`src/application/policies/setup_credential_binding_policy.ts`](../src/application/policies/setup_credential_binding_policy.ts) · [`src/application/policies/setup_organization_import_policy.ts`](../src/application/policies/setup_organization_import_policy.ts) · [`src/application/policies/setup_organization_variable_bindings.ts`](../src/application/policies/setup_organization_variable_bindings.ts) · [`src/application/policies/setup_organization_variable_reuse_policy.ts`](../src/application/policies/setup_organization_variable_reuse_policy.ts) · [`src/application/ports/setup_batch_ports.ts`](../src/application/ports/setup_batch_ports.ts) · [`src/application/usecases/setup/apply_setup_batch_use_case.ts`](../src/application/usecases/setup/apply_setup_batch_use_case.ts) · [`src/application/usecases/setup/configure_setup_batch_target_use_case.ts`](../src/application/usecases/setup/configure_setup_batch_target_use_case.ts) · [`src/application/usecases/setup/prepare_setup_batch_use_case.ts`](../src/application/usecases/setup/prepare_setup_batch_use_case.ts) · [`src/application/usecases/setup/select_setup_batch_targets_use_case.ts`](../src/application/usecases/setup/select_setup_batch_targets_use_case.ts) · [`src/application/usecases/setup/setup_batch_session_use_case.ts`](../src/application/usecases/setup/setup_batch_session_use_case.ts) · [`src/cli/setup_batch_adapter.ts`](../src/cli/setup_batch_adapter.ts) · [`src/cli/setup_source_adapter.ts`](../src/cli/setup_source_adapter.ts) · [`src/domain/setup_batch.ts`](../src/domain/setup_batch.ts) · [`src/domain/setup_credential_health.ts`](../src/domain/setup_credential_health.ts) · [`src/domain/setup_organization_reuse.ts`](../src/domain/setup_organization_reuse.ts) · [`src/infrastructure/composition/setup_batch_composition_root.ts`](../src/infrastructure/composition/setup_batch_composition_root.ts) · [`src/infrastructure/github/ports/github_setup_batch_protocol.ts`](../src/infrastructure/github/ports/github_setup_batch_protocol.ts) · [`src/infrastructure/github/ports/github_trusted_credential_health_protocol.ts`](../src/infrastructure/github/ports/github_trusted_credential_health_protocol.ts) · [`src/infrastructure/setup_batch_github_apply_adapter.ts`](../src/infrastructure/setup_batch_github_apply_adapter.ts) · [`src/infrastructure/setup_batch_github_read_adapter.ts`](../src/infrastructure/setup_batch_github_read_adapter.ts) · [`src/infrastructure/setup_batch_journal_adapter.ts`](../src/infrastructure/setup_batch_journal_adapter.ts) · [`src/infrastructure/setup_batch_workspace_adapter.ts`](../src/infrastructure/setup_batch_workspace_adapter.ts) · [`src/infrastructure/setup_trusted_credential_health_adapter.ts`](../src/infrastructure/setup_trusted_credential_health_adapter.ts) · [`src/application/policies/setup_remote_facts_policy.ts`](../src/application/policies/setup_remote_facts_policy.ts) · [`src/cli/setup_confirmation_adapter.ts`](../src/cli/setup_confirmation_adapter.ts) · [`src/cli/web_setup_adapters.ts`](../src/cli/web_setup_adapters.ts) · [`scripts/validate-agent-documentation.cjs`](../scripts/validate-agent-documentation.cjs) · [`src/utils/setup_workflow_upgrade.ts`](../src/utils/setup_workflow_upgrade.ts) · [`src/domain/setup_runner.ts`](../src/domain/setup_runner.ts) · [`src/application/policies/setup_runner_policy.ts`](../src/application/policies/setup_runner_policy.ts) · [`src/application/policies/setup_runner_question_policy.ts`](../src/application/policies/setup_runner_question_policy.ts) · [`src/application/policies/setup_runner_guidance_policy.ts`](../src/application/policies/setup_runner_guidance_policy.ts) · [`src/application/ports/setup_runner_ports.ts`](../src/application/ports/setup_runner_ports.ts) · [`src/infrastructure/setup_runner_discovery_adapter.ts`](../src/infrastructure/setup_runner_discovery_adapter.ts) · [`src/infrastructure/composition/setup_runner_composition_root.ts`](../src/infrastructure/composition/setup_runner_composition_root.ts) · [`src/utils/setup_installation_review.ts`](../src/utils/setup_installation_review.ts) · [`src/cli/setup_installation_review_presenter.ts`](../src/cli/setup_installation_review_presenter.ts) · [`src/application/policies/setup_runner_installation_summary_policy.ts`](../src/application/policies/setup_runner_installation_summary_policy.ts) · [`src/utils/setup_files.ts`](../src/utils/setup_files.ts) · [`src/domain/specification_catalog_path_policy.cjs`](../src/domain/specification_catalog_path_policy.cjs) · [`src/domain/sdd_prerequisite_policy.cjs`](../src/domain/sdd_prerequisite_policy.cjs) · [`src/application/contracts/setup_documentation.ts`](../src/application/contracts/setup_documentation.ts) · [`src/application/policies/setup_question_documentation_policy.ts`](../src/application/policies/setup_question_documentation_policy.ts) · [`web/src/components/SetupHelp.svelte`](../web/src/components/SetupHelp.svelte) · [`web/src/components/CompletedSetupHelp.svelte`](../web/src/components/CompletedSetupHelp.svelte) · [`src/application/policies/verification_runtime_policy.ts`](../src/application/policies/verification_runtime_policy.ts) · [`src/infrastructure/verification_runtime_manifest.ts`](../src/infrastructure/verification_runtime_manifest.ts) · [`src/infrastructure/verification_runtime_preflight.ts`](../src/infrastructure/verification_runtime_preflight.ts) · [`src/infrastructure/untrusted_command_environment.ts`](../src/infrastructure/untrusted_command_environment.ts) · [`src/application/ports/git_ports.ts`](../src/application/ports/git_ports.ts) · [`src/application/ports/bugbot_git_ports.ts`](../src/application/ports/bugbot_git_ports.ts) · [`src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts`](../src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts) · [`src/application/usecases/steps/commit/bugbot/verify_command_runner.ts`](../src/application/usecases/steps/commit/bugbot/verify_command_runner.ts) · [`src/cli/setup_config_file.ts`](../src/cli/setup_config_file.ts) +- Tests: [`src/data/repository/__tests__/repository_version_tags_query_repository.test.ts`](../src/data/repository/__tests__/repository_version_tags_query_repository.test.ts) · [`src/cli/__tests__/setup_command_options.test.ts`](../src/cli/__tests__/setup_command_options.test.ts) · [`src/data/repository/__tests__/setup_variable_write_failures.test.ts`](../src/data/repository/__tests__/setup_variable_write_failures.test.ts) · [`src/infrastructure/composition/__tests__/initial_setup_composition_root.test.ts`](../src/infrastructure/composition/__tests__/initial_setup_composition_root.test.ts) · [`src/application/policies/__tests__/setup_questionnaire_policy.test.ts`](../src/application/policies/__tests__/setup_questionnaire_policy.test.ts) · [`src/application/policies/__tests__/setup_configuration_policy.test.ts`](../src/application/policies/__tests__/setup_configuration_policy.test.ts) · [`src/application/policies/__tests__/setup_token_permission_policy.test.ts`](../src/application/policies/__tests__/setup_token_permission_policy.test.ts) · [`src/application/policies/__tests__/setup_remote_facts_policy.test.ts`](../src/application/policies/__tests__/setup_remote_facts_policy.test.ts) · [`src/application/policies/__tests__/setup_doctor_message_catalog.test.ts`](../src/application/policies/__tests__/setup_doctor_message_catalog.test.ts) · [`src/application/policies/__tests__/setup_doctor_report_policy.test.ts`](../src/application/policies/__tests__/setup_doctor_report_policy.test.ts) · [`src/application/usecases/setup/__tests__/setup_questionnaire_controller.test.ts`](../src/application/usecases/setup/__tests__/setup_questionnaire_controller.test.ts) · [`src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts`](../src/application/usecases/setup/__tests__/setup_wizard_use_case.test.ts) · [`src/application/usecases/setup/__tests__/setup_journey_use_case.test.ts`](../src/application/usecases/setup/__tests__/setup_journey_use_case.test.ts) · [`src/application/usecases/setup/__tests__/setup_session_coordinator.test.ts`](../src/application/usecases/setup/__tests__/setup_session_coordinator.test.ts) · [`src/application/usecases/setup/__tests__/setup_credentials_use_case.test.ts`](../src/application/usecases/setup/__tests__/setup_credentials_use_case.test.ts) · [`src/application/usecases/setup/__tests__/setup_token_permissions_use_case.test.ts`](../src/application/usecases/setup/__tests__/setup_token_permissions_use_case.test.ts) · [`src/application/usecases/setup/__tests__/doctor_use_case.test.ts`](../src/application/usecases/setup/__tests__/doctor_use_case.test.ts) · [`src/application/usecases/setup/__tests__/merge_queue_readiness_use_case.test.ts`](../src/application/usecases/setup/__tests__/merge_queue_readiness_use_case.test.ts) · [`src/application/usecases/actions/__tests__/initial_setup_use_case.test.ts`](../src/application/usecases/actions/__tests__/initial_setup_use_case.test.ts) · [`src/application/usecases/actions/__tests__/setup_resource_provisioning.test.ts`](../src/application/usecases/actions/__tests__/setup_resource_provisioning.test.ts) · [`src/infrastructure/__tests__/setup_workspace_adapter.test.ts`](../src/infrastructure/__tests__/setup_workspace_adapter.test.ts) · [`src/infrastructure/__tests__/setup_remote_credential_health_adapter.test.ts`](../src/infrastructure/__tests__/setup_remote_credential_health_adapter.test.ts) · [`src/infrastructure/__tests__/setup_token_permission_query_adapter.test.ts`](../src/infrastructure/__tests__/setup_token_permission_query_adapter.test.ts) · [`src/infrastructure/__tests__/setup_permission_resource_probes.test.ts`](../src/infrastructure/__tests__/setup_permission_resource_probes.test.ts) · [`src/infrastructure/__tests__/setup_projects_probe_page_policy.test.ts`](../src/infrastructure/__tests__/setup_projects_probe_page_policy.test.ts) · [`src/infrastructure/composition/__tests__/setup_token_permissions_composition_root.test.ts`](../src/infrastructure/composition/__tests__/setup_token_permissions_composition_root.test.ts) · [`src/data/repository/__tests__/repository_variables_repository.test.ts`](../src/data/repository/__tests__/repository_variables_repository.test.ts) · [`src/cli/__tests__/setup_presenters.test.ts`](../src/cli/__tests__/setup_presenters.test.ts) · [`src/cli/__tests__/setup_journey_presenter.test.ts`](../src/cli/__tests__/setup_journey_presenter.test.ts) · [`src/cli/__tests__/setup_prompt_rendering.test.ts`](../src/cli/__tests__/setup_prompt_rendering.test.ts) · [`src/cli/__tests__/setup_token_permission_presenter.test.ts`](../src/cli/__tests__/setup_token_permission_presenter.test.ts) · [`src/__tests__/cli.test.ts`](../src/__tests__/cli.test.ts) · [`src/cli/__tests__/setup_terminal_driver.test.ts`](../src/cli/__tests__/setup_terminal_driver.test.ts) · [`src/architecture/__tests__/setup_doctor_boundaries.test.ts`](../src/architecture/__tests__/setup_doctor_boundaries.test.ts) · [`src/tooling/__tests__/documentation_pat_exception_policy.test.ts`](../src/tooling/__tests__/documentation_pat_exception_policy.test.ts) · [`src/infrastructure/__tests__/setup_pat_audit.test.ts`](../src/infrastructure/__tests__/setup_pat_audit.test.ts) · [`src/infrastructure/__tests__/setup_permission_projects_access.test.ts`](../src/infrastructure/__tests__/setup_permission_projects_access.test.ts) · [`src/infrastructure/__tests__/setup_credential_validation_adapter.test.ts`](../src/infrastructure/__tests__/setup_credential_validation_adapter.test.ts) · [`src/infrastructure/__tests__/setup_github_identity_query_adapter.test.ts`](../src/infrastructure/__tests__/setup_github_identity_query_adapter.test.ts) · [`src/infrastructure/__tests__/git_commit_adapter.test.ts`](../src/infrastructure/__tests__/git_commit_adapter.test.ts) · [`src/actions/__tests__/github_action_input.test.ts`](../src/actions/__tests__/github_action_input.test.ts) · [`src/infrastructure/__tests__/setup_permission_cleanup_contract.test.ts`](../src/infrastructure/__tests__/setup_permission_cleanup_contract.test.ts) · [`src/infrastructure/__tests__/setup_permission_actions_cleanup_timing.test.ts`](../src/infrastructure/__tests__/setup_permission_actions_cleanup_timing.test.ts) · [`src/infrastructure/__tests__/setup_permission_operator_actions.test.ts`](../src/infrastructure/__tests__/setup_permission_operator_actions.test.ts) · [`src/infrastructure/__tests__/setup_permission_journal_contract.test.ts`](../src/infrastructure/__tests__/setup_permission_journal_contract.test.ts) · [`src/infrastructure/__tests__/setup_permission_audit_transactions.test.ts`](../src/infrastructure/__tests__/setup_permission_audit_transactions.test.ts) · [`src/infrastructure/__tests__/setup_permission_write_failures.test.ts`](../src/infrastructure/__tests__/setup_permission_write_failures.test.ts) · [`src/infrastructure/__tests__/setup_permission_resource_failures.test.ts`](../src/infrastructure/__tests__/setup_permission_resource_failures.test.ts) · [`src/infrastructure/__tests__/setup_permission_boundary_failures.test.ts`](../src/infrastructure/__tests__/setup_permission_boundary_failures.test.ts) · [`src/infrastructure/__tests__/github_secret_encryption.test.ts`](../src/infrastructure/__tests__/github_secret_encryption.test.ts) · [`src/infrastructure/__tests__/setup_permission_organization_owner.test.ts`](../src/infrastructure/__tests__/setup_permission_organization_owner.test.ts) · [`src/infrastructure/__tests__/setup_pat_bootstrap_transactions.test.ts`](../src/infrastructure/__tests__/setup_pat_bootstrap_transactions.test.ts) · [`src/data/repository/__tests__/repository_actions_sdk_contract.integration.test.ts`](../src/data/repository/__tests__/repository_actions_sdk_contract.integration.test.ts) · [`src/data/repository/__tests__/repository_actions_resource_edge_cases.test.ts`](../src/data/repository/__tests__/repository_actions_resource_edge_cases.test.ts) · [`src/data/repository/__tests__/repository_selected_resource_grants.test.ts`](../src/data/repository/__tests__/repository_selected_resource_grants.test.ts) · [`src/infrastructure/__tests__/setup_permission_read_contract.test.ts`](../src/infrastructure/__tests__/setup_permission_read_contract.test.ts) · [`src/application/usecases/setup/__tests__/setup_workflow_pat_conflict.test.ts`](../src/application/usecases/setup/__tests__/setup_workflow_pat_conflict.test.ts) · [`src/application/usecases/setup/__tests__/setup_workflow_pat_credential_rotation.test.ts`](../src/application/usecases/setup/__tests__/setup_workflow_pat_credential_rotation.test.ts) · [`src/data/repository/__tests__/setup_organization_pat_inventory.test.ts`](../src/data/repository/__tests__/setup_organization_pat_inventory.test.ts) · [`src/cli/__tests__/web_setup_bot_pat_scope_conflict.test.ts`](../src/cli/__tests__/web_setup_bot_pat_scope_conflict.test.ts) · [`src/actions/__tests__/local_setup_apply_contract.test.ts`](../src/actions/__tests__/local_setup_apply_contract.test.ts) · [`src/cli/commands/__tests__/setup_policy.test.ts`](../src/cli/commands/__tests__/setup_policy.test.ts) · [`src/application/usecases/execution/__tests__/execution_issue_number_policy.test.ts`](../src/application/usecases/execution/__tests__/execution_issue_number_policy.test.ts) · [`src/application/usecases/execution/__tests__/setup_execution_workflow.test.ts`](../src/application/usecases/execution/__tests__/setup_execution_workflow.test.ts) · [`src/domain/__tests__/github_runtime_doctor.test.ts`](../src/domain/__tests__/github_runtime_doctor.test.ts) · [`src/domain/__tests__/copilot_command.test.ts`](../src/domain/__tests__/copilot_command.test.ts) · [`src/application/usecases/__tests__/comment_automation_use_case.test.ts`](../src/application/usecases/__tests__/comment_automation_use_case.test.ts) · [`src/application/policies/__tests__/github_runtime_doctor_policy.test.ts`](../src/application/policies/__tests__/github_runtime_doctor_policy.test.ts) · [`src/application/policies/__tests__/doctor_workflow_condition_policy.test.ts`](../src/application/policies/__tests__/doctor_workflow_condition_policy.test.ts) · [`src/application/policies/__tests__/github_runtime_doctor_presentation_policy.test.ts`](../src/application/policies/__tests__/github_runtime_doctor_presentation_policy.test.ts) · [`src/application/usecases/__tests__/read_copilot_doctor_workflow.test.ts`](../src/application/usecases/__tests__/read_copilot_doctor_workflow.test.ts) · [`src/application/usecases/__tests__/runtime_doctor_security_integration.test.ts`](../src/application/usecases/__tests__/runtime_doctor_security_integration.test.ts) · [`src/data/repository/github/__tests__/runtime_doctor_repository.test.ts`](../src/data/repository/github/__tests__/runtime_doctor_repository.test.ts) · [`src/infrastructure/composition/__tests__/github_runtime_doctor_composition_root.test.ts`](../src/infrastructure/composition/__tests__/github_runtime_doctor_composition_root.test.ts) · [`src/application/policies/__tests__/runtime_doctor_agent_activity.test.ts`](../src/application/policies/__tests__/runtime_doctor_agent_activity.test.ts) · [`src/application/policies/__tests__/doctor_workflow_matrix_policy.test.ts`](../src/application/policies/__tests__/doctor_workflow_matrix_policy.test.ts) · [`src/application/policies/__tests__/setup_approval_operator_permissions.test.ts`](../src/application/policies/__tests__/setup_approval_operator_permissions.test.ts) · [`src/application/usecases/setup/__tests__/audit_configured_setup_pat_use_case.test.ts`](../src/application/usecases/setup/__tests__/audit_configured_setup_pat_use_case.test.ts) · [`src/application/policies/__tests__/setup_batch_permission_policy.test.ts`](../src/application/policies/__tests__/setup_batch_permission_policy.test.ts) · [`src/application/policies/__tests__/setup_batch_policy.test.ts`](../src/application/policies/__tests__/setup_batch_policy.test.ts) · [`src/application/policies/__tests__/setup_batch_retry_policy.test.ts`](../src/application/policies/__tests__/setup_batch_retry_policy.test.ts) · [`src/application/policies/__tests__/setup_batch_selection_policy.test.ts`](../src/application/policies/__tests__/setup_batch_selection_policy.test.ts) · [`src/application/policies/__tests__/setup_organization_approval_draft.test.ts`](../src/application/policies/__tests__/setup_organization_approval_draft.test.ts) · [`src/application/policies/__tests__/setup_organization_import_policy.test.ts`](../src/application/policies/__tests__/setup_organization_import_policy.test.ts) · [`src/application/policies/__tests__/setup_organization_import_safety.test.ts`](../src/application/policies/__tests__/setup_organization_import_safety.test.ts) · [`src/application/policies/__tests__/setup_organization_variable_reuse_policy.test.ts`](../src/application/policies/__tests__/setup_organization_variable_reuse_policy.test.ts) · [`src/application/usecases/actions/__tests__/setup_organization_reuse_provisioning.test.ts`](../src/application/usecases/actions/__tests__/setup_organization_reuse_provisioning.test.ts) · [`src/application/usecases/setup/__tests__/apply_setup_batch_use_case.test.ts`](../src/application/usecases/setup/__tests__/apply_setup_batch_use_case.test.ts) · [`src/application/usecases/setup/__tests__/select_setup_batch_targets_use_case.test.ts`](../src/application/usecases/setup/__tests__/select_setup_batch_targets_use_case.test.ts) · [`src/application/usecases/setup/__tests__/setup_batch_flow.test.ts`](../src/application/usecases/setup/__tests__/setup_batch_flow.test.ts) · [`src/application/usecases/setup/__tests__/setup_organization_import_wizard.test.ts`](../src/application/usecases/setup/__tests__/setup_organization_import_wizard.test.ts) · [`src/application/usecases/setup/__tests__/setup_organization_secret_reuse.test.ts`](../src/application/usecases/setup/__tests__/setup_organization_secret_reuse.test.ts) · [`src/cli/__tests__/setup_source_adapter.test.ts`](../src/cli/__tests__/setup_source_adapter.test.ts) · [`src/cli/__tests__/web_setup_batch_components.test.ts`](../src/cli/__tests__/web_setup_batch_components.test.ts) · [`src/cli/__tests__/web_setup_batch_selection.test.ts`](../src/cli/__tests__/web_setup_batch_selection.test.ts) · [`src/infrastructure/__tests__/setup_batch_github_apply_adapter.test.ts`](../src/infrastructure/__tests__/setup_batch_github_apply_adapter.test.ts) · [`src/infrastructure/__tests__/setup_batch_github_read_adapter.test.ts`](../src/infrastructure/__tests__/setup_batch_github_read_adapter.test.ts) · [`src/infrastructure/__tests__/setup_batch_local_adapters.test.ts`](../src/infrastructure/__tests__/setup_batch_local_adapters.test.ts) · [`src/infrastructure/__tests__/setup_reuse_empty_collections.integration.test.ts`](../src/infrastructure/__tests__/setup_reuse_empty_collections.integration.test.ts) · [`src/infrastructure/__tests__/setup_trusted_credential_health_adapter.test.ts`](../src/infrastructure/__tests__/setup_trusted_credential_health_adapter.test.ts) · [`src/infrastructure/__tests__/setup_trusted_health_sdk_contract.integration.test.ts`](../src/infrastructure/__tests__/setup_trusted_health_sdk_contract.integration.test.ts) · [`src/application/policies/__tests__/setup_batch_access_and_view.test.ts`](../src/application/policies/__tests__/setup_batch_access_and_view.test.ts) · [`src/cli/__tests__/setup_batch_adapter.test.ts`](../src/cli/__tests__/setup_batch_adapter.test.ts) · [`src/infrastructure/__tests__/setup_batch_composition.test.ts`](../src/infrastructure/__tests__/setup_batch_composition.test.ts) · [`src/infrastructure/__tests__/setup_batch_guidance_compatibility.test.ts`](../src/infrastructure/__tests__/setup_batch_guidance_compatibility.test.ts) · [`src/utils/__tests__/setup_workflow_upgrade.test.ts`](../src/utils/__tests__/setup_workflow_upgrade.test.ts) · [`src/cli/__tests__/setup_pat_intent_adapter.test.ts`](../src/cli/__tests__/setup_pat_intent_adapter.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/application/policies/__tests__/setup_runner_policy.test.ts`](../src/application/policies/__tests__/setup_runner_policy.test.ts) · [`src/application/policies/__tests__/setup_runner_question_policy.test.ts`](../src/application/policies/__tests__/setup_runner_question_policy.test.ts) · [`src/infrastructure/__tests__/setup_runner_discovery_adapter.test.ts`](../src/infrastructure/__tests__/setup_runner_discovery_adapter.test.ts) · [`src/utils/__tests__/setup_runner_installation.test.ts`](../src/utils/__tests__/setup_runner_installation.test.ts) · [`src/cli/__tests__/web_setup_runner_components.test.ts`](../src/cli/__tests__/web_setup_runner_components.test.ts) · [`src/application/policies/__tests__/setup_runner_installation_summary_policy.test.ts`](../src/application/policies/__tests__/setup_runner_installation_summary_policy.test.ts) · [`src/application/usecases/setup/__tests__/setup_runner_planning.test.ts`](../src/application/usecases/setup/__tests__/setup_runner_planning.test.ts) · [`src/cli/__tests__/setup_runner_config_file.test.ts`](../src/cli/__tests__/setup_runner_config_file.test.ts) · [`src/infrastructure/__tests__/setup_runner_composition.test.ts`](../src/infrastructure/__tests__/setup_runner_composition.test.ts) · [`src/tooling/__tests__/setup_provisioning_workflow.test.ts`](../src/tooling/__tests__/setup_provisioning_workflow.test.ts) · [`src/infrastructure/__tests__/setup_sdd_prerequisites.test.ts`](../src/infrastructure/__tests__/setup_sdd_prerequisites.test.ts) · [`src/cli/__tests__/setup_documentation_navigation.test.ts`](../src/cli/__tests__/setup_documentation_navigation.test.ts) · [`src/application/policies/__tests__/setup_question_documentation_policy.test.ts`](../src/application/policies/__tests__/setup_question_documentation_policy.test.ts) · [`src/application/policies/__tests__/verification_runtime_policy.test.ts`](../src/application/policies/__tests__/verification_runtime_policy.test.ts) · [`src/infrastructure/__tests__/verification_runtime_preflight.test.ts`](../src/infrastructure/__tests__/verification_runtime_preflight.test.ts) · [`src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts`](../src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts) · [`src/infrastructure/__tests__/untrusted_command_environment.test.ts`](../src/infrastructure/__tests__/untrusted_command_environment.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts) · [`src/tooling/__tests__/verification_runtime_guidance.test.ts`](../src/tooling/__tests__/verification_runtime_guidance.test.ts) · [`src/infrastructure/__tests__/verification_setup_configuration.test.ts`](../src/infrastructure/__tests__/verification_setup_configuration.test.ts) - User documentation: [`README.md`](../README.md) · [`docs/how-to-use.mdx`](../docs/how-to-use.mdx) · [`docs/configuration.mdx`](../docs/configuration.mdx) · [`docs/configuration-checklist.mdx`](../docs/configuration-checklist.mdx) · [`docs/authentication.mdx`](../docs/authentication.mdx) · [`docs/development/architecture.mdx`](../docs/development/architecture.mdx) · [`docs/security-operations/operations/provisioning.mdx`](../docs/security-operations/operations/provisioning.mdx) · [`docs/security-operations/operations/troubleshooting.mdx`](../docs/security-operations/operations/troubleshooting.mdx) · [`docs/security-operations/security/credentials.mdx`](../docs/security-operations/security/credentials.mdx) · [`docs/single-actions/workflow-and-cli.mdx`](../docs/single-actions/workflow-and-cli.mdx) · [`docs/security-operations/operations/verification.mdx`](../docs/security-operations/operations/verification.mdx) · [`docs/development/setup-pat-provider-audit-2026-10-06.md`](../docs/development/setup-pat-provider-audit-2026-10-06.md) · [`docs/issues/comment-commands.mdx`](../docs/issues/comment-commands.mdx) · [`docs/issues/branch-synchronization.mdx`](../docs/issues/branch-synchronization.mdx) · [`docs/setup-web.mdx`](../docs/setup-web.mdx) · [`docs/first-workflow.mdx`](../docs/first-workflow.mdx) · [`docs/using-the-docs.mdx`](../docs/using-the-docs.mdx) · [`docs/guides/index.mdx`](../docs/guides/index.mdx) · [`docs/guides/issues-and-branches.mdx`](../docs/guides/issues-and-branches.mdx) · [`docs/guides/review-pull-requests.mdx`](../docs/guides/review-pull-requests.mdx) · [`docs/guides/work-with-the-bot.mdx`](../docs/guides/work-with-the-bot.mdx) · [`docs/guides/releases-and-hotfixes.mdx`](../docs/guides/releases-and-hotfixes.mdx) · [`docs/agents/repository-collaboration.mdx`](../docs/agents/repository-collaboration.mdx) ### `local-web-setup-assistant` — Local web setup assistant @@ -191,12 +191,12 @@ debt or convert unknown historic intent into a design decision. ### `branch-synchronization` — Branch synchronization and conflict recovery - Owner: Copilot maintainers -- Last verified: 2026-10-20 +- Last verified: 2026-10-21 - Specifications: [`specs/branch-synchronization-and-conflict-recovery.md`](./branch-synchronization-and-conflict-recovery.md) · [`specs/repository-verification-runtime-prerequisites.md`](./repository-verification-runtime-prerequisites.md) · [`specs/repository-verification-setup-hooks.md`](./repository-verification-setup-hooks.md) - Workflows: [`.github/workflows/copilot_branch_sync.yml`](../.github/workflows/copilot_branch_sync.yml) · [`setup/workflows/copilot_branch_sync.yml`](../setup/workflows/copilot_branch_sync.yml) · [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`setup/workflows/copilot_issue.yml`](../setup/workflows/copilot_issue.yml) · [`setup/workflows/copilot_issue_comment.yml`](../setup/workflows/copilot_issue_comment.yml) · [`setup/workflows/copilot_pull_request_comment.yml`](../setup/workflows/copilot_pull_request_comment.yml) - Entrypoints: [`src/application/usecases/branch_sync/sync_branch_use_case.ts`](../src/application/usecases/branch_sync/sync_branch_use_case.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) -- Core code: [`src/domain/branch_sync_command.ts`](../src/domain/branch_sync_command.ts) · [`src/application/ports/branch_sync_ports.ts`](../src/application/ports/branch_sync_ports.ts) · [`src/application/policies/branch_sync_message_catalog.ts`](../src/application/policies/branch_sync_message_catalog.ts) · [`src/application/policies/branch_sync_notification_policy.ts`](../src/application/policies/branch_sync_notification_policy.ts) · [`src/application/usecases/branch_sync/branch_sync_execution_policy.ts`](../src/application/usecases/branch_sync/branch_sync_execution_policy.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/application/usecases/steps/common/transition_notification_workflow.ts`](../src/application/usecases/steps/common/transition_notification_workflow.ts) · [`src/data/repository/branch_sync/branch_dependency_policy.ts`](../src/data/repository/branch_sync/branch_dependency_policy.ts) · [`src/data/repository/branch_sync/branch_dependency_repository.ts`](../src/data/repository/branch_sync/branch_dependency_repository.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/infrastructure/composition/push_single_action_capability_port_binding.ts`](../src/infrastructure/composition/push_single_action_capability_port_binding.ts) · [`src/infrastructure/branch_sync_workspace_adapter.ts`](../src/infrastructure/branch_sync_workspace_adapter.ts) · [`src/prompts/branch_sync_conflicts.ts`](../src/prompts/branch_sync_conflicts.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`src/infrastructure/branch_sync_sdd_catalog.ts`](../src/infrastructure/branch_sync_sdd_catalog.ts) · [`src/domain/branch_sync_validation.ts`](../src/domain/branch_sync_validation.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/infrastructure/branch_sync_communication_budget.ts`](../src/infrastructure/branch_sync_communication_budget.ts) · [`src/application/usecases/actions/retire_branch_sync_use_case.ts`](../src/application/usecases/actions/retire_branch_sync_use_case.ts) · [`src/application/ports/issue_completion_ports.ts`](../src/application/ports/issue_completion_ports.ts) · [`src/application/usecases/pull_request_workflow.ts`](../src/application/usecases/pull_request_workflow.ts) · [`src/application/usecases/pull_request_workflow_context.ts`](../src/application/usecases/pull_request_workflow_context.ts) · [`src/infrastructure/composition/pull_request_use_case_composition_root.ts`](../src/infrastructure/composition/pull_request_use_case_composition_root.ts) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) · [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`scripts/validate-workflow-contract.cjs`](../scripts/validate-workflow-contract.cjs) · [`src/application/ports/reply_request_read_ports.ts`](../src/application/ports/reply_request_read_ports.ts) · [`src/domain/reply_request_digest.ts`](../src/domain/reply_request_digest.ts) · [`src/infrastructure/composition/reply_request_composition_root.ts`](../src/infrastructure/composition/reply_request_composition_root.ts) · [`src/data/repository/reply_request_repository.ts`](../src/data/repository/reply_request_repository.ts) · [`src/application/policies/verification_runtime_policy.ts`](../src/application/policies/verification_runtime_policy.ts) · [`src/infrastructure/verification_runtime_manifest.ts`](../src/infrastructure/verification_runtime_manifest.ts) · [`src/infrastructure/verification_runtime_preflight.ts`](../src/infrastructure/verification_runtime_preflight.ts) · [`src/infrastructure/untrusted_command_environment.ts`](../src/infrastructure/untrusted_command_environment.ts) · [`src/application/ports/git_ports.ts`](../src/application/ports/git_ports.ts) · [`src/application/ports/bugbot_git_ports.ts`](../src/application/ports/bugbot_git_ports.ts) · [`src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts`](../src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts) · [`src/application/usecases/steps/commit/bugbot/verify_command_runner.ts`](../src/application/usecases/steps/commit/bugbot/verify_command_runner.ts) · [`src/domain/closed_issue_reconciliation.ts`](../src/domain/closed_issue_reconciliation.ts) · [`src/application/errors/closed_issue_reconciliation_error.ts`](../src/application/errors/closed_issue_reconciliation_error.ts) -- Tests: [`src/application/policies/__tests__/branch_sync_notification_policy.test.ts`](../src/application/policies/__tests__/branch_sync_notification_policy.test.ts) · [`src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts) · [`src/application/usecases/branch_sync/__tests__/sync_branch_use_case.test.ts`](../src/application/usecases/branch_sync/__tests__/sync_branch_use_case.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/application/usecases/steps/common/__tests__/transition_notification_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/transition_notification_workflow.test.ts) · [`src/data/repository/branch_sync/__tests__/branch_dependency_policy.test.ts`](../src/data/repository/branch_sync/__tests__/branch_dependency_policy.test.ts) · [`src/data/repository/branch_sync/__tests__/branch_dependency_repository.test.ts`](../src/data/repository/branch_sync/__tests__/branch_dependency_repository.test.ts) · [`src/infrastructure/__tests__/branch_sync_workspace_adapter.test.ts`](../src/infrastructure/__tests__/branch_sync_workspace_adapter.test.ts) · [`src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts) · [`src/application/usecases/branch_sync/__tests__/branch_sync_execution_policy.test.ts`](../src/application/usecases/branch_sync/__tests__/branch_sync_execution_policy.test.ts) · [`src/application/errors/__tests__/application_error.test.ts`](../src/application/errors/__tests__/application_error.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/infrastructure/__tests__/git_commit_adapter.test.ts`](../src/infrastructure/__tests__/git_commit_adapter.test.ts) · [`src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts`](../src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts) · [`src/infrastructure/__tests__/branch_sync_generated_bundle_validation.test.ts`](../src/infrastructure/__tests__/branch_sync_generated_bundle_validation.test.ts) · [`src/application/policies/__tests__/branch_sync_lifecycle_recovery.test.ts`](../src/application/policies/__tests__/branch_sync_lifecycle_recovery.test.ts) · [`src/infrastructure/__tests__/branch_sync_resolved_workspace.test.ts`](../src/infrastructure/__tests__/branch_sync_resolved_workspace.test.ts) · [`src/infrastructure/__tests__/branch_sync_communication_budget.test.ts`](../src/infrastructure/__tests__/branch_sync_communication_budget.test.ts) · [`src/application/usecases/actions/__tests__/retire_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/retire_branch_sync_use_case.test.ts) · [`src/application/usecases/__tests__/pull_request_use_case.test.ts`](../src/application/usecases/__tests__/pull_request_use_case.test.ts) · [`src/actions/__tests__/common_action.test.ts`](../src/actions/__tests__/common_action.test.ts) · [`src/actions/__tests__/github_action.test.ts`](../src/actions/__tests__/github_action.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) · [`src/data/repository/__tests__/reply_request_repository.test.ts`](../src/data/repository/__tests__/reply_request_repository.test.ts) · [`src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts`](../src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts) · [`src/application/policies/__tests__/verification_runtime_policy.test.ts`](../src/application/policies/__tests__/verification_runtime_policy.test.ts) · [`src/infrastructure/__tests__/verification_runtime_preflight.test.ts`](../src/infrastructure/__tests__/verification_runtime_preflight.test.ts) · [`src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts`](../src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts) · [`src/infrastructure/__tests__/untrusted_command_environment.test.ts`](../src/infrastructure/__tests__/untrusted_command_environment.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts) · [`src/tooling/__tests__/verification_runtime_guidance.test.ts`](../src/tooling/__tests__/verification_runtime_guidance.test.ts) +- Core code: [`src/domain/branch_sync_command.ts`](../src/domain/branch_sync_command.ts) · [`src/application/ports/branch_sync_ports.ts`](../src/application/ports/branch_sync_ports.ts) · [`src/application/policies/branch_sync_message_catalog.ts`](../src/application/policies/branch_sync_message_catalog.ts) · [`src/application/policies/branch_sync_notification_policy.ts`](../src/application/policies/branch_sync_notification_policy.ts) · [`src/application/usecases/branch_sync/branch_sync_execution_policy.ts`](../src/application/usecases/branch_sync/branch_sync_execution_policy.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/application/usecases/steps/common/transition_notification_workflow.ts`](../src/application/usecases/steps/common/transition_notification_workflow.ts) · [`src/data/repository/branch_sync/branch_dependency_policy.ts`](../src/data/repository/branch_sync/branch_dependency_policy.ts) · [`src/data/repository/branch_sync/branch_dependency_repository.ts`](../src/data/repository/branch_sync/branch_dependency_repository.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/infrastructure/composition/push_single_action_capability_port_binding.ts`](../src/infrastructure/composition/push_single_action_capability_port_binding.ts) · [`src/infrastructure/branch_sync_workspace_adapter.ts`](../src/infrastructure/branch_sync_workspace_adapter.ts) · [`src/prompts/branch_sync_conflicts.ts`](../src/prompts/branch_sync_conflicts.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`src/infrastructure/branch_sync_sdd_catalog.ts`](../src/infrastructure/branch_sync_sdd_catalog.ts) · [`src/domain/branch_sync_validation.ts`](../src/domain/branch_sync_validation.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/infrastructure/branch_sync_communication_budget.ts`](../src/infrastructure/branch_sync_communication_budget.ts) · [`src/application/usecases/actions/retire_branch_sync_use_case.ts`](../src/application/usecases/actions/retire_branch_sync_use_case.ts) · [`src/application/ports/issue_completion_ports.ts`](../src/application/ports/issue_completion_ports.ts) · [`src/application/usecases/pull_request_workflow.ts`](../src/application/usecases/pull_request_workflow.ts) · [`src/application/usecases/pull_request_workflow_context.ts`](../src/application/usecases/pull_request_workflow_context.ts) · [`src/infrastructure/composition/pull_request_use_case_composition_root.ts`](../src/infrastructure/composition/pull_request_use_case_composition_root.ts) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) · [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`scripts/validate-workflow-contract.cjs`](../scripts/validate-workflow-contract.cjs) · [`src/application/ports/reply_request_read_ports.ts`](../src/application/ports/reply_request_read_ports.ts) · [`src/domain/reply_request_digest.ts`](../src/domain/reply_request_digest.ts) · [`src/infrastructure/composition/reply_request_composition_root.ts`](../src/infrastructure/composition/reply_request_composition_root.ts) · [`src/data/repository/reply_request_repository.ts`](../src/data/repository/reply_request_repository.ts) · [`src/application/policies/verification_runtime_policy.ts`](../src/application/policies/verification_runtime_policy.ts) · [`src/infrastructure/verification_runtime_manifest.ts`](../src/infrastructure/verification_runtime_manifest.ts) · [`src/infrastructure/verification_runtime_preflight.ts`](../src/infrastructure/verification_runtime_preflight.ts) · [`src/infrastructure/untrusted_command_environment.ts`](../src/infrastructure/untrusted_command_environment.ts) · [`src/application/ports/git_ports.ts`](../src/application/ports/git_ports.ts) · [`src/application/ports/bugbot_git_ports.ts`](../src/application/ports/bugbot_git_ports.ts) · [`src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts`](../src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts) · [`src/application/usecases/steps/commit/bugbot/verify_command_runner.ts`](../src/application/usecases/steps/commit/bugbot/verify_command_runner.ts) · [`src/domain/closed_issue_reconciliation.ts`](../src/domain/closed_issue_reconciliation.ts) · [`src/application/errors/closed_issue_reconciliation_error.ts`](../src/application/errors/closed_issue_reconciliation_error.ts) · [`src/application/policies/verification_setup_policy.ts`](../src/application/policies/verification_setup_policy.ts) · [`src/application/ports/verification_setup_ports.ts`](../src/application/ports/verification_setup_ports.ts) · [`src/infrastructure/verification_setup_snapshot.ts`](../src/infrastructure/verification_setup_snapshot.ts) · [`src/infrastructure/verification_setup_result.ts`](../src/infrastructure/verification_setup_result.ts) · [`src/infrastructure/verification_setup_process.ts`](../src/infrastructure/verification_setup_process.ts) · [`src/infrastructure/verification_setup_runner.ts`](../src/infrastructure/verification_setup_runner.ts) +- Tests: [`src/application/policies/__tests__/branch_sync_notification_policy.test.ts`](../src/application/policies/__tests__/branch_sync_notification_policy.test.ts) · [`src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts) · [`src/application/usecases/branch_sync/__tests__/sync_branch_use_case.test.ts`](../src/application/usecases/branch_sync/__tests__/sync_branch_use_case.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/application/usecases/steps/common/__tests__/transition_notification_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/transition_notification_workflow.test.ts) · [`src/data/repository/branch_sync/__tests__/branch_dependency_policy.test.ts`](../src/data/repository/branch_sync/__tests__/branch_dependency_policy.test.ts) · [`src/data/repository/branch_sync/__tests__/branch_dependency_repository.test.ts`](../src/data/repository/branch_sync/__tests__/branch_dependency_repository.test.ts) · [`src/infrastructure/__tests__/branch_sync_workspace_adapter.test.ts`](../src/infrastructure/__tests__/branch_sync_workspace_adapter.test.ts) · [`src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts) · [`src/application/usecases/branch_sync/__tests__/branch_sync_execution_policy.test.ts`](../src/application/usecases/branch_sync/__tests__/branch_sync_execution_policy.test.ts) · [`src/application/errors/__tests__/application_error.test.ts`](../src/application/errors/__tests__/application_error.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/infrastructure/__tests__/git_commit_adapter.test.ts`](../src/infrastructure/__tests__/git_commit_adapter.test.ts) · [`src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts`](../src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts) · [`src/infrastructure/__tests__/branch_sync_generated_bundle_validation.test.ts`](../src/infrastructure/__tests__/branch_sync_generated_bundle_validation.test.ts) · [`src/application/policies/__tests__/branch_sync_lifecycle_recovery.test.ts`](../src/application/policies/__tests__/branch_sync_lifecycle_recovery.test.ts) · [`src/infrastructure/__tests__/branch_sync_resolved_workspace.test.ts`](../src/infrastructure/__tests__/branch_sync_resolved_workspace.test.ts) · [`src/infrastructure/__tests__/branch_sync_communication_budget.test.ts`](../src/infrastructure/__tests__/branch_sync_communication_budget.test.ts) · [`src/application/usecases/actions/__tests__/retire_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/retire_branch_sync_use_case.test.ts) · [`src/application/usecases/__tests__/pull_request_use_case.test.ts`](../src/application/usecases/__tests__/pull_request_use_case.test.ts) · [`src/actions/__tests__/common_action.test.ts`](../src/actions/__tests__/common_action.test.ts) · [`src/actions/__tests__/github_action.test.ts`](../src/actions/__tests__/github_action.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) · [`src/data/repository/__tests__/reply_request_repository.test.ts`](../src/data/repository/__tests__/reply_request_repository.test.ts) · [`src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts`](../src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts) · [`src/application/policies/__tests__/verification_runtime_policy.test.ts`](../src/application/policies/__tests__/verification_runtime_policy.test.ts) · [`src/infrastructure/__tests__/verification_runtime_preflight.test.ts`](../src/infrastructure/__tests__/verification_runtime_preflight.test.ts) · [`src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts`](../src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts) · [`src/infrastructure/__tests__/untrusted_command_environment.test.ts`](../src/infrastructure/__tests__/untrusted_command_environment.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts) · [`src/tooling/__tests__/verification_runtime_guidance.test.ts`](../src/tooling/__tests__/verification_runtime_guidance.test.ts) · [`src/application/policies/__tests__/verification_setup_policy.test.ts`](../src/application/policies/__tests__/verification_setup_policy.test.ts) · [`src/infrastructure/__tests__/verification_setup_snapshot.test.ts`](../src/infrastructure/__tests__/verification_setup_snapshot.test.ts) · [`src/infrastructure/__tests__/verification_setup_runner.test.ts`](../src/infrastructure/__tests__/verification_setup_runner.test.ts) · [`src/infrastructure/__tests__/verification_setup_configuration.test.ts`](../src/infrastructure/__tests__/verification_setup_configuration.test.ts) · [`src/infrastructure/__tests__/verification_setup_sync.test.ts`](../src/infrastructure/__tests__/verification_setup_sync.test.ts) - User documentation: [`docs/issues/branch-synchronization.mdx`](../docs/issues/branch-synchronization.mdx) · [`docs/issues/comment-commands.mdx`](../docs/issues/comment-commands.mdx) · [`docs/issues/notifications-and-auto-close.mdx`](../docs/issues/notifications-and-auto-close.mdx) · [`docs/development/architecture.mdx`](../docs/development/architecture.mdx) · [`docs/security-operations/operations/verification.mdx`](../docs/security-operations/operations/verification.mdx) · [`docs/security-operations/operations/troubleshooting.mdx`](../docs/security-operations/operations/troubleshooting.mdx) · [`docs/agents/repository-collaboration.mdx`](../docs/agents/repository-collaboration.mdx) ### `pull-request-lifecycle` — Pull request lifecycle and enrichment diff --git a/specs/branch-synchronization-and-conflict-recovery.md b/specs/branch-synchronization-and-conflict-recovery.md index 59eff22fb..a40fb096c 100644 --- a/specs/branch-synchronization-and-conflict-recovery.md +++ b/specs/branch-synchronization-and-conflict-recovery.md @@ -719,3 +719,7 @@ retained effects and named failures for incomplete cleanup. Each card uses at most two targeted witness reads. This does not widen synchronization, branch, comment creation or deletion authority. Its eight new race/readback and bounded-diagnostic cases are counted only in the lifecycle extension's budget. + +## Repository setup amendment — issue #647 + +The [repository-owned setup contract](./repository-verification-setup-hooks.md) prepares the actual temporary merge immediately before each verification command when explicitly enabled. Composition captures immutable helper bytes from trusted main before workspace mutation; candidate edits never replace them. Setup and runtime preflight share the command's private environment. Setup failure follows existing abort behavior and withholds commit/push. Read-only status, dry run, aligned branches and empty command lists never execute the helper. The companion owns its incremental budget and local same-managed-ref acceptance; no release is included. diff --git a/specs/catalog.json b/specs/catalog.json index 05cff3d73..ed96b6e90 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -930,7 +930,8 @@ "src/application/ports/git_ports.ts", "src/application/ports/bugbot_git_ports.ts", "src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts", - "src/application/usecases/steps/commit/bugbot/verify_command_runner.ts" + "src/application/usecases/steps/commit/bugbot/verify_command_runner.ts", + "src/cli/setup_config_file.ts" ], "tests": [ "src/data/repository/__tests__/repository_version_tags_query_repository.test.ts", @@ -1060,7 +1061,8 @@ "src/infrastructure/__tests__/untrusted_command_environment.test.ts", "src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts", "src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts", - "src/tooling/__tests__/verification_runtime_guidance.test.ts" + "src/tooling/__tests__/verification_runtime_guidance.test.ts", + "src/infrastructure/__tests__/verification_setup_configuration.test.ts" ], "documentation": [ "README.md", @@ -1934,7 +1936,7 @@ "status": "implemented", "scope": "Observe parent drift with one localized status card and transition-only notifications, then safely merge a parent branch into a linked working branch", "owner": "Copilot maintainers", - "lastVerified": "2026-10-20", + "lastVerified": "2026-10-21", "specs": [ "specs/branch-synchronization-and-conflict-recovery.md", "specs/repository-verification-runtime-prerequisites.md", @@ -1995,7 +1997,13 @@ "src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts", "src/application/usecases/steps/commit/bugbot/verify_command_runner.ts", "src/domain/closed_issue_reconciliation.ts", - "src/application/errors/closed_issue_reconciliation_error.ts" + "src/application/errors/closed_issue_reconciliation_error.ts", + "src/application/policies/verification_setup_policy.ts", + "src/application/ports/verification_setup_ports.ts", + "src/infrastructure/verification_setup_snapshot.ts", + "src/infrastructure/verification_setup_result.ts", + "src/infrastructure/verification_setup_process.ts", + "src/infrastructure/verification_setup_runner.ts" ], "tests": [ "src/application/policies/__tests__/branch_sync_notification_policy.test.ts", @@ -2029,7 +2037,12 @@ "src/infrastructure/__tests__/untrusted_command_environment.test.ts", "src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts", "src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts", - "src/tooling/__tests__/verification_runtime_guidance.test.ts" + "src/tooling/__tests__/verification_runtime_guidance.test.ts", + "src/application/policies/__tests__/verification_setup_policy.test.ts", + "src/infrastructure/__tests__/verification_setup_snapshot.test.ts", + "src/infrastructure/__tests__/verification_setup_runner.test.ts", + "src/infrastructure/__tests__/verification_setup_configuration.test.ts", + "src/infrastructure/__tests__/verification_setup_sync.test.ts" ], "documentation": [ "docs/issues/branch-synchronization.mdx", diff --git a/specs/repository-verification-runtime-prerequisites.md b/specs/repository-verification-runtime-prerequisites.md index 1d3b45837..46f96ce6a 100644 --- a/specs/repository-verification-runtime-prerequisites.md +++ b/specs/repository-verification-runtime-prerequisites.md @@ -317,3 +317,7 @@ this change adds no stateful retry or remote race boundary. inference, credential broadening and parsing untrusted failed-command output. - Outside scope: live consumer Kotlin/Gradle retest, other home/cache variables, automatic setup questionnaire requirements, automatic dependency provisioning. + +## Optional repository setup amendment — issue #647 + +The [repository-owned setup contract](./repository-verification-setup-hooks.md) adds an explicit disabled-default `verification-setup` boolean. When enabled, a trusted main-branch Node helper prepares each candidate command environment before these existing fixed runtime probes. The runtime declaration remains a version-only policy and never supplies script paths, commands, credentials or installer arguments. With setup disabled, prerequisite behavior is unchanged. The companion owns its incremental tests and bounded setup limits; all existing no-push, isolation and preflight constraints remain required. diff --git a/specs/repository-verification-setup-hooks.md b/specs/repository-verification-setup-hooks.md index 76116edc5..2f7ff5416 100644 --- a/specs/repository-verification-setup-hooks.md +++ b/specs/repository-verification-setup-hooks.md @@ -1,9 +1,9 @@ # Repository-Owned Verification Setup Hooks -- Status: Draft +- Status: Implementation candidate - Date: 2026-10-21 - Catalog capability ID: branch-synchronization -- Last verified: not applicable (prospective specification) +- Last verified: 2026-10-21 - Owners: Copilot maintainers - Scope: Define an optional repository-owned preparation helper for authorized operations that run repository verification, including branch synchronization and automated fixes. - Related issues/PRs: [Issue #647](https://github.com/vypdev/copilot/issues/647); [verification runtime prerequisites](https://github.com/vypdev/copilot/blob/master/docs/security-operations/operations/verification.mdx#repository-verification-prerequisites) @@ -12,7 +12,7 @@ ## 1. Executive summary -Consumers may add a reviewed helper at `.github/copilot/verification-setup` on their trusted main/production branch. Copilot captures an immutable snapshot and invokes it in the credential-free environment immediately before each configured verification command, using the candidate tree being checked. The helper prepares only that repository’s required runtimes and dependencies. Without an enabled helper, existing prerequisite checks and verification behavior remain in force. Any enabled-helper validation, execution, timeout, or result failure blocks verification and commit/push. +Consumers may enable `verification-setup: true` and add a reviewed helper at `.copilot/verification-setup.mjs` on their trusted main/production branch. Copilot captures an immutable snapshot and invokes it in the credential-free environment immediately before each configured verification command, using the candidate tree being checked. The helper prepares only that repository’s required runtimes and dependencies. Without an enabled helper, existing prerequisite checks and verification behavior remain in force. Any enabled-helper validation, execution, timeout, or result failure blocks verification and commit/push. ```text authorized operation -> candidate tree ready -> trusted helper snapshot prepares isolated verifier -> preflight and verification -> existing commit/push policy @@ -135,12 +135,19 @@ Duplicate events follow existing route idempotency. A new candidate or command i | Input | Type | Recommended default | Allowed values/range | Scope/persistence | |---|---|---|---|---| -| `.github/copilot/verification-setup` | Optional repository file | Absent/disabled | Regular executable helper in the trusted branch snapshot; fixed path and invocation contract | Repository-owned; snapshot SHA/bytes per admitted operation | +| `verification-setup` / `VERIFICATION_SETUP` / `repository.verificationSetup` | Boolean | `false` | Only true/false; no arbitrary helper paths or commands | Action input / Repository Variable / setup config | +| `.copilot/verification-setup.mjs` | Optional repository file | Absent/disabled | Standalone regular Node ESM file of at most 64 KiB in the trusted branch snapshot; Node argv contains only its private snapshot path | Repository-owned; snapshot SHA/bytes per admitted operation | | Helper timeout | Fixed duration | 300 seconds | Not configurable | Per invocation | | Captured output ceiling | Fixed limit | 64 KiB | Not configurable | Per invocation | | Result file ceiling | Fixed limit | 16 KiB | Not configurable | Per invocation | -The helper contract is a bounded process invocation with candidate cwd and a narrow documented input/output interface. It MAY install tools and dependencies needed by that candidate, using declared runtime versions and lockfiles. It MUST NOT require shell interpolation. Node execution uses argument arrays. Provision pnpm explicitly; implicit Corepack downloads are not a supported prerequisite. Helper-owned workspace caches must validate lockfiles. Private-profile tools may be recreated for each command. +The helper is standalone Node ESM, invoked with the trusted Action Node executable, `shell: false` and no user arguments. Its cwd is the exact candidate workspace. The private invocation environment adds only `COPILOT_VERIFICATION_SETUP_PROTOCOL=1`, `COPILOT_VERIFICATION_SETUP_SOURCE_SHA` and `COPILOT_VERIFICATION_SETUP_RESULT` (a private output file path). These metadata variables are not forwarded to verification commands. Relative module imports resolve in the private snapshot directory; use Node built-ins and explicit candidate-cwd subprocesses instead of sibling imports. + +The result file is optional. When present it is one regular, nonlinked UTF-8 JSON file, at most 16 KiB, with numeric `schemaVersion: 1`, optional `path` (at most eight unique absolute directories of at most 2,048 characters each) and optional `environment` containing only `JAVA_HOME`. Null values, unknown keys, non-directories, traversal, control characters, path delimiter injection and escaping symlinks are rejected. Approved roots are the canonical candidate workspace, private HOME and captured trusted `RUNNER_TOOL_CACHE`. PATH additions precede the existing PATH; JAVA_HOME is the only environment override. No result means no overlay. There is no stdout policy channel. + +Trusted acquisition uses local Git reads of `refs/remotes/origin/` at admission, regular Git blob mode and verified object digest/UTF-8 bytes. A full checkout must have the configured main ref available; no credentialed fetch occurs during acquisition. Acquisition failures are retained as closed facts and raised only if verification runs. Result reads are bounded and compare exact BigInt filesystem identities. Each setup invocation has a fresh profile; success logs only the fixed path and pinned SHA. Existing localized operation shells own presentation; the technical diagnostic is the same English static fallback used by runtime prerequisites. + +The helper MAY install tools and dependencies needed by that candidate, using declared runtime versions and lockfiles. It MUST NOT require shell interpolation. Node execution uses argument arrays. Provision pnpm explicitly; implicit Corepack downloads are not a supported prerequisite. Helper-owned workspace caches must validate lockfiles. Private-profile tools may be recreated for each command. Only approved output paths are accepted: existing directories contained in the workspace, private profile, or trusted tool root. Reject unknown environment fields, unsafe links, and paths. Invalid cross-field/output combinations fail closed. Helper stdout/stderr is never emitted raw; diagnostics expose bounded stage, cause, pinned path/SHA and exit/status facts only. @@ -153,7 +160,7 @@ Recommended setup: use the repository’s declared Python version and lockfile t | Layer/boundary | Owns | Must not own/import | |---|---|---| | Domain/pure policy | Whether verification is requested; fixed limits and eligibility rules | GitHub SDK, process APIs | -| Application | Snapshot lifecycle and per-command setup/verify ordering | Concrete process/provider DTOs | +| Application | Immutable snapshot contract and existing verification intent/order via the Git port | Concrete process/provider DTOs | | Adapters/data | Trusted branch file retrieval and immutable bytes/SHA mapping | Authorization or product policy | | Infrastructure/composition | Isolated environment creation, process execution, cleanup and route wiring | Decisions about whether a route is authorized | | Entrypoints | Convert issue/PR/comment/review/commit/sync events to use-case input | Duplicated verification orchestration | @@ -198,13 +205,13 @@ Blocked before irreversible effects: > **Current status:** Repository setup failed before verification. > -> **Action required:** Review `.github/copilot/verification-setup` on the trusted branch and rerun the operation. +> **Action required:** Review `.copilot/verification-setup.mjs` on the trusted branch and rerun the operation. ## Progress - [x] Candidate workspace prepared -- [ ] Project verification not run -- [ ] Commit and push withheld +- [x] Project verification not run +- [x] Commit and push withheld ## What happens next @@ -261,7 +268,7 @@ Errors follow impact -> cause -> action -> retained state. No automatic retry is - Existing in-flight operations retain their captured policy and do not acquire a helper mid-operation. - No schema migration is required. Existing repositories without a helper retain compatible behavior and prerequisite checks. -- The helper is opt-in at its fixed path; invalid enabled configuration fails closed. Generated workflow updates must preserve the consumer-owned helper. +- The helper requires the explicit boolean opt-in and its fixed path; invalid enabled configuration fails closed. Generated workflow updates must preserve the consumer-owned helper. - Roll out through setup CLI/template wiring and documentation, then verify controlled local same-managed-branch synchronization. Do not launch a release as part of this work. - Rollback removes hook invocation while preserving existing verification policy. Never undo a completed external commit/push as part of rollback; this design blocks those effects on failure. @@ -321,7 +328,7 @@ Document credential boundaries, update preservation, migration/rollback, and the 1. Define helper interface, fixed path, snapshot/result contracts and architecture checks. 2. Implement pure eligibility, limit and output-validation policies with focused tests. -3. Add application snapshot lifecycle and per-command setup/verification orchestration. +3. Add composition snapshot lifecycle and the existing semantic verification port. 4. Add trusted-file and isolated-process adapters with credential cleanup and bounded execution. 5. Wire sync and shared commit-verification boundaries across source and distributed templates and setup CLI planning. 6. Add sanitized status, fixtures, documentation, troubleshooting, and update-preservation behavior. @@ -329,21 +336,43 @@ Document credential boundaries, update preservation, migration/rollback, and the ## 19. Definition of Done -- [ ] Every normative requirement maps to acceptance and traceability. -- [ ] Helper path, invocation, limits, trusted SHA and output contract are implemented and validated. -- [ ] All actual verification routes use the shared hook; read-only/no-op/dry-run paths do not. -- [ ] Snapshot, candidate freshness, credentials, unsafe paths, timeout, output bounds and fail-closed commit gate pass. -- [ ] Numeric test budget of at least 36 distinct cases and coverage requirements pass. -- [ ] UI states, sanitized diagnostics, navigation, accessibility and localization are reviewed. -- [ ] User, setup-owner, operator and contributor documentation is discoverable and examples match fixtures. -- [ ] Setup/update preserves consumer helper contents; generated workflow contract checks pass. -- [ ] Controlled same-managed-branch sync acceptance passes without manual workflow edits; no release is launched. -- [ ] `specs/catalog.json` evidence and `specs/CATALOG.md` are current and `pnpm run validate:specifications` passes. -- [ ] No readiness-blocking decision remains unresolved. +- [x] Every normative requirement maps to acceptance and traceability. +- [x] Helper path, invocation, limits, trusted SHA and output contract are implemented and validated. +- [x] All actual verification routes use the shared hook; read-only/no-op/dry-run paths do not. +- [x] Snapshot, candidate freshness, credentials, unsafe paths, timeout, output bounds and fail-closed commit gate pass. +- [x] Numeric test budget of at least 36 distinct cases and coverage requirements pass. +- [x] UI states, sanitized diagnostics, navigation, accessibility and localization are reviewed. +- [x] User, setup-owner, operator and contributor documentation is discoverable and examples match fixtures. +- [x] Setup/update preserves consumer helper contents; generated workflow contract checks pass. +- [x] Controlled same-managed-branch sync acceptance passes without manual workflow edits; no release is launched. +- [x] `specs/catalog.json` evidence and `specs/CATALOG.md` are current and `pnpm run validate:specifications` passes. +- [x] No readiness-blocking decision remains unresolved. ## 20. References and decisions - Primary sources: [Issue #647](https://github.com/vypdev/copilot/issues/647); [repository verification prerequisites](https://github.com/vypdev/copilot/blob/master/docs/security-operations/operations/verification.mdx#repository-verification-prerequisites); [consumer issue #19](https://github.com/vypdev/homeassistant-gateway/issues/19); [failed run](https://github.com/vypdev/homeassistant-gateway/actions/runs/39570398824). - Related specs/ADRs/issues/PRs: owning specifications are `specs/branch-synchronization-and-conflict-recovery.md` and `specs/repository-verification-runtime-prerequisites.md`; this companion is `specs/repository-verification-setup-hooks.md` and does not replace either owner. - Decisions and rejected alternatives: fixed optional repository helper and shared managed invocation boundary; rejected editing generated workflows per consumer, global dependency installation, separate preparation jobs, weakening verification, automatic retries, and contributor-created helper activation. -- Follow-up work explicitly outside this spec: implementation admission, Action-managed branch, PR review, release, and any release operation. \ No newline at end of file +- Follow-up work explicitly outside this spec: implementation admission, Action-managed branch, PR review, release, and any release operation. +## Implementation evidence — issue #647 + +The two executable verification boundaries are `SyncBranchUseCase.verifyPreparedMerge` and the shared automated commit preflight. General issue/PR comments, natural-language modification, `/copilot implement`, Bugbot autofix, remember-branch changes and review-comment fixes reach those boundaries through the two comment composition roots. Issue lifecycle, PR review, commit analysis and review-state events do not currently run repository verification themselves. The lightweight push sync observer only reads status. All six source/distributed event workflows carry the disabled-default input, so forwarding does not silently activate setup. Existing actor, native-head, workspace and fork admission precedes any verification request; the snapshot itself is read-only and can be captured before admission without executing it. + +New files: pure `verification_setup_policy`, semantic `verification_setup_ports`, infrastructure snapshot/process/result/runner adapters. `GitCommitAdapter` runs setup, existing prerequisites and the command in one fresh environment whenever the existing `untrusted + verifyRuntimePrerequisites` intent is present. Trusted commits and Git reads never invoke setup. Agent runtime cleanup still completes before agent results return to verification. Setup never owns or rewrites the helper. + +Executable evidence is in `verification_setup_policy.test.ts`, `verification_setup_snapshot.test.ts`, `verification_setup_runner.test.ts`, `verification_setup_configuration.test.ts`, `verification_setup_sync.test.ts`, composition tests and the structural workflow validator. The real local Git fixture first fails unprepared, aborts without changing its remote managed ref, then prepares the merged lockfile twice and pushes the verified merge to exactly that same ref. Its candidate helper deliberately throws; only the captured main helper runs. Other rows cover dry run, aligned state, empty commands, setup failure, byte provenance, credential exclusion, limits, cancellation, paths, update preservation and legacy configuration. No public consumer run or release is claimed. + +### Controlled local acceptance (2026-10-21) + +Both cases ran through the real `SyncBranchUseCase`, Git adapter, workspace adapter and trusted snapshot against temporary local bare remotes and `feature/42-controlled-managed`. Neither case changed generated workflows or used an agent/network during setup. The first attempt, without setup, failed verification, aborted and retained the remote head. The second attempt prepared the temporary merge, passed the configured command and pushed only the same managed ref with a clean tracked workspace. + +| Fixture | Actual toolchain/command | Dependency preparation | Unprepared | Prepared | +|---|---|---|---|---| +| Python | Python 3.14.6; `python3 -m pytest -q -p no:cacheprovider` | Fresh private venv; offline wheelhouse, all five requirements version/hash locked, pytest 8.4.2 | Blocked, no push | `merged-cleanly`, one verification | +| Node/pnpm | Node 24.20.0; explicitly provisioned pnpm 10.12.4; `pnpm test` | Offline frozen install and candidate-manifest preparation | Blocked, no push | `merged-cleanly`, one verification | + +The Python wheelhouse was downloaded before the credential-free test from public packages: pytest 8.4.2, pluggy 1.7.0, iniconfig 2.3.1, packaging 26.3 and pygments 2.21.0. Every retained wheel was SHA-256 hashed into the fixture lockfile and installed with `--no-index --require-hashes`. No global dependencies, real consumer branches, public remote refs, tags or releases were modified. This is local controlled acceptance, not a claim that the consumer incident run has been retried. + +### Incremental executable inventory + +The implementation adds 102 distinct rows in five focused files (27 pure policy, 21 snapshot, 34 runner/result, 14 configuration/workflow, six real-Git sync), two composition admission-order rows and one structural workflow mutation row: **105 new automated cases**, exceeding the 36-case risk minimum above. Existing generated-link assertions are updated without counting them again. The two real Python/pnpm controlled acceptance runs are separate manual evidence, not additional automated budget claims. The new executable coverage budget retains 95% lines/statements and 90% branches/functions for the pure policy and aggregate adapters; the pure policy reached 100% of all metrics in focused coverage. diff --git a/src/actions/github_action_execution.ts b/src/actions/github_action_execution.ts index 003e6cfb7..333506181 100644 --- a/src/actions/github_action_execution.ts +++ b/src/actions/github_action_execution.ts @@ -86,6 +86,7 @@ export async function buildGithubActionExecution( eventInputs, ), preBranchSdd: parseIssueWorkflowBoolean(getInput(INPUT_KEYS.PRE_BRANCH_SDD), INPUT_KEYS.PRE_BRANCH_SDD, false), + verificationSetup: parseIssueWorkflowBoolean(getInput(INPUT_KEYS.VERIFICATION_SETUP), INPUT_KEYS.VERIFICATION_SETUP, false), pullRequest: buildPullRequest( parseIntegerInput(getInput(INPUT_KEYS.PULL_REQUEST_DESIRED_ASSIGNEES_COUNT), 0), parseIntegerInput(getInput(INPUT_KEYS.PULL_REQUEST_DESIRED_REVIEWERS_COUNT), 0), diff --git a/src/actions/local_action_configuration_sections.ts b/src/actions/local_action_configuration_sections.ts index 40c614b98..03960c691 100644 --- a/src/actions/local_action_configuration_sections.ts +++ b/src/actions/local_action_configuration_sections.ts @@ -185,6 +185,7 @@ export function readLocalWorkflowConfiguration( commitPrefixBuilder: read(INPUT_KEYS.COMMIT_PREFIX_TRANSFORMS) || 'replace-slash', issueManagedBranches: parseIssueWorkflowBoolean(read(INPUT_KEYS.ISSUE_MANAGED_BRANCHES), INPUT_KEYS.ISSUE_MANAGED_BRANCHES, true), preBranchSdd: parseIssueWorkflowBoolean(read(INPUT_KEYS.PRE_BRANCH_SDD), INPUT_KEYS.PRE_BRANCH_SDD, false), + verificationSetup: parseIssueWorkflowBoolean(read(INPUT_KEYS.VERIFICATION_SETUP), INPUT_KEYS.VERIFICATION_SETUP, false), reopenIssueOnPush: isEnabledInput(read(INPUT_KEYS.REOPEN_ISSUE_ON_PUSH)), issueDesiredAssigneesCount: parseIntegerInput(read(INPUT_KEYS.DESIRED_ASSIGNEES_COUNT), 0), pullRequestDesiredAssigneesCount: parseIntegerInput(read(INPUT_KEYS.PULL_REQUEST_DESIRED_ASSIGNEES_COUNT), 0), diff --git a/src/actions/local_action_execution.ts b/src/actions/local_action_execution.ts index 5cc94c60d..0300b59e2 100644 --- a/src/actions/local_action_execution.ts +++ b/src/actions/local_action_execution.ts @@ -17,7 +17,7 @@ export function buildLocalActionExecution( debug, singleAction, singleActionIssue, singleActionVersion, singleActionTitle, singleActionChangelog, singleActionMessage, singleActionCommentId, singleActionCommentMode, singleActionOperationId, inactivityThresholdHours, - commitPrefixBuilder, issueManagedBranches, preBranchSdd, reopenIssueOnPush, issueDesiredAssigneesCount, + commitPrefixBuilder, issueManagedBranches, preBranchSdd, verificationSetup, reopenIssueOnPush, issueDesiredAssigneesCount, pullRequestDesiredAssigneesCount, pullRequestDesiredReviewersCount, titleEmoji, branchManagementEmoji, token, agentModel, aiPullRequestDescriptionMode, aiMembersOnly, aiIgnoreFiles, aiIncludeReasoning, bugbotSeverity, @@ -58,6 +58,7 @@ export function buildLocalActionExecution( commitPrefixBuilder, issue: buildIssue(issueManagedBranches, reopenIssueOnPush, issueDesiredAssigneesCount, additionalParams), preBranchSdd, + verificationSetup, pullRequest: buildPullRequest(pullRequestDesiredAssigneesCount, pullRequestDesiredReviewersCount, additionalParams), emoji: buildEmoji(titleEmoji, branchManagementEmoji), tokens: buildTokens(token), diff --git a/src/application/contracts/input_keys.ts b/src/application/contracts/input_keys.ts index 9debb36ff..2a6725222 100644 --- a/src/application/contracts/input_keys.ts +++ b/src/application/contracts/input_keys.ts @@ -219,6 +219,7 @@ export const INPUT_KEYS = { // Issue ISSUE_MANAGED_BRANCHES: 'issue-managed-branches', PRE_BRANCH_SDD: 'pre-branch-sdd', + VERIFICATION_SETUP: 'verification-setup', REOPEN_ISSUE_ON_PUSH: 'reopen-issue-on-push', DESIRED_ASSIGNEES_COUNT: 'desired-assignees-count', diff --git a/src/application/policies/__tests__/verification_setup_policy.test.ts b/src/application/policies/__tests__/verification_setup_policy.test.ts new file mode 100644 index 000000000..1e0c15df1 --- /dev/null +++ b/src/application/policies/__tests__/verification_setup_policy.test.ts @@ -0,0 +1,39 @@ +import { parseVerificationSetupResult, VerificationSetupError, VERIFICATION_SETUP_LIMITS, VERIFICATION_SETUP_PATH } from '../verification_setup_policy'; + +describe('repository verification setup policy', () => { + it('fixes the helper and limits without caller overrides', () => { + expect(VERIFICATION_SETUP_PATH).toBe('.copilot/verification-setup.mjs'); + expect(VERIFICATION_SETUP_LIMITS).toEqual({ timeoutMs: 300_000, outputBytes: 65_536, resultBytes: 16_384, scriptBytes: 65_536 }); + expect(Object.isFrozen(VERIFICATION_SETUP_LIMITS)).toBe(true); + }); + it.each([ + [{ schemaVersion: 1 }, { path: [] }], + [{ schemaVersion: 1, path: ['/tools/bin'] }, { path: ['/tools/bin'] }], + [{ schemaVersion: 1, environment: { JAVA_HOME: '/tools/java' } }, { path: [], javaHome: '/tools/java' }], + ])('accepts closed protocol v1 output %j', (input, expected) => { + const parsed = parseVerificationSetupResult(input); + expect(parsed).toEqual(expected); + expect(Object.isFrozen(parsed)).toBe(true); + expect(Object.isFrozen(parsed.path)).toBe(true); + }); + it.each([ + null, [], false, 'value', {}, { schemaVersion: 2 }, { schemaVersion: 1, timeout: 1 }, + { schemaVersion: 1, path: 'bin' }, { schemaVersion: 1, path: null }, { schemaVersion: 1, environment: null }, { schemaVersion: 1, path: Array(9).fill('/bin') }, + { schemaVersion: 1, path: ['/bin', '/bin'] }, { schemaVersion: 1, path: [''] }, + { schemaVersion: 1, path: [false] }, { schemaVersion: 1, path: ['a'.repeat(2049)] }, + { schemaVersion: 1, path: ['line\nbreak'] }, { schemaVersion: 1, environment: [] }, + { schemaVersion: 1, environment: 'env' }, { schemaVersion: 1, environment: { HOME: '/tmp' } }, + { schemaVersion: 1, environment: { NODE_OPTIONS: '--require candidate' } }, + { schemaVersion: 1, environment: { JAVA_HOME: '' } }, { schemaVersion: 1, environment: { JAVA_HOME: 3 } }, + ])('rejects invalid or unrestricted output %j', input => { + expect(() => parseVerificationSetupResult(input)).toThrow(expect.objectContaining({ reason: 'result-invalid' })); + }); + it('reports only fixed facts and safe pinned source/exit metadata', () => { + expect(new VerificationSetupError('timeout', 'a'.repeat(40), 7).message).toContain('Exit status: 7'); + const error = new VerificationSetupError('source-unavailable', 'secret\n@everyone', 999); + expect(error.message).toContain('Trusted source SHA: unavailable'); + expect(error.message).toContain('Verification, commit and push are withheld'); + expect(error.message).not.toMatch(/secret|@everyone|999/); + expect(error).not.toHaveProperty('cause'); + }); +}); diff --git a/src/application/policies/repository_agent_guidance_policy.ts b/src/application/policies/repository_agent_guidance_policy.ts index 1fb70f2f6..961ca1ed1 100644 --- a/src/application/policies/repository_agent_guidance_policy.ts +++ b/src/application/policies/repository_agent_guidance_policy.ts @@ -200,6 +200,8 @@ The setup runner profile selects placement; it does not install the project's la Declare reviewed version prefixes in \`.copilot/verification-prerequisites.json\` (schemaVersion 1, runtimes such as java 17, node 24, pnpm 10). Copilot checks declared versions in the credential-free verification environment before each configured verification command; a missing or incompatible runtime blocks publication. Without this optional declaration, existing verification behavior remains and no language is inferred. Java provisioning may use \`actions/setup-java\`; \`JAVA_HOME\` and \`PATH\` survive isolation, while agent credentials and tool-option injection variables do not. Do not add an unreviewed bootstrap or weaken verification to work around a missing runtime. Read [verification prerequisites and same-job setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites). +The optional repository-owned setup helper prepares each candidate before verification without editing generated workflows. Review \`.copilot/verification-setup.mjs\` onto trusted main before enabling \`VERIFICATION_SETUP=true\` (Action input \`verification-setup\`, disabled by default). Copilot executes captured trusted bytes before each command in a fresh credential-free profile; candidate changes cannot replace them. Setup failure blocks verification and commit/push. Read [repository-owned setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup). + For the full contributor flow, read [repository collaboration](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-collaboration.mdx). For drift and recovery, read [guidance troubleshooting](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-guidance-troubleshooting.mdx). `; } diff --git a/src/application/policies/setup_configuration_defaults.ts b/src/application/policies/setup_configuration_defaults.ts index 1da4832ba..44064ec94 100644 --- a/src/application/policies/setup_configuration_defaults.ts +++ b/src/application/policies/setup_configuration_defaults.ts @@ -103,6 +103,7 @@ export function createDefaultSetupConfiguration(): SetupConfiguration { choreTree: 'chore', issueManagedBranches: true, preBranchSdd: false, + verificationSetup: false, reopenIssueOnPush: true, desiredAssigneesCount: 1, desiredReviewersCount: 1, diff --git a/src/application/policies/setup_configuration_plan.ts b/src/application/policies/setup_configuration_plan.ts index fe5c50f48..1a771cdcb 100644 --- a/src/application/policies/setup_configuration_plan.ts +++ b/src/application/policies/setup_configuration_plan.ts @@ -140,6 +140,7 @@ export function buildSetupRepositoryVariables(configuration: SetupConfiguration) add('CHORE_TREE', repository.choreTree); add('ISSUE_MANAGED_BRANCHES', repository.issueManagedBranches); add('PRE_BRANCH_SDD', repository.preBranchSdd); + add('VERIFICATION_SETUP', repository.verificationSetup ?? false); add('REOPEN_ISSUE_ON_PUSH', repository.reopenIssueOnPush); add('DESIRED_ASSIGNEES_COUNT', repository.desiredAssigneesCount); add('DESIRED_REVIEWERS_COUNT', repository.desiredReviewersCount); @@ -207,6 +208,7 @@ export function buildSetupActionInputs(configuration: SetupConfiguration): Recor 'chore-tree': repository.choreTree, 'issue-managed-branches': String(repository.issueManagedBranches), 'pre-branch-sdd': String(repository.preBranchSdd), + 'verification-setup': String(repository.verificationSetup ?? false), 'reopen-issue-on-push': String(repository.reopenIssueOnPush), 'desired-assignees-count': String(repository.desiredAssigneesCount), 'desired-reviewers-count': String(repository.desiredReviewersCount), diff --git a/src/application/policies/setup_configuration_validation.ts b/src/application/policies/setup_configuration_validation.ts index 6f2162dc2..76e734a45 100644 --- a/src/application/policies/setup_configuration_validation.ts +++ b/src/application/policies/setup_configuration_validation.ts @@ -42,6 +42,9 @@ export function validateSetupConfiguration(configuration: SetupConfiguration, op || typeof configuration.repository.preBranchSdd !== 'boolean') { errors.push('issue-managed-branches and pre-branch-sdd must be boolean values.'); } + if (configuration.repository.verificationSetup !== undefined && typeof configuration.repository.verificationSetup !== 'boolean') { + errors.push('verification-setup must be a boolean value.'); + } if (configuration.repository.preBranchSdd && !configuration.repository.issueManagedBranches) { errors.push('pre-branch-sdd requires issue-managed-branches.'); } diff --git a/src/application/policies/setup_organization_variable_bindings.ts b/src/application/policies/setup_organization_variable_bindings.ts index ce9483b2f..f89fe7836 100644 --- a/src/application/policies/setup_organization_variable_bindings.ts +++ b/src/application/policies/setup_organization_variable_bindings.ts @@ -14,6 +14,7 @@ const repositoryFields = [ ['HOTFIX_TREE', 'hotfixTree'], ['RELEASE_TREE', 'releaseTree'], ['DOCS_TREE', 'docsTree'], ['CHORE_TREE', 'choreTree'], ['ISSUE_MANAGED_BRANCHES', 'issueManagedBranches'], ['PRE_BRANCH_SDD', 'preBranchSdd'], + ['VERIFICATION_SETUP', 'verificationSetup'], ['REOPEN_ISSUE_ON_PUSH', 'reopenIssueOnPush'], ['DESIRED_ASSIGNEES_COUNT', 'desiredAssigneesCount'], ['DESIRED_REVIEWERS_COUNT', 'desiredReviewersCount'], ['INACTIVITY_THRESHOLD_HOURS', 'inactivityThresholdHours'], ['REPOSITORY_LOCALE', 'repositoryLocale'], ['ISSUES_LOCALE', 'issueLocale'], diff --git a/src/application/policies/verification_setup_policy.ts b/src/application/policies/verification_setup_policy.ts new file mode 100644 index 000000000..7058ef4aa --- /dev/null +++ b/src/application/policies/verification_setup_policy.ts @@ -0,0 +1,44 @@ +export const VERIFICATION_SETUP_PATH = '.copilot/verification-setup.mjs'; +export const VERIFICATION_SETUP_LIMITS = Object.freeze({ timeoutMs: 300_000, outputBytes: 65_536, resultBytes: 16_384, scriptBytes: 65_536 }); +export const VERIFICATION_SETUP_HELP = 'https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup'; + +export type VerificationSetupFailure = 'source-unavailable' | 'helper-invalid' | 'workspace-changed' | 'process-failed' | 'timeout' | 'cancelled' | 'output-limit' | 'result-invalid'; + +/** Closed facts only. Provider/helper output and raw exception causes never cross this boundary. */ +export class VerificationSetupError extends Error { + constructor(readonly reason: VerificationSetupFailure, sourceSha?: string, readonly exitCode?: number) { + const sha = sourceSha && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(sourceSha) ? sourceSha : 'unavailable'; + const exit = Number.isInteger(exitCode) && exitCode! >= 0 && exitCode! <= 255 ? ` Exit status: ${exitCode}.` : ''; + super(`Repository setup blocked verification (${reason}). Review ${VERIFICATION_SETUP_PATH} on the trusted main branch and rerun. Trusted source SHA: ${sha}.${exit} Verification, commit and push are withheld. Help: ${VERIFICATION_SETUP_HELP}`); + this.name = 'VerificationSetupError'; + } +} + +export interface VerificationSetupResult { + readonly path: readonly string[]; + readonly javaHome?: string; +} + +function record(value: unknown): value is Record { + return !!value && typeof value === 'object' && !Array.isArray(value); +} + +function pathString(value: unknown): value is string { + return typeof value === 'string' && value.length > 0 && value.length <= 2048 + && ![...value].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127); +} + +/** Filesystem containment is checked by the adapter, after this closed schema is parsed. */ +export function parseVerificationSetupResult(value: unknown): VerificationSetupResult { + if (!record(value) || value.schemaVersion !== 1 || Object.keys(value).some(key => !['schemaVersion', 'path', 'environment'].includes(key))) { + throw new VerificationSetupError('result-invalid'); + } + const paths = value.path === undefined ? [] : value.path; + const environment = value.environment === undefined ? {} : value.environment; + if (!Array.isArray(paths) || paths.length > 8 || !paths.every(pathString) || new Set(paths).size !== paths.length + || !record(environment) || Object.keys(environment).some(key => key !== 'JAVA_HOME') + || ('JAVA_HOME' in environment && !pathString(environment.JAVA_HOME))) { + throw new VerificationSetupError('result-invalid'); + } + return Object.freeze({ path: Object.freeze([...paths]), ...(environment.JAVA_HOME ? { javaHome: environment.JAVA_HOME as string } : {}) }); +} diff --git a/src/application/ports/verification_setup_ports.ts b/src/application/ports/verification_setup_ports.ts new file mode 100644 index 000000000..d643f6453 --- /dev/null +++ b/src/application/ports/verification_setup_ports.ts @@ -0,0 +1,9 @@ +import type { VerificationSetupFailure } from '../policies/verification_setup_policy'; + +/** An operation-private immutable snapshot, captured before candidate mutation. No credentials. */ +export type PreparedVerificationSetup = + | { readonly kind: 'disabled' } + | { readonly kind: 'blocked'; readonly reason: VerificationSetupFailure; readonly sourceSha?: string } + | { readonly kind: 'ready'; readonly script: string; readonly sourceSha: string; readonly workspace: string; readonly node: string; readonly toolRoot?: string }; + +export const DISABLED_VERIFICATION_SETUP: PreparedVerificationSetup = Object.freeze({ kind: 'disabled' }); diff --git a/src/application/usecases/steps/commit/bugbot/verify_command_runner.ts b/src/application/usecases/steps/commit/bugbot/verify_command_runner.ts index c82012da8..0930a00dd 100644 --- a/src/application/usecases/steps/commit/bugbot/verify_command_runner.ts +++ b/src/application/usecases/steps/commit/bugbot/verify_command_runner.ts @@ -1,6 +1,7 @@ import { logError } from '../../../../ports/logging_ports'; import { parseVerifyCommand, type ParsedVerifyCommand } from './verify_command_policy'; import { VerificationPrerequisiteError } from '../../../../policies/verification_runtime_policy'; +import { VerificationSetupError } from '../../../../policies/verification_setup_policy'; export interface VerifyCommandResult { success: boolean; @@ -33,7 +34,7 @@ async function executeVerifyCommand( ? { success: true } : { success: false, failedCommand: formatCommandForDiagnostics(parsed) }; } catch (error) { - if (error instanceof VerificationPrerequisiteError) { + if (error instanceof VerificationPrerequisiteError || error instanceof VerificationSetupError) { return { success: false, failedCommand: formatCommandForDiagnostics(parsed), error: `${error.message} Verification command: ${formatCommandForDiagnostics(parsed)}.` }; } diff --git a/src/cli/setup_config_file.ts b/src/cli/setup_config_file.ts index 712137495..8507a9331 100644 --- a/src/cli/setup_config_file.ts +++ b/src/cli/setup_config_file.ts @@ -48,7 +48,7 @@ const REPOSITORY_STRING_KEYS = new Set([ 'orchestrationPresentationMode', 'orchestrationCommentMode', ]); -const REPOSITORY_BOOLEAN_KEYS = new Set(['issueManagedBranches', 'preBranchSdd', 'reopenIssueOnPush', 'orchestrationDiagrams']); +const REPOSITORY_BOOLEAN_KEYS = new Set(['issueManagedBranches', 'preBranchSdd', 'verificationSetup', 'reopenIssueOnPush', 'orchestrationDiagrams']); const REPOSITORY_NUMBER_KEYS = new Set(['desiredAssigneesCount', 'desiredReviewersCount', 'inactivityThresholdHours']); const REPOSITORY_STRUCTURED_KEYS = new Set(['mergeQueueCheckAttestations']); const AI_STRING_KEYS = new Set(['ignoreFiles', 'pullRequestDescriptionMode', 'bugbotSeverity', 'bugbotFixVerifyCommands', 'bugbotEffort', 'bugbotOrganizationRules', 'provisioningMode']); diff --git a/src/data/model/execution.ts b/src/data/model/execution.ts index 0c2296b40..b56d86c16 100644 --- a/src/data/model/execution.ts +++ b/src/data/model/execution.ts @@ -65,6 +65,7 @@ export class Execution { currentIssueWorkflowAdmission?: IssueWorkflowAdmission; issueWorkflowRuntimeMode: IssueWorkflowRuntimeMode = 'execute'; readonly preBranchSdd: boolean; + readonly verificationSetup: boolean; get eventName(): string { return this.inputs?.eventName ?? ''; @@ -206,6 +207,7 @@ export class Execution { this.currentIssueWorkflowAdmission = components.issueWorkflowAdmission; this.currentConfiguration.issueWorkflowProfileDigest = components.issueWorkflowProfileDigest; this.preBranchSdd = components.preBranchSdd ?? false; + this.verificationSetup = components.verificationSetup ?? false; } } diff --git a/src/data/model/execution_components.ts b/src/data/model/execution_components.ts index 9f906253d..e9a9c0886 100644 --- a/src/data/model/execution_components.ts +++ b/src/data/model/execution_components.ts @@ -46,4 +46,5 @@ export interface ExecutionComponents { issueWorkflowProfileDigest?: string; issueWorkflowAdmission?: IssueWorkflowAdmission; preBranchSdd?: boolean; + verificationSetup?: boolean; } diff --git a/src/domain/setup.ts b/src/domain/setup.ts index 1aa80db77..2623f1481 100644 --- a/src/domain/setup.ts +++ b/src/domain/setup.ts @@ -55,6 +55,8 @@ export interface SetupRepositoryConfiguration { choreTree: string; issueManagedBranches: boolean; preBranchSdd: boolean; + /** Advanced opt-in; absent legacy configuration remains disabled. */ + verificationSetup?: boolean; reopenIssueOnPush: boolean; desiredAssigneesCount: number; desiredReviewersCount: number; diff --git a/src/infrastructure/__tests__/verification_setup_configuration.test.ts b/src/infrastructure/__tests__/verification_setup_configuration.test.ts new file mode 100644 index 000000000..305362496 --- /dev/null +++ b/src/infrastructure/__tests__/verification_setup_configuration.test.ts @@ -0,0 +1,64 @@ +import fs from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import * as yaml from 'js-yaml'; +import { createDefaultSetupConfiguration, mergeSetupConfiguration, buildSetupActionInputs, buildSetupRepositoryVariables, validateSetupConfiguration } from '../../application/policies/setup_configuration_policy'; +import { loadSetupConfigurationOverrides } from '../../cli/setup_config_file'; +import { SETUP_ORGANIZATION_VARIABLE_BINDINGS } from '../../application/policies/setup_organization_variable_bindings'; +import { copySetupFiles, ensureGitHubDirs } from '../../utils/setup_files'; +import { parseIssueWorkflowBoolean } from '../../actions/input_boolean_policy'; + +jest.mock('../../utils/logger', () => ({ logInfo: jest.fn() })); +describe('verification setup installation contract', () => { + let root: string; + beforeEach(() => { root = fs.mkdtempSync(path.join(tmpdir(), 'copilot-setup-config-')); }); + afterEach(() => { fs.rmSync(root, { recursive: true, force: true }); }); + it('defaults off and accepts legacy configuration without the additive property', () => { + const config = createDefaultSetupConfiguration(); + expect(config.repository.verificationSetup).toBe(false); + delete config.repository.verificationSetup; + expect(validateSetupConfiguration(config)).not.toContain('verification-setup must be a boolean value.'); + expect(buildSetupActionInputs(config)['verification-setup']).toBe('false'); + expect(buildSetupRepositoryVariables(config)).toContainEqual({ name: 'VERIFICATION_SETUP', value: 'false' }); + }); + it('loads and plans the explicit opt-in consistently', () => { + const file = path.join(root, 'setup.yml'); fs.writeFileSync(file, 'repository:\n verificationSetup: true\n'); + const config = mergeSetupConfiguration(createDefaultSetupConfiguration(), loadSetupConfigurationOverrides(file)); + expect(buildSetupActionInputs(config)['verification-setup']).toBe('true'); + expect(buildSetupRepositoryVariables(config)).toContainEqual({ name: 'VERIFICATION_SETUP', value: 'true' }); + expect(SETUP_ORGANIZATION_VARIABLE_BINDINGS).toContainEqual({ variable: 'VERIFICATION_SETUP', path: 'repository.verificationSetup' }); + }); + it.each(['true', '/arbitrary/helper', 1])('rejects nonboolean opt-in %j in config files and plans', value => { + const file = path.join(root, 'setup.json'); fs.writeFileSync(file, JSON.stringify({ repository: { verificationSetup: value } })); + expect(() => loadSetupConfigurationOverrides(file)).toThrow('repository.verificationSetup must be a boolean'); + const config = createDefaultSetupConfiguration(); Object.assign(config.repository, { verificationSetup: value }); + expect(validateSetupConfiguration(config)).toContain('verification-setup must be a boolean value.'); + }); + it.each(['yes', 'script.mjs'])('rejects arbitrary Action opt-in %s', value => { + expect(() => parseIssueWorkflowBoolean(value, 'verification-setup', false)).toThrow('verification-setup must be true or false'); + }); + it.each(['copilot_issue.yml', 'copilot_issue_comment.yml', 'copilot_pull_request.yml', 'copilot_pull_request_comment.yml', 'copilot_pull_request_review_state.yml', 'copilot_commit.yml'])( + 'keeps source and distributed %s opt-in explicit and disabled by default', name => { + for (const directory of ['.github/workflows', 'setup/workflows']) { + const workflow = yaml.load(fs.readFileSync(path.resolve(__dirname, '../../..', directory, name), 'utf8')) as {jobs: Record}[]}>}; + const actions = Object.values(workflow.jobs).flatMap(job => job.steps).filter(step => step.uses === './' || step.uses?.startsWith('vypdev/copilot@')); + expect(actions.length).toBeGreaterThan(0); + for (const action of actions) expect(action.with?.['verification-setup']).toBe("${{ vars.VERIFICATION_SETUP || 'false' }}"); + } + }, + ); + it('preserves the consumer helper through install and repeated workflow updates', () => { + const helper = path.join(root, '.copilot/verification-setup.mjs'); fs.mkdirSync(path.dirname(helper), { recursive: true }); + const bytes = 'export const repositoryOwned = true;\n'; fs.writeFileSync(helper, bytes); + const config = createDefaultSetupConfiguration(); config.repository.verificationSetup = true; + const source = path.resolve(__dirname, '../../../setup'); + ensureGitHubDirs(root); copySetupFiles(root, source, config.features, { setupConfiguration: config }); + const approved = fs.readdirSync(path.join(root, '.github/workflows')); + for (let count = 0; count < 2; count++) { + copySetupFiles(root, source, config.features, { setupConfiguration: config, updateExistingWorkflows: true, approvedWorkflowFiles: approved }); + expect(fs.readFileSync(helper, 'utf8')).toBe(bytes); + } + const manifest = fs.readFileSync(path.join(root, '.copilot/setup-manifest.json'), 'utf8'); + expect(manifest).not.toContain('verification-setup.mjs'); + }); +}); diff --git a/src/infrastructure/__tests__/verification_setup_runner.test.ts b/src/infrastructure/__tests__/verification_setup_runner.test.ts new file mode 100644 index 000000000..75fefd720 --- /dev/null +++ b/src/infrastructure/__tests__/verification_setup_runner.test.ts @@ -0,0 +1,189 @@ +import fs from 'node:fs'; +import { tmpdir } from 'node:os'; +import { delimiter, join } from 'node:path'; +import { GitCommitAdapter } from '../git_commit_adapter'; +import { prepareUntrustedCommandEnvironment } from '../untrusted_command_environment'; +import { prepareRepositoryVerification } from '../verification_setup_runner'; +import { applyVerificationSetupResult } from '../verification_setup_result'; +import { runVerificationSetupProcess } from '../verification_setup_process'; +import type { PreparedVerificationSetup } from '../../application/ports/verification_setup_ports'; +import { VerificationSetupError } from '../../application/policies/verification_setup_policy'; +import { runVerifyCommands } from '../../application/usecases/steps/commit/bugbot/verify_command_runner'; + +const sourceSha = 'a'.repeat(40); +describe('repository verification setup execution', () => { + let root: string; + let runtime: ReturnType; + const setup = (script = 'export {};'): PreparedVerificationSetup => Object.freeze({ kind: 'ready', script, sourceSha, workspace: root, node: process.execPath }); + beforeEach(() => { + root = fs.realpathSync(fs.mkdtempSync(join(tmpdir(), 'copilot-setup-runner-'))); + runtime = prepareUntrustedCommandEnvironment({ PATH: process.env.PATH, SystemRoot: process.env.SystemRoot }); + jest.spyOn(process, 'cwd').mockReturnValue(root); + }); + afterEach(() => { jest.restoreAllMocks(); runtime.cleanup(); fs.rmSync(root, { recursive: true, force: true }); }); + + it('skips disabled setup and defers blocked snapshots until actual verification', async () => { + await prepareRepositoryVerification({ kind: 'disabled' }, runtime.environment); + const execute = jest.fn().mockResolvedValue(0); + const adapter = new GitCommitAdapter(execute, jest.fn(), { kind: 'blocked', reason: 'source-unavailable' }); + await adapter.execute('git', ['status'], { untrusted: true }); + await expect(adapter.execute('node', ['test'], { untrusted: true, verifyRuntimePrerequisites: true })).rejects.toMatchObject({ reason: 'source-unavailable' }); + expect(execute).toHaveBeenCalledTimes(1); + }); + it('does not prepare for an empty command list', async () => { + const execute = jest.fn(); + expect(await runVerifyCommands([], execute)).toEqual({ success: true }); + expect(execute).not.toHaveBeenCalled(); + }); + it('executes trusted standalone ESM in candidate cwd, without process/agent credentials', async () => { + const script = `import fs from 'node:fs'; + for (const key of ['PAT','GITHUB_TOKEN','OPENAI_API_KEY','AWS_SECRET_ACCESS_KEY','CODEX_HOME','NODE_OPTIONS','GIT_CONFIG_VALUE_0']) + if (process.env[key]) throw new Error('credential leaked'); + if (process.env.COPILOT_VERIFICATION_SETUP_PROTOCOL !== '1') throw new Error('protocol'); + fs.writeFileSync('observed.json', JSON.stringify({cwd:process.cwd(),home:process.env.HOME,sha:process.env.COPILOT_VERIFICATION_SETUP_SOURCE_SHA}));`; + await prepareRepositoryVerification(setup(script), runtime.environment); + expect(JSON.parse(fs.readFileSync(join(root, 'observed.json'), 'utf8'))).toEqual({ cwd: root, home: runtime.environment.HOME, sha: sourceSha }); + expect(runtime.environment).not.toHaveProperty('COPILOT_VERIFICATION_SETUP_RESULT'); + }); + it('refreshes setup from changed candidate manifests for each command, before probes in the same environment', async () => { + const script = `import fs from 'node:fs'; import path from 'node:path'; + const bin=path.join(process.env.HOME,'bin'); fs.mkdirSync(bin); + fs.writeFileSync(path.join(bin,'candidate'),fs.readFileSync('manifest.json')); + fs.appendFileSync('preparations','1'); + fs.writeFileSync(process.env.COPILOT_VERIFICATION_SETUP_RESULT,JSON.stringify({schemaVersion:1,path:[bin],environment:{JAVA_HOME:bin}}));`; + const order: string[] = []; const homes: string[] = []; + let observed: Record | undefined; + const adapter = new GitCommitAdapter(async (_program, _args, options) => { + order.push('verify'); expect(options?.env).toBe(observed); homes.push(options!.env!.HOME); + expect(fs.readFileSync(join(options!.env!.JAVA_HOME, 'candidate'), 'utf8')).toBe(fs.readFileSync(join(root, 'manifest.json'), 'utf8')); + return 0; + }, environment => { + order.push('preflight'); observed = environment; + expect(environment.PATH.split(delimiter)[0]).toBe(environment.JAVA_HOME); + }, setup(script)); + fs.writeFileSync(join(root, 'manifest.json'), 'first'); + await adapter.execute('node', ['test'], { untrusted: true, verifyRuntimePrerequisites: true }); + fs.writeFileSync(join(root, 'manifest.json'), 'changed'); + await adapter.execute('node', ['test'], { untrusted: true, verifyRuntimePrerequisites: true }); + expect(order).toEqual(['preflight','verify','preflight','verify']); + expect(new Set(homes).size).toBe(2); expect(homes.every(home => !fs.existsSync(home))).toBe(true); + expect(fs.readFileSync(join(root, 'preparations'), 'utf8')).toBe('11'); + }); + it('rejects a workspace switch before running any helper code', async () => { + jest.mocked(process.cwd).mockReturnValue(runtime.environment.HOME); + await expect(prepareRepositoryVerification(setup(), runtime.environment)).rejects.toMatchObject({ reason: 'workspace-changed' }); + }); + it('retains safe exit metadata and never emits helper output or raw exception text', async () => { + await expect(prepareRepositoryVerification(setup('console.error("secret @everyone"); process.exit(7);'), runtime.environment)) + .rejects.toMatchObject({ reason: 'process-failed', exitCode: 7, message: expect.stringContaining('Exit status: 7') }); + }); + it('withholds verification and cleans the private home when setup fails', async () => { + const execute = jest.fn(); const preflight = jest.fn(); + const adapter = new GitCommitAdapter(execute, preflight, setup('throw new Error("secret");')); + const result = await runVerifyCommands(['node test'], (program, args) => adapter.execute(program, args, { untrusted: true, verifyRuntimePrerequisites: true })); + expect(result).toMatchObject({ success: false, error: expect.stringContaining('Verification, commit and push are withheld') }); + expect(result.error).not.toContain('secret'); expect(execute).not.toHaveBeenCalled(); expect(preflight).not.toHaveBeenCalled(); + }); + it('fails closed on private script write errors without exposing filesystem details', async () => { + const error = jest.spyOn(fs, 'writeFileSync').mockImplementationOnce(() => { throw new Error('secret-path'); }); + await expect(prepareRepositoryVerification(setup(), runtime.environment)).rejects.toMatchObject({ reason: 'process-failed' }); + error.mockRestore(); + }); + it.each([ + ['process.exit(4);', 'process-failed', { timeoutMs: 5_000, outputBytes: 65_536 }], + ['setInterval(()=>{},1000);', 'timeout', { timeoutMs: 100, outputBytes: 65_536 }], + ['process.stdout.write("x".repeat(100000));', 'output-limit', { timeoutMs: 5_000, outputBytes: 100 }], + ['process.stderr.write("x".repeat(100000));', 'output-limit', { timeoutMs: 5_000, outputBytes: 100 }], + ])('bounds process execution %#', async (script, reason, limits) => { + const file = join(runtime.environment.HOME, 'bounded.mjs'); fs.writeFileSync(file, script); + await expect(runVerificationSetupProcess(process.execPath, file, root, runtime.environment, sourceSha, limits)).rejects.toMatchObject({ reason }); + }); + it('maps spawn failure to a sanitized closed diagnostic', async () => { + await expect(runVerificationSetupProcess(join(root, 'missing-node'), 'script', root, runtime.environment, sourceSha)).rejects.toBeInstanceOf(VerificationSetupError); + }); + it('terminates setup on cancellation and removes its signal listeners', async () => { + const file = join(runtime.environment.HOME, 'cancel.mjs'); fs.writeFileSync(file, 'setInterval(()=>{},1000);'); + const before = process.listenerCount('SIGTERM'); + const once = jest.spyOn(process, 'once'); + const running = runVerificationSetupProcess(process.execPath, file, root, runtime.environment, sourceSha); + const cancel = once.mock.calls.find(([signal]) => signal === 'SIGTERM')![1]; + cancel(); + await expect(running).rejects.toMatchObject({ reason: 'cancelled' }); + expect(process.listenerCount('SIGTERM')).toBe(before); + }); +}); + +describe('bounded setup result filesystem contract', () => { + let root: string; let environment: Record; let result: string; + beforeEach(() => { + root = fs.realpathSync(fs.mkdtempSync(join(tmpdir(), 'copilot-setup-result-'))); + environment = { HOME: root, PATH: 'baseline' }; result = join(root, 'result.json'); + }); + afterEach(() => { jest.restoreAllMocks(); fs.rmSync(root, { recursive: true, force: true }); }); + it('accepts an absent result without changing verification environment', () => { + applyVerificationSetupResult(result, environment, [root]); expect(environment).toEqual({ HOME: root, PATH: 'baseline' }); + }); + it('prepends approved directories and changes only JAVA_HOME', () => { + const bin = join(root, 'bin'); fs.mkdirSync(bin); + fs.writeFileSync(result, JSON.stringify({ schemaVersion: 1, path: [bin], environment: { JAVA_HOME: root } })); + applyVerificationSetupResult(result, environment, [root]); + expect(environment).toEqual({ HOME: root, PATH: `${bin}${delimiter}baseline`, JAVA_HOME: root }); + }); + it('supports a PATH addition with no inherited PATH', () => { + delete environment.PATH; fs.writeFileSync(result, JSON.stringify({ schemaVersion: 1, path: [root] })); + applyVerificationSetupResult(result, environment, [root]); expect(environment.PATH).toBe(root); + }); + it.each([ + 'not json secret', 'x'.repeat(16_385), JSON.stringify({ schemaVersion: 1, environment: { PAT: 'secret' } }), + JSON.stringify({ schemaVersion: 1, path: ['relative'] }), + ])('rejects unsafe result content %#', content => { + fs.writeFileSync(result, content); + expect(() => applyVerificationSetupResult(result, environment, [root])).toThrow(expect.objectContaining({ reason: 'result-invalid' })); + expect(environment).toEqual({ HOME: root, PATH: 'baseline' }); + }); + it('rejects directory results', () => { + fs.mkdirSync(result); expect(() => applyVerificationSetupResult(result, environment, [root])).toThrow(VerificationSetupError); + }); + it('rejects hard-linked result files', () => { + const original = join(root, 'original'); fs.writeFileSync(original, '{"schemaVersion":1}'); fs.linkSync(original, result); + expect(() => applyVerificationSetupResult(result, environment, [root])).toThrow(VerificationSetupError); + }); + it.each(['outside', 'traversal', 'delimiter', 'missing', 'file', 'link'])('rejects %s PATH directories', kind => { + const outside = fs.realpathSync(fs.mkdtempSync(join(tmpdir(), 'copilot-setup-outside-'))); + const file = join(root, 'ordinary'); fs.writeFileSync(file, 'text'); + const link = join(root, 'link'); fs.symlinkSync(outside, link, process.platform === 'win32' ? 'junction' : 'dir'); + const values: Record = { outside, traversal: join(root, 'child') + '/../../escape', delimiter: root + delimiter + outside, missing: join(root, 'missing'), file, link }; + fs.writeFileSync(result, JSON.stringify({ schemaVersion: 1, path: [values[kind]] })); + try { expect(() => applyVerificationSetupResult(result, environment, [root])).toThrow(VerificationSetupError); } + finally { fs.rmSync(outside, { recursive: true, force: true }); } + }); + it('does not apply partial PATH changes when JAVA_HOME is rejected', () => { + fs.writeFileSync(result, JSON.stringify({ schemaVersion: 1, path: [root], environment: { JAVA_HOME: '/outside/untrusted' } })); + expect(() => applyVerificationSetupResult(result, environment, [root])).toThrow(VerificationSetupError); + expect(environment.PATH).toBe('baseline'); + }); + it('rejects a result replaced between lstat and open', () => { + fs.writeFileSync(result, '{"schemaVersion":1}'); const original = fs.fstatSync; + jest.spyOn(fs, 'fstatSync').mockImplementation(((fd: number, options: {bigint?: boolean}) => { + const stat = original(fd, options as { bigint: true }); return { ...stat, ino: stat.ino + 1n }; + }) as typeof fs.fstatSync); + expect(() => applyVerificationSetupResult(result, environment, [root])).toThrow(VerificationSetupError); + }); + it('bounds a result that grows after its initial stat', () => { + fs.writeFileSync(result, '{"schemaVersion":1}'); + jest.spyOn(fs, 'readSync').mockReturnValueOnce(16_385); + expect(() => applyVerificationSetupResult(result, environment, [root])).toThrow(VerificationSetupError); + }); + it('rejects a result mutated during the bounded read', () => { + fs.writeFileSync(result, '{"schemaVersion":1}'); const original = fs.fstatSync; let calls = 0; + jest.spyOn(fs, 'fstatSync').mockImplementation(((fd: number, options: {bigint?: boolean}) => { + const stat = original(fd, options as { bigint: true }); calls++; + return calls === 2 ? { ...stat, mtimeNs: stat.mtimeNs + 1n } : stat; + }) as typeof fs.fstatSync); + expect(() => applyVerificationSetupResult(result, environment, [root])).toThrow(VerificationSetupError); + }); + it('maps access denial without exposing raw filesystem diagnostics', () => { + jest.spyOn(fs, 'lstatSync').mockImplementationOnce(() => { throw Object.assign(new Error('secret path'), { code: 'EACCES' }); }); + expect(() => applyVerificationSetupResult(result, environment, [root])).toThrow(expect.objectContaining({ reason: 'result-invalid', message: expect.not.stringContaining('secret') })); + }); +}); diff --git a/src/infrastructure/__tests__/verification_setup_snapshot.test.ts b/src/infrastructure/__tests__/verification_setup_snapshot.test.ts new file mode 100644 index 000000000..115eeda25 --- /dev/null +++ b/src/infrastructure/__tests__/verification_setup_snapshot.test.ts @@ -0,0 +1,100 @@ +import cp from 'node:child_process'; +import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { captureVerificationSetup } from '../verification_setup_snapshot'; +import { VERIFICATION_SETUP_PATH } from '../../application/policies/verification_setup_policy'; +import * as environment from '../untrusted_command_environment'; + +describe('trusted repository setup snapshot', () => { + let root: string; + const git = (...args: string[]) => cp.execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); + const commit = (script: string | Buffer): string => { + mkdirSync(join(root, '.copilot'), { recursive: true }); + writeFileSync(join(root, VERIFICATION_SETUP_PATH), script); + git('add', '.'); git('commit', '-m', 'fixture'); + const sha = git('rev-parse', 'HEAD'); + git('update-ref', 'refs/remotes/origin/main', sha); + return sha; + }; + beforeEach(() => { + root = realpathSync(mkdtempSync(join(tmpdir(), 'copilot-setup-snapshot-'))); + git('init'); git('config', 'user.name', 'Test'); git('config', 'user.email', 'test@example.test'); + }); + afterEach(() => { jest.restoreAllMocks(); rmSync(root, { recursive: true, force: true }); }); + + it('does no acquisition when disabled, even with absent workspace/ref', () => { + expect(captureVerificationSetup(false, 'invalid', '/missing')).toEqual({ kind: 'disabled' }); + }); + it('pins trusted bytes and SHA before candidate changes or remote-ref mutation', () => { + const sha = commit('console.log("trusted");'); + const snapshot = captureVerificationSetup(true, 'main', root); + commit('throw new Error("candidate");'); + expect(snapshot).toMatchObject({ kind: 'ready', sourceSha: sha, script: 'console.log("trusted");', workspace: root, node: process.execPath }); + expect(Object.isFrozen(snapshot)).toBe(true); + }); + it('defers acquisition failure on query/no-op routes', () => { + expect(captureVerificationSetup(true, 'main', '/absent/workspace')).toEqual({ kind: 'blocked', reason: 'source-unavailable' }); + }); + it('defers private acquisition cleanup failure instead of failing a read-only route', () => { + const cleanup = jest.fn(() => { throw new Error('private runtime failure'); }); + jest.spyOn(environment, 'prepareUntrustedCommandEnvironment').mockReturnValue({ environment: {}, cleanup }); + expect(captureVerificationSetup(true, '--invalid', root)).toEqual({ kind: 'blocked', reason: 'source-unavailable' }); + expect(cleanup).toHaveBeenCalledTimes(1); + }); + it('defers missing trusted refs without consulting the candidate helper', () => { + expect(captureVerificationSetup(true, 'main', root)).toEqual({ kind: 'blocked', reason: 'source-unavailable' }); + }); + it('captures the canonical trusted tool root independently of later environment changes', () => { + commit('export {};'); const previous = process.env.RUNNER_TOOL_CACHE; + try { + process.env.RUNNER_TOOL_CACHE = root; + expect(captureVerificationSetup(true, 'main', root)).toMatchObject({ kind: 'ready', toolRoot: root }); + } finally { if (previous === undefined) delete process.env.RUNNER_TOOL_CACHE; else process.env.RUNNER_TOOL_CACHE = previous; } + }); + it('verifies SHA-256 Git object stores without truncating their identity', () => { + rmSync(join(root, '.git'), { recursive: true }); git('init', '--object-format=sha256'); + git('config', 'user.name', 'Test'); git('config', 'user.email', 'test@example.test'); + const sha = commit('export {};'); expect(sha).toHaveLength(64); + expect(captureVerificationSetup(true, 'main', root)).toMatchObject({ kind: 'ready', sourceSha: sha }); + }); + it('rejects enabled missing helpers at the captured trusted SHA', () => { + git('commit', '--allow-empty', '-m', 'empty'); const sha = git('rev-parse', 'HEAD'); + git('update-ref', 'refs/remotes/origin/main', sha); + expect(captureVerificationSetup(true, 'main', root)).toEqual({ kind: 'blocked', reason: 'helper-invalid', sourceSha: sha }); + }); + it.each(['--bad', 'main..branch', 'a//b', 'a/', 'a.lock', 'a\nbranch'])('rejects unsafe trusted source %j', branch => { + expect(captureVerificationSetup(true, branch, root)).toMatchObject({ kind: 'blocked', reason: 'source-unavailable' }); + }); + it.each([Buffer.from(''), Buffer.from([255, 254]), Buffer.from('a\0b'), Buffer.alloc(65_537, 97)])('rejects unsafe helper bytes %#', bytes => { + commit(bytes); + expect(captureVerificationSetup(true, 'main', root)).toMatchObject({ kind: 'blocked', reason: 'helper-invalid' }); + }); + it('rejects Git symlink mode without following the candidate filesystem', () => { + commit('target'); const blob = git('rev-parse', `HEAD:${VERIFICATION_SETUP_PATH}`); + git('update-index', '--cacheinfo', `120000,${blob},${VERIFICATION_SETUP_PATH}`); git('commit', '-m', 'link'); + git('update-ref', 'refs/remotes/origin/main', git('rev-parse', 'HEAD')); + expect(captureVerificationSetup(true, 'main', root)).toMatchObject({ reason: 'helper-invalid' }); + }); + it('never forwards runner credentials or inherited auth profiles to Git reads', () => { + commit('export {};'); + const original = cp.spawnSync; + const reads = jest.spyOn(cp, 'spawnSync').mockImplementation(((program: string, args: string[], options: cp.SpawnSyncOptions) => { + expect(options.env).not.toHaveProperty('PAT'); + expect(options.env).not.toHaveProperty('OPENAI_API_KEY'); + expect(options.env?.HOME).not.toBe(process.env.HOME); + expect(options.shell).toBe(false); + return original(program, args, options); + }) as typeof cp.spawnSync); + expect(captureVerificationSetup(true, 'main', root).kind).toBe('ready'); + expect(reads).toHaveBeenCalledTimes(3); + }); + it('detects corrupted blob responses instead of executing mismatched bytes', () => { + commit('export {};'); const original = cp.spawnSync; + jest.spyOn(cp, 'spawnSync').mockImplementation(((program: string, args: string[], options: cp.SpawnSyncOptions) => { + const value = original(program, args, options); + return args.includes('cat-file') ? { ...value, stdout: Buffer.from('different') } : value; + }) as typeof cp.spawnSync); + expect(captureVerificationSetup(true, 'main', root)).toMatchObject({ reason: 'helper-invalid' }); + }); +}); diff --git a/src/infrastructure/__tests__/verification_setup_sync.test.ts b/src/infrastructure/__tests__/verification_setup_sync.test.ts new file mode 100644 index 000000000..0519fe5e6 --- /dev/null +++ b/src/infrastructure/__tests__/verification_setup_sync.test.ts @@ -0,0 +1,104 @@ +import fs from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { captureVerificationSetup } from '../verification_setup_snapshot'; +import { GitCommitAdapter } from '../git_commit_adapter'; +import { BranchSyncWorkspaceAdapter } from '../branch_sync_workspace_adapter'; +import { SyncBranchUseCase } from '../../application/usecases/branch_sync/sync_branch_use_case'; +import { projectBranchSyncContext } from '../../application/usecases/push_single_action_contexts'; +import type { Execution } from '../../data/model/execution'; + +const branch = 'feature/42-managed-fixture'; +const helper = `import fs from 'node:fs'; import path from 'node:path'; + const bin=path.join(process.env.HOME,'tools'); fs.mkdirSync(bin); + fs.writeFileSync(path.join(bin,'prepared'),fs.readFileSync('dependencies.lock')); + fs.writeFileSync(process.env.COPILOT_VERIFICATION_SETUP_RESULT,JSON.stringify({schemaVersion:1,path:[bin]}));`; +const verifier = `import fs from 'node:fs'; import path from 'node:path'; import assert from 'node:assert/strict'; + assert.equal(fs.readFileSync(path.join(process.env.HOME,'tools','prepared'),'utf8'),fs.readFileSync('dependencies.lock','utf8'));`; + +/** Real local Git remote, exact same managed ref, actual merge/verify/abort/push; no GitHub or network. */ +describe('repository setup during same-managed-branch synchronization', () => { + let directory: string; let root: string; let remote: string; let childSha: string; + const git = (...args: string[]) => execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); + const remoteHead = () => git('--git-dir', remote, 'rev-parse', `refs/heads/${branch}`); + beforeEach(() => { + directory = fs.realpathSync(fs.mkdtempSync(path.join(tmpdir(), 'copilot-setup-sync-'))); + root = path.join(directory, 'workspace'); remote = path.join(directory, 'origin.git'); fs.mkdirSync(root); + git('init', '--bare', remote); git('init', '-b', 'main'); + const emptyHooks = path.join(directory, 'empty-hooks'); fs.mkdirSync(emptyHooks); + git('config', 'core.hooksPath', emptyHooks); git('--git-dir', remote, 'config', 'core.hooksPath', emptyHooks); + git('config', 'user.name', 'Test'); git('config', 'user.email', 'test@example.test'); + fs.mkdirSync(path.join(root, '.copilot')); + fs.writeFileSync(path.join(root, '.copilot/verification-setup.mjs'), helper); + fs.writeFileSync(path.join(root, 'verify.mjs'), verifier); + fs.writeFileSync(path.join(root, 'dependencies.lock'), 'version-one'); + git('add', '.'); git('commit', '-m', 'trusted main'); git('remote', 'add', 'origin', remote); git('push', 'origin', 'main'); + git('checkout', '-b', 'develop'); git('push', 'origin', 'develop'); git('checkout', '-b', branch); + fs.writeFileSync(path.join(root, '.copilot/verification-setup.mjs'), 'throw new Error("candidate helper must never execute");'); + git('add', '.'); git('commit', '-m', 'candidate helper change'); git('push', 'origin', branch); childSha = git('rev-parse', 'HEAD'); + git('checkout', 'develop'); fs.writeFileSync(path.join(root, 'dependencies.lock'), 'version-two'); + git('add', '.'); git('commit', '-m', 'parent dependency update'); git('push', 'origin', 'develop'); git('checkout', branch); + jest.spyOn(process, 'cwd').mockReturnValue(root); + }); + afterEach(() => { jest.restoreAllMocks(); fs.rmSync(directory, { recursive: true, force: true }); }); + async function synchronize(enabled: boolean, commands = ['node verify.mjs'], dryRun = false) { + const gitPort = new GitCommitAdapter(async (program, args, options) => { + const output = execFileSync(program, args, { cwd: root, env: options?.env, stdio: ['ignore', 'pipe', 'pipe'] }); + options?.stdout?.(output); return 0; + }, undefined, captureVerificationSetup(enabled, 'main', root)); + const workspace = new BranchSyncWorkspaceAdapter(gitPort, root); + const fixer = { fix: jest.fn() }; + const useCase = new SyncBranchUseCase({ + resolveTarget: async () => ({ issueNumber: 42, conversationNumber: 42, parentBranch: 'develop', workingBranch: branch }), + listOpenDependencies: async () => [], + }, { + prepare: (parent, child) => workspace.prepare(parent, child, ''), + validatePreparedMerge: paths => workspace.validatePreparedMerge(paths), + assertRemoteHeadsUnchanged: (parent, parentSha, child, sha) => workspace.assertRemoteHeadsUnchanged(parent, parentSha, child, sha, ''), + commitAndPush: (child, message, author) => workspace.commitAndPush(child, message, author, ''), + abort: () => workspace.abort(), + }, fixer, { getUserDetails: async () => ({ name: 'Test', email: 'test@example.test' }) } as never, gitPort); + const context = projectBranchSyncContext({ owner: 'fixture', repo: 'local', tokens: { token: '' }, issue: { number: 42 }, + pullRequest: { number: -1 }, issueNumber: 42, + ai: { getAgentConfiguration: () => ({ provider: 'codex', model: '' }), getBugbotFixVerifyCommands: () => commands }, + } as unknown as Execution); + const [result] = await useCase.invoke({ context, options: { dryRun, useAgent: false } }); + expect(fixer.fix).not.toHaveBeenCalled(); + return result; + } + it('fails without preparation, aborting the merge and preserving the exact remote head', async () => { + const result = await synchronize(false); + expect(result.success).toBe(false); expect(remoteHead()).toBe(childSha); expect(git('rev-parse', 'HEAD')).toBe(childSha); + expect(git('status', '--porcelain')).toBe(''); + }); + it('prepares merged manifests from trusted main, verifies twice, and pushes only the same managed ref', async () => { + const result = await synchronize(true, ['node verify.mjs', 'node verify.mjs']); + expect(result).toMatchObject({ success: true, payload: { outcome: 'merged-cleanly', verificationCount: 2 } }); + expect(remoteHead()).toBe(git('rev-parse', 'HEAD')); expect(remoteHead()).not.toBe(childSha); + expect(git('show', `${remoteHead()}:dependencies.lock`)).toBe('version-two'); + expect(git('show', `${remoteHead()}:.copilot/verification-setup.mjs`)).toContain('candidate helper must never execute'); + expect(git('--git-dir', remote, 'for-each-ref', '--format=%(refname)', 'refs/heads').split('\n').sort()) + .toEqual(['refs/heads/develop', `refs/heads/${branch}`, 'refs/heads/main']); + }); + it('skips setup in dry runs even when the trusted helper is invalid', async () => { + git('update-ref', 'refs/remotes/origin/main', childSha); + const result = await synchronize(true, ['node verify.mjs'], true); + expect(result).toMatchObject({ success: true, payload: { outcome: 'dry-run-clean' } }); expect(remoteHead()).toBe(childSha); + }); + it('does not run setup for aligned branches', async () => { + git('merge', '--no-edit', 'develop'); git('push', 'origin', branch); childSha = remoteHead(); + git('update-ref', 'refs/remotes/origin/main', childSha); + expect(await synchronize(true)).toMatchObject({ success: true, payload: { outcome: 'already-aligned' } }); + expect(remoteHead()).toBe(childSha); + }); + it('retains existing no-command policy without invoking an invalid helper', async () => { + git('update-ref', 'refs/remotes/origin/main', childSha); + expect(await synchronize(true, [])).toMatchObject({ success: true, payload: { verificationCount: 0 } }); + }); + it('aborts and withholds push when trusted setup fails', async () => { + git('update-ref', 'refs/remotes/origin/main', childSha); + const result = await synchronize(true); + expect(result.success).toBe(false); expect(remoteHead()).toBe(childSha); expect(git('status', '--porcelain')).toBe(''); + }); +}); diff --git a/src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts b/src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts index 7c5073484..3c91cb1eb 100644 --- a/src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts +++ b/src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts @@ -9,6 +9,7 @@ import { ThinkUseCase } from "../../../application/usecases/steps/common/think_u import { RememberBugbotRuleUseCase } from "../../../application/usecases/steps/commit/bugbot/remember_bugbot_rule_use_case"; import { CheckIssueCommentLanguageUseCase } from "../../../application/usecases/steps/issue_comment/check_issue_comment_language_use_case"; import { GitCommitAdapter } from "../../git_commit_adapter"; +import { captureVerificationSetup } from '../../verification_setup_snapshot'; import type { ProjectBoardCommandPort } from "../../../application/ports/project_board_command_ports"; import type { Execution } from "../../../data/model/execution"; import { @@ -84,10 +85,23 @@ jest.mock( "../../../application/usecases/steps/issue_comment/check_issue_comment_language_use_case", ); jest.mock("../../git_commit_adapter"); +jest.mock('../../verification_setup_snapshot', () => ({ captureVerificationSetup: jest.fn(() => ({ kind: 'disabled' })) })); describe("main run route composition root", () => { beforeEach(() => jest.clearAllMocks()); + it.each(['issue-comment', 'pull-request-review-comment'] as const)('captures trusted setup before %s can invoke an agent', async route => { + const snapshot = { kind: 'blocked', reason: 'source-unavailable' } as const; + jest.mocked(captureVerificationSetup).mockReturnValueOnce(snapshot); + const execution = { owner: 'owner', repo: 'repo', tokens: { token: 'token' }, branches: { main: 'production' }, verificationSetup: true } as Execution; + const handlers = createMainRunRouteCompositionRoot({} as ProjectBoardCommandPort, 'local'); + await handlers[route](execution); + expect(captureVerificationSetup).toHaveBeenCalledWith(true, 'production'); + expect(GitCommitAdapter).toHaveBeenCalledWith(undefined, undefined, snapshot); + const useCase = route === 'issue-comment' ? jest.mocked(IssueCommentUseCase).mock.instances[0] : jest.mocked(PullRequestReviewCommentUseCase).mock.instances[0]; + expect(jest.mocked(captureVerificationSetup).mock.invocationCallOrder[0]).toBeLessThan(jest.mocked(useCase.invoke).mock.invocationCallOrder[0]); + }); + it("shares route-scoped Bugbot, agent, and Git commit capabilities", () => { const useCase = createIssueCommentUseCaseCompositionRoot(binding); @@ -199,6 +213,8 @@ describe("main run route composition root", () => { owner: 'owner', repo: 'repo', tokens: { token: 'token' }, + branches: { main: 'master' }, + verificationSetup: false, } as Execution; const handlers = createMainRunRouteCompositionRoot( {} as ProjectBoardCommandPort, diff --git a/src/infrastructure/composition/main_run_route_composition_root.ts b/src/infrastructure/composition/main_run_route_composition_root.ts index f026e87cb..8528c1857 100644 --- a/src/infrastructure/composition/main_run_route_composition_root.ts +++ b/src/infrastructure/composition/main_run_route_composition_root.ts @@ -201,13 +201,15 @@ function createDeploymentOrchestrationUseCase( } import { createCopilotHistoryReadPort } from './copilot_history_port_factory'; +import { captureVerificationSetup } from '../verification_setup_snapshot'; +import { DISABLED_VERIFICATION_SETUP, type PreparedVerificationSetup } from '../../application/ports/verification_setup_ports'; -export function createIssueCommentUseCaseCompositionRoot(binding: BugbotScmBinding): IssueCommentUseCase { +export function createIssueCommentUseCaseCompositionRoot(binding: BugbotScmBinding, setup: PreparedVerificationSetup = DISABLED_VERIFICATION_SETUP): IssueCommentUseCase { const bugbot = createBugbotCompositionRoot(binding); const findings = createFindingsQueryPort(); const language = createLanguageQueryPort(); const fixer = createFixerQueryPort(); - const gitCommit = new GitCommitAdapter(); + const gitCommit = new GitCommitAdapter(undefined, undefined, setup); const authenticatedUser = createAuthenticatedUserCompositionRoot(); const bugbotGit = new BoundBugbotGitMutationAdapter(gitCommit, authenticatedUser, binding.token); const pullRequestDescription = new UpdatePullRequestDescriptionUseCase( @@ -265,12 +267,12 @@ export function createIssueCommentUseCaseCompositionRoot(binding: BugbotScmBindi ); } -export function createPullRequestReviewCommentUseCaseCompositionRoot(binding: BugbotScmBinding): PullRequestReviewCommentUseCase { +export function createPullRequestReviewCommentUseCaseCompositionRoot(binding: BugbotScmBinding, setup: PreparedVerificationSetup = DISABLED_VERIFICATION_SETUP): PullRequestReviewCommentUseCase { const bugbot = createBugbotCompositionRoot(binding); const findings = createFindingsQueryPort(); const language = createLanguageQueryPort(); const fixer = createFixerQueryPort(); - const gitCommit = new GitCommitAdapter(); + const gitCommit = new GitCommitAdapter(undefined, undefined, setup); const authenticatedUser = createAuthenticatedUserCompositionRoot(); const bugbotGit = new BoundBugbotGitMutationAdapter(gitCommit, authenticatedUser, binding.token); const pullRequestDescription = new UpdatePullRequestDescriptionUseCase( @@ -362,7 +364,7 @@ export function createMainRunRouteCompositionRoot( return singleAction.invoke(execution); }, "issue-comment": async (execution) => { - issueComment ??= createIssueCommentUseCaseCompositionRoot(bugbotBinding(execution)); + issueComment ??= createIssueCommentUseCaseCompositionRoot(bugbotBinding(execution), captureVerificationSetup(execution.verificationSetup, execution.branches.main)); return issueComment.invoke(execution); }, issue: async (execution) => { @@ -370,7 +372,7 @@ export function createMainRunRouteCompositionRoot( return issue.invoke(execution); }, "pull-request-review-comment": async (execution) => { - pullRequestReviewComment ??= createPullRequestReviewCommentUseCaseCompositionRoot(bugbotBinding(execution)); + pullRequestReviewComment ??= createPullRequestReviewCommentUseCaseCompositionRoot(bugbotBinding(execution), captureVerificationSetup(execution.verificationSetup, execution.branches.main)); return pullRequestReviewComment.invoke(execution); }, "pull-request": async (execution) => { diff --git a/src/infrastructure/git_commit_adapter.ts b/src/infrastructure/git_commit_adapter.ts index dd2c57dc4..1e9ed8f7b 100644 --- a/src/infrastructure/git_commit_adapter.ts +++ b/src/infrastructure/git_commit_adapter.ts @@ -4,6 +4,8 @@ import { buildGitAuthenticationEnvironment } from './git_authentication_environm import { prepareUntrustedCommandEnvironment } from './untrusted_command_environment'; import { withoutGitHooks } from './git_hook_isolation'; import { assertVerificationRuntimePrerequisites } from './verification_runtime_preflight'; +import { DISABLED_VERIFICATION_SETUP, type PreparedVerificationSetup } from '../application/ports/verification_setup_ports'; +import { prepareRepositoryVerification } from './verification_setup_runner'; type GitCommandOptions = { stdout?: (data: Buffer) => void; env?: Record; untrusted?: boolean; verifyRuntimePrerequisites?: boolean }; type GitCommandExecutor = (program: string, args: string[], options?: GitCommandOptions) => Promise; @@ -20,6 +22,7 @@ export class GitCommitAdapter implements GitCommitPort { }) : exec.exec(program, args), private readonly checkPrerequisites: (environment: Record) => void = assertVerificationRuntimePrerequisites, + private readonly verificationSetup: PreparedVerificationSetup = DISABLED_VERIFICATION_SETUP, ) {} async execute(program: string, args: string[], options?: GitCommandOptions): Promise { @@ -28,7 +31,10 @@ export class GitCommitAdapter implements GitCommitPort { if (options.env) throw new Error('Untrusted command execution does not accept a caller-supplied environment.'); const runtime = prepareUntrustedCommandEnvironment(); try { - if (options.verifyRuntimePrerequisites) this.checkPrerequisites(runtime.environment); + if (options.verifyRuntimePrerequisites) { + await prepareRepositoryVerification(this.verificationSetup, runtime.environment); + this.checkPrerequisites(runtime.environment); + } return await this.executeCommand(program, args, { ...(options.stdout ? { stdout: options.stdout } : {}), env: runtime.environment, diff --git a/src/infrastructure/untrusted_command_environment.ts b/src/infrastructure/untrusted_command_environment.ts index f8f0604b4..57b5045c8 100644 --- a/src/infrastructure/untrusted_command_environment.ts +++ b/src/infrastructure/untrusted_command_environment.ts @@ -1,4 +1,4 @@ -import { mkdirSync, rmSync } from 'node:fs'; +import { mkdirSync, realpathSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { createPrivateVerificationRuntimeRoot } from './agents/agent_private_install_root'; @@ -45,7 +45,7 @@ export function prepareUntrustedCommandEnvironment( source: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform, ): PreparedUntrustedCommandEnvironment { - const runtimeHome = createPrivateVerificationRuntimeRoot(); + const runtimeHome = realpathSync(createPrivateVerificationRuntimeRoot()); const environment: Record = { HOME: runtimeHome }; for (const variable of ALLOWED_VARIABLES) { const value = source[variable]; diff --git a/src/infrastructure/verification_setup_process.ts b/src/infrastructure/verification_setup_process.ts new file mode 100644 index 000000000..a1590a25b --- /dev/null +++ b/src/infrastructure/verification_setup_process.ts @@ -0,0 +1,39 @@ +import { spawn, spawnSync } from 'node:child_process'; +import { trustedWindowsSystemTool } from './agents/agent_trusted_system_tools'; +import { VerificationSetupError, VERIFICATION_SETUP_LIMITS } from '../application/policies/verification_setup_policy'; + +/** All output is counted and discarded. Fixed production limits; short injected limits are for deterministic tests. */ +export function runVerificationSetupProcess(node: string, script: string, cwd: string, env: Record, sourceSha: string, + limits: { timeoutMs: number; outputBytes: number } = VERIFICATION_SETUP_LIMITS): Promise { + return new Promise((resolve, reject) => { + const child = spawn(node, [script], { cwd, env, shell: false, windowsHide: true, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] }); + let failure: 'timeout' | 'cancelled' | 'output-limit' | 'process-failed' | undefined; + let outputBytes = 0; + const terminate = (): void => { + if (!child.pid) return; + try { + if (process.platform === 'win32') spawnSync(trustedWindowsSystemTool('taskkill.exe'), ['/PID', String(child.pid), '/T', '/F'], { env, windowsHide: true, timeout: 5_000, stdio: 'ignore', shell: false }); + else process.kill(-child.pid, 'SIGKILL'); + } catch { child.kill('SIGKILL'); } + }; + const timer = setTimeout(() => { failure ??= 'timeout'; terminate(); }, limits.timeoutMs); + const cancel = (): void => { failure ??= 'cancelled'; terminate(); }; + process.once('SIGINT', cancel); + process.once('SIGTERM', cancel); + const capture = (data: Buffer): void => { + outputBytes += data.length; + if (outputBytes > limits.outputBytes) { failure ??= 'output-limit'; terminate(); } + }; + child.stdout.on('data', capture); + child.stderr.on('data', capture); + child.on('error', () => { failure ??= 'process-failed'; }); + child.on('close', code => { + clearTimeout(timer); + process.removeListener('SIGINT', cancel); + process.removeListener('SIGTERM', cancel); + terminate(); + if (failure || code !== 0) reject(new VerificationSetupError(failure ?? 'process-failed', sourceSha, code ?? undefined)); + else resolve(); + }); + }); +} diff --git a/src/infrastructure/verification_setup_result.ts b/src/infrastructure/verification_setup_result.ts new file mode 100644 index 000000000..909bdeada --- /dev/null +++ b/src/infrastructure/verification_setup_result.ts @@ -0,0 +1,42 @@ +import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync } from 'node:fs'; +import { delimiter, isAbsolute, relative, sep } from 'node:path'; +import { parseVerificationSetupResult, VerificationSetupError, VERIFICATION_SETUP_LIMITS } from '../application/policies/verification_setup_policy'; + +function contained(root: string, path: string): boolean { + const remainder = relative(root, path); + return remainder === '' || (remainder !== '..' && !remainder.startsWith(`..${sep}`) && !isAbsolute(remainder)); +} + +/** Accept only existing contained directories, checking both lexical paths and symlink targets. */ +function approvedDirectory(value: string, roots: readonly string[]): string { + if (!isAbsolute(value) || value.includes(delimiter) || value.split(/[\\/]/u).includes('..') + || !roots.some(root => contained(root, value))) throw new VerificationSetupError('result-invalid'); + const actual = realpathSync(value); + if (!roots.some(root => contained(root, actual)) || !lstatSync(actual).isDirectory()) throw new VerificationSetupError('result-invalid'); + return actual; +} + +export function applyVerificationSetupResult(file: string, environment: Record, roots: readonly string[]): void { + let descriptor: number | undefined; + try { + let before; + try { before = lstatSync(file, { bigint: true }); } + catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return; throw error; } + if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.size > BigInt(VERIFICATION_SETUP_LIMITS.resultBytes)) throw new Error(); + descriptor = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)); + const opened = fstatSync(descriptor, { bigint: true }); + if (!opened.isFile() || opened.nlink !== 1n || opened.dev !== before.dev || opened.ino !== before.ino + || opened.size !== before.size || opened.mtimeNs !== before.mtimeNs) throw new Error(); + const bytes = Buffer.alloc(VERIFICATION_SETUP_LIMITS.resultBytes + 1); + const size = readSync(descriptor, bytes, 0, bytes.length, 0); + if (size > VERIFICATION_SETUP_LIMITS.resultBytes) throw new Error(); + const after = fstatSync(descriptor, { bigint: true }); + if (after.size !== opened.size || after.mtimeNs !== opened.mtimeNs) throw new Error(); + const result = parseVerificationSetupResult(JSON.parse(bytes.subarray(0, size).toString('utf8'))); + const paths = result.path.map(value => approvedDirectory(value, roots)); + const javaHome = result.javaHome ? approvedDirectory(result.javaHome, roots) : undefined; + if (paths.length) environment.PATH = [...paths, ...(environment.PATH ? [environment.PATH] : [])].join(delimiter); + if (javaHome) environment.JAVA_HOME = javaHome; + } catch { throw new VerificationSetupError('result-invalid'); } + finally { if (descriptor !== undefined) closeSync(descriptor); } +} diff --git a/src/infrastructure/verification_setup_runner.ts b/src/infrastructure/verification_setup_runner.ts new file mode 100644 index 000000000..ae9d918f6 --- /dev/null +++ b/src/infrastructure/verification_setup_runner.ts @@ -0,0 +1,32 @@ +import { realpathSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import type { PreparedVerificationSetup } from '../application/ports/verification_setup_ports'; +import { VerificationSetupError } from '../application/policies/verification_setup_policy'; +import { applyVerificationSetupResult } from './verification_setup_result'; +import { runVerificationSetupProcess } from './verification_setup_process'; +import { logInfo } from '../application/ports/logging_ports'; +import { VERIFICATION_SETUP_PATH } from '../application/policies/verification_setup_policy'; + +/** Called once per verification command, on its fresh private profile, before runtime probes. */ +export async function prepareRepositoryVerification(setup: PreparedVerificationSetup, environment: Record): Promise { + if (setup.kind === 'disabled') return; + if (setup.kind === 'blocked') throw new VerificationSetupError(setup.reason, setup.sourceSha); + try { + if (realpathSync(process.cwd()) !== setup.workspace) throw new VerificationSetupError('workspace-changed', setup.sourceSha); + const profile = realpathSync(environment.HOME); + const script = join(profile, 'verification-setup.mjs'); + const result = join(profile, 'verification-setup-result.json'); + writeFileSync(script, setup.script, { mode: 0o600, flag: 'wx' }); + await runVerificationSetupProcess(setup.node, script, setup.workspace, { + ...environment, + COPILOT_VERIFICATION_SETUP_PROTOCOL: '1', + COPILOT_VERIFICATION_SETUP_RESULT: result, + COPILOT_VERIFICATION_SETUP_SOURCE_SHA: setup.sourceSha, + }, setup.sourceSha); + applyVerificationSetupResult(result, environment, [setup.workspace, profile, ...(setup.toolRoot ? [setup.toolRoot] : [])]); + logInfo(`Repository setup prepared (${VERIFICATION_SETUP_PATH}; trusted source SHA: ${setup.sourceSha}).`); + } catch (error) { + throw new VerificationSetupError(error instanceof VerificationSetupError ? error.reason : 'process-failed', setup.sourceSha, + error instanceof VerificationSetupError ? error.exitCode : undefined); + } +} diff --git a/src/infrastructure/verification_setup_snapshot.ts b/src/infrastructure/verification_setup_snapshot.ts new file mode 100644 index 000000000..f28dcfc5a --- /dev/null +++ b/src/infrastructure/verification_setup_snapshot.ts @@ -0,0 +1,46 @@ +import { spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { realpathSync } from 'node:fs'; +import type { PreparedVerificationSetup } from '../application/ports/verification_setup_ports'; +import { DISABLED_VERIFICATION_SETUP } from '../application/ports/verification_setup_ports'; +import { VERIFICATION_SETUP_LIMITS, VERIFICATION_SETUP_PATH, VerificationSetupError } from '../application/policies/verification_setup_policy'; +import { prepareUntrustedCommandEnvironment } from './untrusted_command_environment'; + +/** No network, agent, worktree-file reads, or credentials. Acquisition errors are deferred until verification. */ +export function captureVerificationSetup(enabled: boolean, mainBranch: string, root = process.cwd()): PreparedVerificationSetup { + if (!enabled) return DISABLED_VERIFICATION_SETUP; + let sourceSha: string | undefined; + let runtime: ReturnType | undefined; + let snapshot: PreparedVerificationSetup; + try { + runtime = prepareUntrustedCommandEnvironment(); + const environment = runtime.environment; + if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/u.test(mainBranch) || mainBranch.includes('..') + || mainBranch.includes('//') || mainBranch.endsWith('/') || mainBranch.endsWith('.lock')) throw new Error(); + const workspace = realpathSync(root); + const git = (...args: string[]): Buffer => { + const result = spawnSync('git', ['--no-replace-objects', ...args], { cwd: workspace, env: environment, + shell: false, windowsHide: true, timeout: 5_000, maxBuffer: VERIFICATION_SETUP_LIMITS.scriptBytes + 4096 }); + if (result.error || result.status !== 0) throw new Error(); + return result.stdout; + }; + const candidateSha = git('rev-parse', '--verify', `refs/remotes/origin/${mainBranch}^{commit}`).toString('utf8').trim(); + if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(candidateSha)) throw new Error(); + sourceSha = candidateSha; + const entry = git('ls-tree', '-z', sourceSha, '--', VERIFICATION_SETUP_PATH).toString('utf8'); + const match = /^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64})\t\.copilot\/verification-setup\.mjs\0$/u.exec(entry); + if (!match) throw new VerificationSetupError('helper-invalid'); + const bytes = git('cat-file', 'blob', match[2]); + const digest = createHash(match[2].length === 40 ? 'sha1' : 'sha256').update(`blob ${bytes.length}\0`).update(bytes).digest('hex'); + const script = bytes.toString('utf8'); + if (!bytes.length || bytes.length > VERIFICATION_SETUP_LIMITS.scriptBytes || digest !== match[2] + || !Buffer.from(script).equals(bytes) || script.includes('\0')) throw new VerificationSetupError('helper-invalid'); + const toolRoot = environment.RUNNER_TOOL_CACHE ? realpathSync(environment.RUNNER_TOOL_CACHE) : undefined; + snapshot = Object.freeze({ kind: 'ready', script, sourceSha, workspace, node: process.execPath, ...(toolRoot ? { toolRoot } : {}) }); + } catch (error) { + snapshot = Object.freeze({ kind: 'blocked', reason: error instanceof VerificationSetupError ? error.reason : 'source-unavailable', ...(sourceSha ? { sourceSha } : {}) }); + } + try { runtime?.cleanup(); } + catch { snapshot = Object.freeze({ kind: 'blocked', reason: 'source-unavailable', ...(sourceSha ? { sourceSha } : {}) }); } + return snapshot; +} diff --git a/src/tooling/__tests__/validate_workflow_contract.test.ts b/src/tooling/__tests__/validate_workflow_contract.test.ts index db7489c7a..d93f6a1a7 100644 --- a/src/tooling/__tests__/validate_workflow_contract.test.ts +++ b/src/tooling/__tests__/validate_workflow_contract.test.ts @@ -153,7 +153,7 @@ describe('workflow contract validator', () => { } }); - it.each(['issue-managed-branches', 'pre-branch-sdd'])( + it.each(['issue-managed-branches', 'pre-branch-sdd', 'verification-setup'])( 'rejects a PR comment callback that omits or disables the installed %s policy', input => { const { file, workflow } = loadMutationWorkflow('.github/workflows', 'copilot_pull_request_comment.yml'); const action = workflow.jobs['copilot-pull-requests'].steps.find((step: { uses?: string }) => step.uses === './'); diff --git a/src/utils/__tests__/setup_files.test.ts b/src/utils/__tests__/setup_files.test.ts index e5b3146c7..4e79692df 100644 --- a/src/utils/__tests__/setup_files.test.ts +++ b/src/utils/__tests__/setup_files.test.ts @@ -141,9 +141,9 @@ describe('setup_files', () => { const url = new URL(destination); if (url.origin === 'https://docs.page') { expect(url.pathname).toBe('/vypdev/copilot/security-operations/operations/verification'); - expect(url.hash).toBe('#repository-verification-prerequisites'); + expect(['#repository-verification-prerequisites', '#repository-owned-setup']).toContain(url.hash); expect(fs.readFileSync(path.join(sourceRoot, 'docs/security-operations/operations/verification.mdx'), 'utf8')) - .toContain('## Repository verification prerequisites'); + .toContain(url.hash === '#repository-owned-setup' ? '## Repository-owned setup' : '## Repository verification prerequisites'); } else { expect(url.origin).toBe('https://github.com'); expect(url.pathname).toMatch(/^\/vypdev\/copilot\/blob\/master\/docs\/agents\//u); @@ -158,6 +158,7 @@ describe('setup_files', () => { expect(documentationLinks).toEqual([ 'https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites', + 'https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup', 'https://github.com/vypdev/copilot/blob/master/docs/agents/repository-collaboration.mdx', 'https://github.com/vypdev/copilot/blob/master/docs/agents/repository-guidance-troubleshooting.mdx', ]); From e4387b8cbccce0bff0334f27905869303f5a6a28 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 21 Oct 2026 17:19:33 +0200 Subject: [PATCH 3/8] feat: run repository verification setup as native composite actions --- .copilot/AGENT_GUIDE.md | 2 +- .copilot/setup-manifest.json | 2 +- .../actions/copilot-native-fixture/action.yml | 10 + .github/workflows/copilot_issue_comment.yml | 515 +++++++- .../copilot_pull_request_comment.yml | 515 +++++++- .github/workflows/setup_platform_smoke.yml | 1034 +++++++++++++++++ .gitignore | 3 + action.yml | 4 +- build/cli/index.js | 452 +++++-- build/github_action/index.js | 821 +++++++++++-- docs/configuration.mdx | 2 +- docs/development/architecture.mdx | 42 +- .../operations/troubleshooting.mdx | 36 +- .../operations/verification.mdx | 241 ++-- scripts/coverage-budgets.json | 9 +- ...generate-native-verification-workflows.cjs | 58 + scripts/validate-workflow-contract.cjs | 18 + setup/workflows/copilot_commit.yml | 2 +- setup/workflows/copilot_issue.yml | 2 +- setup/workflows/copilot_issue_comment.yml | 517 ++++++++- setup/workflows/copilot_pull_request.yml | 2 +- .../copilot_pull_request_comment.yml | 517 ++++++++- .../copilot_pull_request_review_state.yml | 2 +- specs/CATALOG.md | 6 +- specs/catalog.json | 18 +- specs/repository-verification-setup-hooks.md | 420 +++---- src/actions/github_action.ts | 3 +- .../verification_setup_policy.test.ts | 4 +- .../repository_agent_guidance_policy.ts | 2 +- .../policies/verification_setup_policy.ts | 12 +- .../ports/verification_setup_ports.ts | 4 +- .../native_verification_controller.test.ts | 164 +++ .../__tests__/native_verification_fixture.ts | 41 + .../native_verification_profiles.test.ts | 38 + .../native_verification_protocol.test.ts | 72 ++ .../native_verification_redaction.test.ts | 40 + .../native_verification_runner.test.ts | 110 ++ .../native_verification_workflow.test.ts | 33 + .../verification_setup_configuration.test.ts | 4 +- .../verification_setup_runner.test.ts | 109 -- .../verification_setup_snapshot.test.ts | 39 +- .../__tests__/verification_setup_sync.test.ts | 40 +- src/infrastructure/git_commit_adapter.ts | 3 +- .../native_verification_candidate.ts | 36 + .../native_verification_controller.ts | 185 +++ .../native_verification_overlay.ts | 51 + .../native_verification_profiles.ts | 36 + .../native_verification_protocol.ts | 62 + .../native_verification_redaction.ts | 45 + .../verification_setup_process.ts | 39 - .../verification_setup_runner.ts | 71 +- .../verification_setup_snapshot.ts | 48 +- .../fixtures/verification_native_fixture.ts | 77 ++ 53 files changed, 5795 insertions(+), 823 deletions(-) create mode 100644 .github/actions/copilot-native-fixture/action.yml create mode 100644 scripts/generate-native-verification-workflows.cjs create mode 100644 src/infrastructure/__tests__/native_verification_controller.test.ts create mode 100644 src/infrastructure/__tests__/native_verification_fixture.ts create mode 100644 src/infrastructure/__tests__/native_verification_profiles.test.ts create mode 100644 src/infrastructure/__tests__/native_verification_protocol.test.ts create mode 100644 src/infrastructure/__tests__/native_verification_redaction.test.ts create mode 100644 src/infrastructure/__tests__/native_verification_runner.test.ts create mode 100644 src/infrastructure/__tests__/native_verification_workflow.test.ts create mode 100644 src/infrastructure/native_verification_candidate.ts create mode 100644 src/infrastructure/native_verification_controller.ts create mode 100644 src/infrastructure/native_verification_overlay.ts create mode 100644 src/infrastructure/native_verification_profiles.ts create mode 100644 src/infrastructure/native_verification_protocol.ts create mode 100644 src/infrastructure/native_verification_redaction.ts delete mode 100644 src/infrastructure/verification_setup_process.ts create mode 100644 src/tooling/__tests__/fixtures/verification_native_fixture.ts diff --git a/.copilot/AGENT_GUIDE.md b/.copilot/AGENT_GUIDE.md index 8b5d3e477..a9d7c453e 100644 --- a/.copilot/AGENT_GUIDE.md +++ b/.copilot/AGENT_GUIDE.md @@ -51,6 +51,6 @@ The setup runner profile selects placement; it does not install the project's la Declare reviewed version prefixes in `.copilot/verification-prerequisites.json` (schemaVersion 1, runtimes such as java 17, node 24, pnpm 10). Copilot checks declared versions in the credential-free verification environment before each configured verification command; a missing or incompatible runtime blocks publication. Without this optional declaration, existing verification behavior remains and no language is inferred. Java provisioning may use `actions/setup-java`; `JAVA_HOME` and `PATH` survive isolation, while agent credentials and tool-option injection variables do not. Do not add an unreviewed bootstrap or weaken verification to work around a missing runtime. Read [verification prerequisites and same-job setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites). -The optional repository-owned setup helper prepares each candidate before verification without editing generated workflows. Review `.copilot/verification-setup.mjs` onto trusted main before enabling `VERIFICATION_SETUP=true` (Action input `verification-setup`, disabled by default). Copilot executes captured trusted bytes before each command in a fresh credential-free profile; candidate changes cannot replace them. Setup failure blocks verification and commit/push. Read [repository-owned setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup). +The optional repository-owned composite action prepares each candidate before verification. Review `.github/actions/copilot-verification-setup/action.yml` and its assets onto trusted main, update generated workflows, then enable `VERIFICATION_SETUP=true` (Action input `verification-setup`, disabled by default). GitHub Actions executes the trusted snapshot before each command with a fresh private HOME; candidate changes cannot replace it. Native post steps finish before profile cleanup. Setup failure blocks verification and commit/push. The local CLI cannot execute composite actions and fails closed when opted in. Read [repository-owned setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup). For the full contributor flow, read [repository collaboration](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-collaboration.mdx). For drift and recovery, read [guidance troubleshooting](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-guidance-troubleshooting.mdx). diff --git a/.copilot/setup-manifest.json b/.copilot/setup-manifest.json index d15350be3..4b26a2fb2 100644 --- a/.copilot/setup-manifest.json +++ b/.copilot/setup-manifest.json @@ -12,7 +12,7 @@ }, ".copilot/AGENT_GUIDE.md": { "role": "guide", - "sha256": "26fad3d7f671c3f839a7b62195c0caae5119631d2e0ef848143998c3cc2b7433" + "sha256": "4c453f9e76287c970d56787bd082fc2d7d8d432b191a96a7132e811657905dbc" }, ".copilot/repository-profile.json": { "role": "profile", diff --git a/.github/actions/copilot-native-fixture/action.yml b/.github/actions/copilot-native-fixture/action.yml new file mode 100644 index 000000000..05ddc8acf --- /dev/null +++ b/.github/actions/copilot-native-fixture/action.yml @@ -0,0 +1,10 @@ +name: Copilot native verification acceptance +inputs: + verification-setup: + default: 'true' + description: Exercise the native verification bridge +runs: + using: node24 + main: dist/index.js + post: dist/index.js + post-if: always() diff --git a/.github/workflows/copilot_issue_comment.yml b/.github/workflows/copilot_issue_comment.yml index d02893b7d..28a557a8b 100644 --- a/.github/workflows/copilot_issue_comment.yml +++ b/.github/workflows/copilot_issue_comment.yml @@ -43,7 +43,8 @@ jobs: run: pnpm install --frozen-lockfile --ignore-scripts - uses: ./ - with: + id: copilot + with: &copilot-inputs repository-locale: ${{ vars.REPOSITORY_LOCALE || 'en-US' }} issues-locale: ${{ vars.ISSUES_LOCALE || '' }} pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || '' }} @@ -95,6 +96,7 @@ jobs: bugbot-fail-on-unresolved: ${{ vars.BUGBOT_FAIL_ON_UNRESOLVED || 'false' }} bugbot-organization-rules: ${{ vars.BUGBOT_ORGANIZATION_RULES }} env: + COPILOT_NATIVE_PHASE: prepare AGENT_PROVIDER: ${{ vars.AGENT_PROVIDER || 'codex' }} AGENT_MODEL_PROVIDER: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} AGENT_MODEL: ${{ vars.AGENT_MODEL }} @@ -110,3 +112,514 @@ jobs: CURSOR_API_KEY: ${{ (vars.AGENT_PROVIDER == 'cursor' || vars.FINDINGS_PROVIDER == 'cursor' || vars.FIXER_PROVIDER == 'cursor' || vars.PLANNER_PROVIDER == 'cursor' || vars.REVIEWER_PROVIDER == 'cursor' || vars.TESTER_PROVIDER == 'cursor') && secrets.CURSOR_API_KEY || '' }} CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }} COPILOT_EVIDENCE_TOKEN: ${{ github.token }} + + # copilot:native-verification:start + - name: Prepare repository verification 1 + id: verification_setup_1 + if: ${{ !cancelled() && steps.copilot.outcome == 'success' && steps.copilot.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.copilot.outputs.verification-home }} + USERPROFILE: ${{ steps.copilot.outputs.verification-userprofile }} + APPDATA: ${{ steps.copilot.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.copilot.outputs.verification-localappdata }} + PATH: ${{ steps.copilot.outputs.verification-path }} + JAVA_HOME: ${{ steps.copilot.outputs.verification-java_home }} + TMP: ${{ steps.copilot.outputs.verification-tmp }} + TEMP: ${{ steps.copilot.outputs.verification-temp }} + TMPDIR: ${{ steps.copilot.outputs.verification-tmpdir }} + - name: Continue repository verification 1 + id: verification_continue_1 + if: ${{ always() && steps.copilot.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.copilot.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_1.outcome }} + - name: Prepare repository verification 2 + id: verification_setup_2 + if: ${{ !cancelled() && steps.verification_continue_1.outcome == 'success' && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_1.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_1.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_1.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_1.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_1.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_1.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_1.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_1.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_1.outputs.verification-tmpdir }} + - name: Continue repository verification 2 + id: verification_continue_2 + if: ${{ always() && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_1.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_2.outcome }} + - name: Prepare repository verification 3 + id: verification_setup_3 + if: ${{ !cancelled() && steps.verification_continue_2.outcome == 'success' && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_2.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_2.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_2.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_2.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_2.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_2.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_2.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_2.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_2.outputs.verification-tmpdir }} + - name: Continue repository verification 3 + id: verification_continue_3 + if: ${{ always() && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_2.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_3.outcome }} + - name: Prepare repository verification 4 + id: verification_setup_4 + if: ${{ !cancelled() && steps.verification_continue_3.outcome == 'success' && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_3.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_3.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_3.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_3.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_3.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_3.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_3.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_3.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_3.outputs.verification-tmpdir }} + - name: Continue repository verification 4 + id: verification_continue_4 + if: ${{ always() && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_3.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_4.outcome }} + - name: Prepare repository verification 5 + id: verification_setup_5 + if: ${{ !cancelled() && steps.verification_continue_4.outcome == 'success' && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_4.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_4.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_4.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_4.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_4.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_4.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_4.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_4.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_4.outputs.verification-tmpdir }} + - name: Continue repository verification 5 + id: verification_continue_5 + if: ${{ always() && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_4.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_5.outcome }} + - name: Prepare repository verification 6 + id: verification_setup_6 + if: ${{ !cancelled() && steps.verification_continue_5.outcome == 'success' && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_5.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_5.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_5.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_5.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_5.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_5.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_5.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_5.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_5.outputs.verification-tmpdir }} + - name: Continue repository verification 6 + id: verification_continue_6 + if: ${{ always() && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_5.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_6.outcome }} + - name: Prepare repository verification 7 + id: verification_setup_7 + if: ${{ !cancelled() && steps.verification_continue_6.outcome == 'success' && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_6.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_6.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_6.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_6.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_6.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_6.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_6.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_6.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_6.outputs.verification-tmpdir }} + - name: Continue repository verification 7 + id: verification_continue_7 + if: ${{ always() && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_6.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_7.outcome }} + - name: Prepare repository verification 8 + id: verification_setup_8 + if: ${{ !cancelled() && steps.verification_continue_7.outcome == 'success' && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_7.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_7.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_7.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_7.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_7.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_7.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_7.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_7.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_7.outputs.verification-tmpdir }} + - name: Continue repository verification 8 + id: verification_continue_8 + if: ${{ always() && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_7.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_8.outcome }} + - name: Prepare repository verification 9 + id: verification_setup_9 + if: ${{ !cancelled() && steps.verification_continue_8.outcome == 'success' && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_8.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_8.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_8.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_8.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_8.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_8.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_8.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_8.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_8.outputs.verification-tmpdir }} + - name: Continue repository verification 9 + id: verification_continue_9 + if: ${{ always() && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_8.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_9.outcome }} + - name: Prepare repository verification 10 + id: verification_setup_10 + if: ${{ !cancelled() && steps.verification_continue_9.outcome == 'success' && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_9.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_9.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_9.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_9.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_9.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_9.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_9.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_9.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_9.outputs.verification-tmpdir }} + - name: Continue repository verification 10 + id: verification_continue_10 + if: ${{ always() && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_9.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_10.outcome }} + - name: Prepare repository verification 11 + id: verification_setup_11 + if: ${{ !cancelled() && steps.verification_continue_10.outcome == 'success' && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_10.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_10.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_10.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_10.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_10.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_10.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_10.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_10.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_10.outputs.verification-tmpdir }} + - name: Continue repository verification 11 + id: verification_continue_11 + if: ${{ always() && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_10.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_11.outcome }} + - name: Prepare repository verification 12 + id: verification_setup_12 + if: ${{ !cancelled() && steps.verification_continue_11.outcome == 'success' && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_11.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_11.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_11.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_11.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_11.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_11.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_11.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_11.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_11.outputs.verification-tmpdir }} + - name: Continue repository verification 12 + id: verification_continue_12 + if: ${{ always() && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_11.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_12.outcome }} + - name: Prepare repository verification 13 + id: verification_setup_13 + if: ${{ !cancelled() && steps.verification_continue_12.outcome == 'success' && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_12.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_12.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_12.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_12.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_12.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_12.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_12.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_12.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_12.outputs.verification-tmpdir }} + - name: Continue repository verification 13 + id: verification_continue_13 + if: ${{ always() && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_12.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_13.outcome }} + - name: Prepare repository verification 14 + id: verification_setup_14 + if: ${{ !cancelled() && steps.verification_continue_13.outcome == 'success' && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_13.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_13.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_13.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_13.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_13.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_13.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_13.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_13.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_13.outputs.verification-tmpdir }} + - name: Continue repository verification 14 + id: verification_continue_14 + if: ${{ always() && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_13.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_14.outcome }} + - name: Prepare repository verification 15 + id: verification_setup_15 + if: ${{ !cancelled() && steps.verification_continue_14.outcome == 'success' && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_14.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_14.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_14.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_14.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_14.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_14.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_14.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_14.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_14.outputs.verification-tmpdir }} + - name: Continue repository verification 15 + id: verification_continue_15 + if: ${{ always() && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_14.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_15.outcome }} + - name: Prepare repository verification 16 + id: verification_setup_16 + if: ${{ !cancelled() && steps.verification_continue_15.outcome == 'success' && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_15.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_15.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_15.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_15.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_15.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_15.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_15.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_15.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_15.outputs.verification-tmpdir }} + - name: Continue repository verification 16 + id: verification_continue_16 + if: ${{ always() && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_15.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_16.outcome }} + - name: Prepare repository verification 17 + id: verification_setup_17 + if: ${{ !cancelled() && steps.verification_continue_16.outcome == 'success' && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_16.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_16.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_16.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_16.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_16.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_16.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_16.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_16.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_16.outputs.verification-tmpdir }} + - name: Continue repository verification 17 + id: verification_continue_17 + if: ${{ always() && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_16.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_17.outcome }} + - name: Prepare repository verification 18 + id: verification_setup_18 + if: ${{ !cancelled() && steps.verification_continue_17.outcome == 'success' && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_17.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_17.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_17.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_17.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_17.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_17.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_17.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_17.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_17.outputs.verification-tmpdir }} + - name: Continue repository verification 18 + id: verification_continue_18 + if: ${{ always() && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_17.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_18.outcome }} + - name: Prepare repository verification 19 + id: verification_setup_19 + if: ${{ !cancelled() && steps.verification_continue_18.outcome == 'success' && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_18.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_18.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_18.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_18.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_18.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_18.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_18.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_18.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_18.outputs.verification-tmpdir }} + - name: Continue repository verification 19 + id: verification_continue_19 + if: ${{ always() && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_18.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_19.outcome }} + - name: Prepare repository verification 20 + id: verification_setup_20 + if: ${{ !cancelled() && steps.verification_continue_19.outcome == 'success' && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_19.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_19.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_19.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_19.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_19.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_19.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_19.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_19.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_19.outputs.verification-tmpdir }} + - name: Continue repository verification 20 + id: verification_continue_20 + if: ${{ always() && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_19.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_20.outcome }} + - name: Complete repository verification + id: verification_complete + if: ${{ always() && steps.copilot.outputs.verification-ticket != '' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: finish + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + # copilot:native-verification:end diff --git a/.github/workflows/copilot_pull_request_comment.yml b/.github/workflows/copilot_pull_request_comment.yml index a81024a92..88f786e3a 100644 --- a/.github/workflows/copilot_pull_request_comment.yml +++ b/.github/workflows/copilot_pull_request_comment.yml @@ -43,7 +43,8 @@ jobs: run: pnpm install --frozen-lockfile --ignore-scripts - uses: ./ - with: + id: copilot + with: &copilot-inputs repository-locale: ${{ vars.REPOSITORY_LOCALE || 'en-US' }} issues-locale: ${{ vars.ISSUES_LOCALE || '' }} pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || '' }} @@ -95,6 +96,7 @@ jobs: bugbot-fail-on-unresolved: ${{ vars.BUGBOT_FAIL_ON_UNRESOLVED || 'false' }} bugbot-organization-rules: ${{ vars.BUGBOT_ORGANIZATION_RULES }} env: + COPILOT_NATIVE_PHASE: prepare AGENT_PROVIDER: ${{ vars.AGENT_PROVIDER || 'codex' }} AGENT_MODEL_PROVIDER: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} AGENT_MODEL: ${{ vars.AGENT_MODEL }} @@ -110,3 +112,514 @@ jobs: CURSOR_API_KEY: ${{ (vars.AGENT_PROVIDER == 'cursor' || vars.FINDINGS_PROVIDER == 'cursor' || vars.FIXER_PROVIDER == 'cursor' || vars.PLANNER_PROVIDER == 'cursor' || vars.REVIEWER_PROVIDER == 'cursor' || vars.TESTER_PROVIDER == 'cursor') && secrets.CURSOR_API_KEY || '' }} CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }} COPILOT_EVIDENCE_TOKEN: ${{ github.token }} + + # copilot:native-verification:start + - name: Prepare repository verification 1 + id: verification_setup_1 + if: ${{ !cancelled() && steps.copilot.outcome == 'success' && steps.copilot.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.copilot.outputs.verification-home }} + USERPROFILE: ${{ steps.copilot.outputs.verification-userprofile }} + APPDATA: ${{ steps.copilot.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.copilot.outputs.verification-localappdata }} + PATH: ${{ steps.copilot.outputs.verification-path }} + JAVA_HOME: ${{ steps.copilot.outputs.verification-java_home }} + TMP: ${{ steps.copilot.outputs.verification-tmp }} + TEMP: ${{ steps.copilot.outputs.verification-temp }} + TMPDIR: ${{ steps.copilot.outputs.verification-tmpdir }} + - name: Continue repository verification 1 + id: verification_continue_1 + if: ${{ always() && steps.copilot.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.copilot.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_1.outcome }} + - name: Prepare repository verification 2 + id: verification_setup_2 + if: ${{ !cancelled() && steps.verification_continue_1.outcome == 'success' && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_1.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_1.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_1.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_1.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_1.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_1.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_1.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_1.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_1.outputs.verification-tmpdir }} + - name: Continue repository verification 2 + id: verification_continue_2 + if: ${{ always() && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_1.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_2.outcome }} + - name: Prepare repository verification 3 + id: verification_setup_3 + if: ${{ !cancelled() && steps.verification_continue_2.outcome == 'success' && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_2.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_2.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_2.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_2.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_2.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_2.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_2.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_2.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_2.outputs.verification-tmpdir }} + - name: Continue repository verification 3 + id: verification_continue_3 + if: ${{ always() && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_2.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_3.outcome }} + - name: Prepare repository verification 4 + id: verification_setup_4 + if: ${{ !cancelled() && steps.verification_continue_3.outcome == 'success' && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_3.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_3.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_3.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_3.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_3.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_3.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_3.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_3.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_3.outputs.verification-tmpdir }} + - name: Continue repository verification 4 + id: verification_continue_4 + if: ${{ always() && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_3.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_4.outcome }} + - name: Prepare repository verification 5 + id: verification_setup_5 + if: ${{ !cancelled() && steps.verification_continue_4.outcome == 'success' && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_4.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_4.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_4.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_4.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_4.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_4.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_4.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_4.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_4.outputs.verification-tmpdir }} + - name: Continue repository verification 5 + id: verification_continue_5 + if: ${{ always() && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_4.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_5.outcome }} + - name: Prepare repository verification 6 + id: verification_setup_6 + if: ${{ !cancelled() && steps.verification_continue_5.outcome == 'success' && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_5.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_5.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_5.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_5.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_5.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_5.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_5.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_5.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_5.outputs.verification-tmpdir }} + - name: Continue repository verification 6 + id: verification_continue_6 + if: ${{ always() && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_5.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_6.outcome }} + - name: Prepare repository verification 7 + id: verification_setup_7 + if: ${{ !cancelled() && steps.verification_continue_6.outcome == 'success' && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_6.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_6.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_6.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_6.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_6.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_6.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_6.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_6.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_6.outputs.verification-tmpdir }} + - name: Continue repository verification 7 + id: verification_continue_7 + if: ${{ always() && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_6.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_7.outcome }} + - name: Prepare repository verification 8 + id: verification_setup_8 + if: ${{ !cancelled() && steps.verification_continue_7.outcome == 'success' && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_7.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_7.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_7.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_7.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_7.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_7.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_7.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_7.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_7.outputs.verification-tmpdir }} + - name: Continue repository verification 8 + id: verification_continue_8 + if: ${{ always() && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_7.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_8.outcome }} + - name: Prepare repository verification 9 + id: verification_setup_9 + if: ${{ !cancelled() && steps.verification_continue_8.outcome == 'success' && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_8.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_8.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_8.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_8.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_8.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_8.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_8.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_8.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_8.outputs.verification-tmpdir }} + - name: Continue repository verification 9 + id: verification_continue_9 + if: ${{ always() && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_8.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_9.outcome }} + - name: Prepare repository verification 10 + id: verification_setup_10 + if: ${{ !cancelled() && steps.verification_continue_9.outcome == 'success' && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_9.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_9.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_9.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_9.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_9.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_9.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_9.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_9.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_9.outputs.verification-tmpdir }} + - name: Continue repository verification 10 + id: verification_continue_10 + if: ${{ always() && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_9.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_10.outcome }} + - name: Prepare repository verification 11 + id: verification_setup_11 + if: ${{ !cancelled() && steps.verification_continue_10.outcome == 'success' && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_10.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_10.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_10.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_10.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_10.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_10.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_10.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_10.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_10.outputs.verification-tmpdir }} + - name: Continue repository verification 11 + id: verification_continue_11 + if: ${{ always() && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_10.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_11.outcome }} + - name: Prepare repository verification 12 + id: verification_setup_12 + if: ${{ !cancelled() && steps.verification_continue_11.outcome == 'success' && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_11.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_11.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_11.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_11.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_11.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_11.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_11.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_11.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_11.outputs.verification-tmpdir }} + - name: Continue repository verification 12 + id: verification_continue_12 + if: ${{ always() && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_11.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_12.outcome }} + - name: Prepare repository verification 13 + id: verification_setup_13 + if: ${{ !cancelled() && steps.verification_continue_12.outcome == 'success' && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_12.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_12.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_12.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_12.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_12.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_12.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_12.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_12.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_12.outputs.verification-tmpdir }} + - name: Continue repository verification 13 + id: verification_continue_13 + if: ${{ always() && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_12.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_13.outcome }} + - name: Prepare repository verification 14 + id: verification_setup_14 + if: ${{ !cancelled() && steps.verification_continue_13.outcome == 'success' && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_13.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_13.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_13.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_13.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_13.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_13.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_13.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_13.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_13.outputs.verification-tmpdir }} + - name: Continue repository verification 14 + id: verification_continue_14 + if: ${{ always() && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_13.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_14.outcome }} + - name: Prepare repository verification 15 + id: verification_setup_15 + if: ${{ !cancelled() && steps.verification_continue_14.outcome == 'success' && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_14.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_14.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_14.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_14.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_14.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_14.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_14.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_14.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_14.outputs.verification-tmpdir }} + - name: Continue repository verification 15 + id: verification_continue_15 + if: ${{ always() && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_14.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_15.outcome }} + - name: Prepare repository verification 16 + id: verification_setup_16 + if: ${{ !cancelled() && steps.verification_continue_15.outcome == 'success' && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_15.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_15.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_15.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_15.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_15.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_15.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_15.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_15.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_15.outputs.verification-tmpdir }} + - name: Continue repository verification 16 + id: verification_continue_16 + if: ${{ always() && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_15.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_16.outcome }} + - name: Prepare repository verification 17 + id: verification_setup_17 + if: ${{ !cancelled() && steps.verification_continue_16.outcome == 'success' && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_16.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_16.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_16.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_16.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_16.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_16.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_16.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_16.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_16.outputs.verification-tmpdir }} + - name: Continue repository verification 17 + id: verification_continue_17 + if: ${{ always() && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_16.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_17.outcome }} + - name: Prepare repository verification 18 + id: verification_setup_18 + if: ${{ !cancelled() && steps.verification_continue_17.outcome == 'success' && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_17.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_17.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_17.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_17.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_17.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_17.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_17.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_17.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_17.outputs.verification-tmpdir }} + - name: Continue repository verification 18 + id: verification_continue_18 + if: ${{ always() && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_17.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_18.outcome }} + - name: Prepare repository verification 19 + id: verification_setup_19 + if: ${{ !cancelled() && steps.verification_continue_18.outcome == 'success' && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_18.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_18.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_18.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_18.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_18.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_18.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_18.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_18.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_18.outputs.verification-tmpdir }} + - name: Continue repository verification 19 + id: verification_continue_19 + if: ${{ always() && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_18.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_19.outcome }} + - name: Prepare repository verification 20 + id: verification_setup_20 + if: ${{ !cancelled() && steps.verification_continue_19.outcome == 'success' && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_19.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_19.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_19.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_19.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_19.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_19.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_19.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_19.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_19.outputs.verification-tmpdir }} + - name: Continue repository verification 20 + id: verification_continue_20 + if: ${{ always() && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_19.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_20.outcome }} + - name: Complete repository verification + id: verification_complete + if: ${{ always() && steps.copilot.outputs.verification-ticket != '' }} + uses: ./ + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: finish + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + # copilot:native-verification:end diff --git a/.github/workflows/setup_platform_smoke.yml b/.github/workflows/setup_platform_smoke.yml index 251118abb..93ea203b6 100644 --- a/.github/workflows/setup_platform_smoke.yml +++ b/.github/workflows/setup_platform_smoke.yml @@ -63,6 +63,523 @@ jobs: if: runner.os == 'Windows' run: node scripts/verify-windows-job-npm.cjs - run: pnpm install --frozen-lockfile + - name: Build native verification acceptance fixture + run: pnpm exec ncc build src/tooling/__tests__/fixtures/verification_native_fixture.ts -o .github/actions/copilot-native-fixture/dist + - name: Prepare native verification acceptance + id: copilot_native_fixture + uses: ./.github/actions/copilot-native-fixture + with: &native-fixture-inputs + verification-setup: 'true' + env: + COPILOT_NATIVE_PHASE: prepare + - name: Prepare repository verification 1 + id: verification_setup_1 + if: ${{ !cancelled() && steps.copilot_native_fixture.outcome == 'success' && steps.copilot_native_fixture.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.copilot_native_fixture.outputs.verification-home }} + USERPROFILE: ${{ steps.copilot_native_fixture.outputs.verification-userprofile }} + APPDATA: ${{ steps.copilot_native_fixture.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.copilot_native_fixture.outputs.verification-localappdata }} + PATH: ${{ steps.copilot_native_fixture.outputs.verification-path }} + JAVA_HOME: ${{ steps.copilot_native_fixture.outputs.verification-java_home }} + TMP: ${{ steps.copilot_native_fixture.outputs.verification-tmp }} + TEMP: ${{ steps.copilot_native_fixture.outputs.verification-temp }} + TMPDIR: ${{ steps.copilot_native_fixture.outputs.verification-tmpdir }} + - name: Continue repository verification 1 + id: verification_continue_1 + if: ${{ always() && steps.copilot_native_fixture.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.copilot_native_fixture.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_1.outcome }} + - name: Prepare repository verification 2 + id: verification_setup_2 + if: ${{ !cancelled() && steps.verification_continue_1.outcome == 'success' && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_1.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_1.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_1.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_1.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_1.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_1.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_1.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_1.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_1.outputs.verification-tmpdir }} + - name: Continue repository verification 2 + id: verification_continue_2 + if: ${{ always() && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_1.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_2.outcome }} + - name: Prepare repository verification 3 + id: verification_setup_3 + if: ${{ !cancelled() && steps.verification_continue_2.outcome == 'success' && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_2.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_2.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_2.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_2.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_2.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_2.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_2.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_2.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_2.outputs.verification-tmpdir }} + - name: Continue repository verification 3 + id: verification_continue_3 + if: ${{ always() && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_2.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_3.outcome }} + - name: Prepare repository verification 4 + id: verification_setup_4 + if: ${{ !cancelled() && steps.verification_continue_3.outcome == 'success' && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_3.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_3.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_3.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_3.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_3.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_3.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_3.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_3.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_3.outputs.verification-tmpdir }} + - name: Continue repository verification 4 + id: verification_continue_4 + if: ${{ always() && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_3.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_4.outcome }} + - name: Prepare repository verification 5 + id: verification_setup_5 + if: ${{ !cancelled() && steps.verification_continue_4.outcome == 'success' && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_4.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_4.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_4.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_4.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_4.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_4.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_4.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_4.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_4.outputs.verification-tmpdir }} + - name: Continue repository verification 5 + id: verification_continue_5 + if: ${{ always() && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_4.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_5.outcome }} + - name: Prepare repository verification 6 + id: verification_setup_6 + if: ${{ !cancelled() && steps.verification_continue_5.outcome == 'success' && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_5.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_5.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_5.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_5.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_5.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_5.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_5.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_5.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_5.outputs.verification-tmpdir }} + - name: Continue repository verification 6 + id: verification_continue_6 + if: ${{ always() && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_5.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_6.outcome }} + - name: Prepare repository verification 7 + id: verification_setup_7 + if: ${{ !cancelled() && steps.verification_continue_6.outcome == 'success' && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_6.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_6.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_6.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_6.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_6.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_6.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_6.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_6.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_6.outputs.verification-tmpdir }} + - name: Continue repository verification 7 + id: verification_continue_7 + if: ${{ always() && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_6.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_7.outcome }} + - name: Prepare repository verification 8 + id: verification_setup_8 + if: ${{ !cancelled() && steps.verification_continue_7.outcome == 'success' && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_7.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_7.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_7.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_7.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_7.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_7.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_7.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_7.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_7.outputs.verification-tmpdir }} + - name: Continue repository verification 8 + id: verification_continue_8 + if: ${{ always() && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_7.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_8.outcome }} + - name: Prepare repository verification 9 + id: verification_setup_9 + if: ${{ !cancelled() && steps.verification_continue_8.outcome == 'success' && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_8.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_8.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_8.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_8.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_8.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_8.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_8.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_8.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_8.outputs.verification-tmpdir }} + - name: Continue repository verification 9 + id: verification_continue_9 + if: ${{ always() && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_8.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_9.outcome }} + - name: Prepare repository verification 10 + id: verification_setup_10 + if: ${{ !cancelled() && steps.verification_continue_9.outcome == 'success' && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_9.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_9.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_9.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_9.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_9.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_9.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_9.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_9.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_9.outputs.verification-tmpdir }} + - name: Continue repository verification 10 + id: verification_continue_10 + if: ${{ always() && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_9.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_10.outcome }} + - name: Prepare repository verification 11 + id: verification_setup_11 + if: ${{ !cancelled() && steps.verification_continue_10.outcome == 'success' && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_10.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_10.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_10.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_10.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_10.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_10.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_10.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_10.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_10.outputs.verification-tmpdir }} + - name: Continue repository verification 11 + id: verification_continue_11 + if: ${{ always() && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_10.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_11.outcome }} + - name: Prepare repository verification 12 + id: verification_setup_12 + if: ${{ !cancelled() && steps.verification_continue_11.outcome == 'success' && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_11.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_11.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_11.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_11.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_11.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_11.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_11.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_11.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_11.outputs.verification-tmpdir }} + - name: Continue repository verification 12 + id: verification_continue_12 + if: ${{ always() && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_11.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_12.outcome }} + - name: Prepare repository verification 13 + id: verification_setup_13 + if: ${{ !cancelled() && steps.verification_continue_12.outcome == 'success' && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_12.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_12.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_12.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_12.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_12.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_12.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_12.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_12.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_12.outputs.verification-tmpdir }} + - name: Continue repository verification 13 + id: verification_continue_13 + if: ${{ always() && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_12.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_13.outcome }} + - name: Prepare repository verification 14 + id: verification_setup_14 + if: ${{ !cancelled() && steps.verification_continue_13.outcome == 'success' && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_13.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_13.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_13.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_13.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_13.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_13.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_13.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_13.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_13.outputs.verification-tmpdir }} + - name: Continue repository verification 14 + id: verification_continue_14 + if: ${{ always() && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_13.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_14.outcome }} + - name: Prepare repository verification 15 + id: verification_setup_15 + if: ${{ !cancelled() && steps.verification_continue_14.outcome == 'success' && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_14.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_14.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_14.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_14.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_14.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_14.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_14.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_14.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_14.outputs.verification-tmpdir }} + - name: Continue repository verification 15 + id: verification_continue_15 + if: ${{ always() && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_14.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_15.outcome }} + - name: Prepare repository verification 16 + id: verification_setup_16 + if: ${{ !cancelled() && steps.verification_continue_15.outcome == 'success' && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_15.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_15.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_15.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_15.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_15.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_15.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_15.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_15.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_15.outputs.verification-tmpdir }} + - name: Continue repository verification 16 + id: verification_continue_16 + if: ${{ always() && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_15.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_16.outcome }} + - name: Prepare repository verification 17 + id: verification_setup_17 + if: ${{ !cancelled() && steps.verification_continue_16.outcome == 'success' && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_16.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_16.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_16.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_16.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_16.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_16.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_16.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_16.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_16.outputs.verification-tmpdir }} + - name: Continue repository verification 17 + id: verification_continue_17 + if: ${{ always() && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_16.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_17.outcome }} + - name: Prepare repository verification 18 + id: verification_setup_18 + if: ${{ !cancelled() && steps.verification_continue_17.outcome == 'success' && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_17.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_17.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_17.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_17.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_17.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_17.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_17.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_17.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_17.outputs.verification-tmpdir }} + - name: Continue repository verification 18 + id: verification_continue_18 + if: ${{ always() && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_17.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_18.outcome }} + - name: Prepare repository verification 19 + id: verification_setup_19 + if: ${{ !cancelled() && steps.verification_continue_18.outcome == 'success' && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_18.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_18.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_18.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_18.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_18.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_18.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_18.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_18.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_18.outputs.verification-tmpdir }} + - name: Continue repository verification 19 + id: verification_continue_19 + if: ${{ always() && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_18.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_19.outcome }} + - name: Prepare repository verification 20 + id: verification_setup_20 + if: ${{ !cancelled() && steps.verification_continue_19.outcome == 'success' && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_19.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_19.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_19.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_19.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_19.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_19.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_19.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_19.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_19.outputs.verification-tmpdir }} + - name: Continue repository verification 20 + id: verification_continue_20 + if: ${{ always() && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_19.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_20.outcome }} + - name: Complete repository verification + id: verification_complete + if: ${{ always() && steps.copilot_native_fixture.outputs.verification-ticket != '' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: finish + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} - run: pnpm run build # Checked-in ncc bundles are generated on Unix; Windows output differs. - if: runner.os != 'Windows' @@ -157,6 +674,523 @@ jobs: run: | git ls-files --eol -- .copilot/repository-profile.json .copilot/AGENT_GUIDE.md .agents/skills/copilot-repository-workflow/SKILL.md pnpm run validate:agent-docs + - name: Build native verification acceptance fixture + run: pnpm exec ncc build src/tooling/__tests__/fixtures/verification_native_fixture.ts -o .github/actions/copilot-native-fixture/dist + - name: Prepare native verification acceptance + id: copilot_native_fixture + uses: ./.github/actions/copilot-native-fixture + with: &native-fixture-inputs + verification-setup: 'true' + env: + COPILOT_NATIVE_PHASE: prepare + - name: Prepare repository verification 1 + id: verification_setup_1 + if: ${{ !cancelled() && steps.copilot_native_fixture.outcome == 'success' && steps.copilot_native_fixture.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.copilot_native_fixture.outputs.verification-home }} + USERPROFILE: ${{ steps.copilot_native_fixture.outputs.verification-userprofile }} + APPDATA: ${{ steps.copilot_native_fixture.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.copilot_native_fixture.outputs.verification-localappdata }} + PATH: ${{ steps.copilot_native_fixture.outputs.verification-path }} + JAVA_HOME: ${{ steps.copilot_native_fixture.outputs.verification-java_home }} + TMP: ${{ steps.copilot_native_fixture.outputs.verification-tmp }} + TEMP: ${{ steps.copilot_native_fixture.outputs.verification-temp }} + TMPDIR: ${{ steps.copilot_native_fixture.outputs.verification-tmpdir }} + - name: Continue repository verification 1 + id: verification_continue_1 + if: ${{ always() && steps.copilot_native_fixture.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.copilot_native_fixture.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_1.outcome }} + - name: Prepare repository verification 2 + id: verification_setup_2 + if: ${{ !cancelled() && steps.verification_continue_1.outcome == 'success' && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_1.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_1.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_1.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_1.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_1.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_1.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_1.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_1.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_1.outputs.verification-tmpdir }} + - name: Continue repository verification 2 + id: verification_continue_2 + if: ${{ always() && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_1.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_2.outcome }} + - name: Prepare repository verification 3 + id: verification_setup_3 + if: ${{ !cancelled() && steps.verification_continue_2.outcome == 'success' && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_2.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_2.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_2.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_2.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_2.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_2.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_2.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_2.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_2.outputs.verification-tmpdir }} + - name: Continue repository verification 3 + id: verification_continue_3 + if: ${{ always() && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_2.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_3.outcome }} + - name: Prepare repository verification 4 + id: verification_setup_4 + if: ${{ !cancelled() && steps.verification_continue_3.outcome == 'success' && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_3.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_3.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_3.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_3.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_3.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_3.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_3.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_3.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_3.outputs.verification-tmpdir }} + - name: Continue repository verification 4 + id: verification_continue_4 + if: ${{ always() && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_3.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_4.outcome }} + - name: Prepare repository verification 5 + id: verification_setup_5 + if: ${{ !cancelled() && steps.verification_continue_4.outcome == 'success' && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_4.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_4.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_4.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_4.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_4.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_4.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_4.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_4.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_4.outputs.verification-tmpdir }} + - name: Continue repository verification 5 + id: verification_continue_5 + if: ${{ always() && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_4.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_5.outcome }} + - name: Prepare repository verification 6 + id: verification_setup_6 + if: ${{ !cancelled() && steps.verification_continue_5.outcome == 'success' && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_5.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_5.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_5.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_5.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_5.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_5.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_5.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_5.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_5.outputs.verification-tmpdir }} + - name: Continue repository verification 6 + id: verification_continue_6 + if: ${{ always() && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_5.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_6.outcome }} + - name: Prepare repository verification 7 + id: verification_setup_7 + if: ${{ !cancelled() && steps.verification_continue_6.outcome == 'success' && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_6.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_6.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_6.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_6.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_6.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_6.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_6.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_6.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_6.outputs.verification-tmpdir }} + - name: Continue repository verification 7 + id: verification_continue_7 + if: ${{ always() && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_6.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_7.outcome }} + - name: Prepare repository verification 8 + id: verification_setup_8 + if: ${{ !cancelled() && steps.verification_continue_7.outcome == 'success' && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_7.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_7.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_7.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_7.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_7.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_7.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_7.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_7.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_7.outputs.verification-tmpdir }} + - name: Continue repository verification 8 + id: verification_continue_8 + if: ${{ always() && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_7.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_8.outcome }} + - name: Prepare repository verification 9 + id: verification_setup_9 + if: ${{ !cancelled() && steps.verification_continue_8.outcome == 'success' && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_8.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_8.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_8.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_8.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_8.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_8.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_8.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_8.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_8.outputs.verification-tmpdir }} + - name: Continue repository verification 9 + id: verification_continue_9 + if: ${{ always() && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_8.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_9.outcome }} + - name: Prepare repository verification 10 + id: verification_setup_10 + if: ${{ !cancelled() && steps.verification_continue_9.outcome == 'success' && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_9.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_9.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_9.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_9.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_9.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_9.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_9.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_9.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_9.outputs.verification-tmpdir }} + - name: Continue repository verification 10 + id: verification_continue_10 + if: ${{ always() && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_9.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_10.outcome }} + - name: Prepare repository verification 11 + id: verification_setup_11 + if: ${{ !cancelled() && steps.verification_continue_10.outcome == 'success' && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_10.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_10.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_10.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_10.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_10.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_10.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_10.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_10.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_10.outputs.verification-tmpdir }} + - name: Continue repository verification 11 + id: verification_continue_11 + if: ${{ always() && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_10.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_11.outcome }} + - name: Prepare repository verification 12 + id: verification_setup_12 + if: ${{ !cancelled() && steps.verification_continue_11.outcome == 'success' && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_11.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_11.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_11.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_11.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_11.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_11.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_11.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_11.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_11.outputs.verification-tmpdir }} + - name: Continue repository verification 12 + id: verification_continue_12 + if: ${{ always() && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_11.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_12.outcome }} + - name: Prepare repository verification 13 + id: verification_setup_13 + if: ${{ !cancelled() && steps.verification_continue_12.outcome == 'success' && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_12.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_12.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_12.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_12.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_12.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_12.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_12.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_12.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_12.outputs.verification-tmpdir }} + - name: Continue repository verification 13 + id: verification_continue_13 + if: ${{ always() && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_12.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_13.outcome }} + - name: Prepare repository verification 14 + id: verification_setup_14 + if: ${{ !cancelled() && steps.verification_continue_13.outcome == 'success' && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_13.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_13.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_13.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_13.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_13.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_13.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_13.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_13.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_13.outputs.verification-tmpdir }} + - name: Continue repository verification 14 + id: verification_continue_14 + if: ${{ always() && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_13.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_14.outcome }} + - name: Prepare repository verification 15 + id: verification_setup_15 + if: ${{ !cancelled() && steps.verification_continue_14.outcome == 'success' && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_14.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_14.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_14.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_14.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_14.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_14.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_14.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_14.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_14.outputs.verification-tmpdir }} + - name: Continue repository verification 15 + id: verification_continue_15 + if: ${{ always() && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_14.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_15.outcome }} + - name: Prepare repository verification 16 + id: verification_setup_16 + if: ${{ !cancelled() && steps.verification_continue_15.outcome == 'success' && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_15.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_15.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_15.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_15.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_15.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_15.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_15.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_15.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_15.outputs.verification-tmpdir }} + - name: Continue repository verification 16 + id: verification_continue_16 + if: ${{ always() && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_15.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_16.outcome }} + - name: Prepare repository verification 17 + id: verification_setup_17 + if: ${{ !cancelled() && steps.verification_continue_16.outcome == 'success' && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_16.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_16.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_16.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_16.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_16.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_16.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_16.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_16.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_16.outputs.verification-tmpdir }} + - name: Continue repository verification 17 + id: verification_continue_17 + if: ${{ always() && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_16.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_17.outcome }} + - name: Prepare repository verification 18 + id: verification_setup_18 + if: ${{ !cancelled() && steps.verification_continue_17.outcome == 'success' && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_17.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_17.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_17.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_17.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_17.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_17.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_17.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_17.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_17.outputs.verification-tmpdir }} + - name: Continue repository verification 18 + id: verification_continue_18 + if: ${{ always() && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_17.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_18.outcome }} + - name: Prepare repository verification 19 + id: verification_setup_19 + if: ${{ !cancelled() && steps.verification_continue_18.outcome == 'success' && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_18.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_18.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_18.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_18.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_18.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_18.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_18.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_18.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_18.outputs.verification-tmpdir }} + - name: Continue repository verification 19 + id: verification_continue_19 + if: ${{ always() && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_18.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_19.outcome }} + - name: Prepare repository verification 20 + id: verification_setup_20 + if: ${{ !cancelled() && steps.verification_continue_19.outcome == 'success' && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_19.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_19.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_19.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_19.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_19.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_19.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_19.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_19.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_19.outputs.verification-tmpdir }} + - name: Continue repository verification 20 + id: verification_continue_20 + if: ${{ always() && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_19.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_20.outcome }} + - name: Complete repository verification + id: verification_complete + if: ${{ always() && steps.copilot_native_fixture.outputs.verification-ticket != '' }} + uses: ./.github/actions/copilot-native-fixture + with: *native-fixture-inputs + env: + COPILOT_NATIVE_PHASE: finish + COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} - run: pnpm run build - run: pnpm run validate:build - run: pnpm run typecheck diff --git a/.gitignore b/.gitignore index eb924bdb6..80f4da40e 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,6 @@ opencode.json # Python analysis tooling __pycache__/ + +# Native Actions acceptance bundle is built only by the platform smoke job. +.github/actions/copilot-native-fixture/dist/ diff --git a/action.yml b/action.yml index ce75110fc..2afacc5af 100644 --- a/action.yml +++ b/action.yml @@ -54,7 +54,7 @@ inputs: description: "Require a validated SDD first commit for started feature and contract-change issues. Requires issue-managed-branches." default: "false" verification-setup: - description: "Run the trusted main-branch .copilot/verification-setup.mjs before each repository verification command. Disabled by default." + description: "Run the trusted repository composite action before repository verification in generated workflows. Disabled by default." default: "false" issue-workflow-profile: description: "JSON profile of enabled issue workflows." @@ -561,6 +561,8 @@ outputs: runs: using: "node24" main: "build/github_action/index.js" + post: "build/github_action/index.js" + post-if: "always()" branding: icon: "cpu" color: "blue" diff --git a/build/cli/index.js b/build/cli/index.js index b0eaca494..90e58e2dc 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -46262,7 +46262,7 @@ The setup runner profile selects placement; it does not install the project's la Declare reviewed version prefixes in \`.copilot/verification-prerequisites.json\` (schemaVersion 1, runtimes such as java 17, node 24, pnpm 10). Copilot checks declared versions in the credential-free verification environment before each configured verification command; a missing or incompatible runtime blocks publication. Without this optional declaration, existing verification behavior remains and no language is inferred. Java provisioning may use \`actions/setup-java\`; \`JAVA_HOME\` and \`PATH\` survive isolation, while agent credentials and tool-option injection variables do not. Do not add an unreviewed bootstrap or weaken verification to work around a missing runtime. Read [verification prerequisites and same-job setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites). -The optional repository-owned setup helper prepares each candidate before verification without editing generated workflows. Review \`.copilot/verification-setup.mjs\` onto trusted main before enabling \`VERIFICATION_SETUP=true\` (Action input \`verification-setup\`, disabled by default). Copilot executes captured trusted bytes before each command in a fresh credential-free profile; candidate changes cannot replace them. Setup failure blocks verification and commit/push. Read [repository-owned setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup). +The optional repository-owned composite action prepares each candidate before verification. Review \`.github/actions/copilot-verification-setup/action.yml\` and its assets onto trusted main, update generated workflows, then enable \`VERIFICATION_SETUP=true\` (Action input \`verification-setup\`, disabled by default). GitHub Actions executes the trusted snapshot before each command with a fresh private HOME; candidate changes cannot replace it. Native post steps finish before profile cleanup. Setup failure blocks verification and commit/push. The local CLI cannot execute composite actions and fails closed when opted in. Read [repository-owned setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup). For the full contributor flow, read [repository collaboration](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-collaboration.mdx). For drift and recovery, read [guidance troubleshooting](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-guidance-troubleshooting.mdx). `; @@ -53396,17 +53396,21 @@ function verificationRuntimeMatches(required, observed) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.VerificationSetupError = exports.VERIFICATION_SETUP_HELP = exports.VERIFICATION_SETUP_LIMITS = exports.VERIFICATION_SETUP_PATH = void 0; +exports.VerificationSetupError = exports.VERIFICATION_SETUP_HELP = exports.VERIFICATION_SETUP_LIMITS = exports.VERIFICATION_SETUP_PATH = exports.VERIFICATION_SETUP_DIRECTORY = void 0; exports.parseVerificationSetupResult = parseVerificationSetupResult; -exports.VERIFICATION_SETUP_PATH = '.copilot/verification-setup.mjs'; -exports.VERIFICATION_SETUP_LIMITS = Object.freeze({ timeoutMs: 300000, outputBytes: 65536, resultBytes: 16384, scriptBytes: 65536 }); +exports.VERIFICATION_SETUP_DIRECTORY = '.github/actions/copilot-verification-setup'; +exports.VERIFICATION_SETUP_PATH = `${exports.VERIFICATION_SETUP_DIRECTORY}/action.yml`; +exports.VERIFICATION_SETUP_LIMITS = Object.freeze({ timeoutMs: 300000, resultBytes: 16384, scriptBytes: 65536, snapshotBytes: 262144, files: 32, requests: 20 }); exports.VERIFICATION_SETUP_HELP = 'https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup'; /** Closed facts only. Provider/helper output and raw exception causes never cross this boundary. */ class VerificationSetupError extends Error { constructor(reason, sourceSha, exitCode) { const sha = sourceSha && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(sourceSha) ? sourceSha : 'unavailable'; const exit = Number.isInteger(exitCode) && exitCode >= 0 && exitCode <= 255 ? ` Exit status: ${exitCode}.` : ''; - super(`Repository setup blocked verification (${reason}). Review ${exports.VERIFICATION_SETUP_PATH} on the trusted main branch and rerun. Trusted source SHA: ${sha}.${exit} Verification, commit and push are withheld. Help: ${exports.VERIFICATION_SETUP_HELP}`); + const action = reason === 'native-workflow-required' + ? 'Update the generated Copilot workflows and run this operation in GitHub Actions; the local CLI cannot execute composite actions.' + : `Review ${exports.VERIFICATION_SETUP_PATH} on the trusted main branch and rerun.`; + super(`Repository setup blocked verification (${reason}). ${action} Trusted source SHA: ${sha}.${exit} Verification, commit and push are withheld. Help: ${exports.VERIFICATION_SETUP_HELP}`); this.reason = reason; this.exitCode = exitCode; this.name = 'VerificationSetupError'; @@ -97207,6 +97211,7 @@ var __importStar = (this && this.__importStar) || (function () { })(); Object.defineProperty(exports, "__esModule", ({ value: true })); exports.GitCommitAdapter = void 0; +const native_verification_profiles_1 = __nccwpck_require__(22290); const exec = __importStar(__nccwpck_require__(18538)); const git_authentication_environment_1 = __nccwpck_require__(16535); const untrusted_command_environment_1 = __nccwpck_require__(2304); @@ -97247,7 +97252,8 @@ class GitCommitAdapter { }); } finally { - runtime.cleanup(); + if (!(0, native_verification_profiles_1.nativeVerificationRetainsProfile)(runtime.environment.HOME)) + runtime.cleanup(); } } async configureAuthor(name, email) { @@ -98235,6 +98241,255 @@ class LoggerWorkflowPollingObserverAdapter { exports.LoggerWorkflowPollingObserverAdapter = LoggerWorkflowPollingObserverAdapter; +/***/ }), + +/***/ 18883: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.verificationCandidateIdentity = verificationCandidateIdentity; +const node_child_process_1 = __nccwpck_require__(17718); +const node_crypto_1 = __nccwpck_require__(6005); +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const verification_setup_policy_1 = __nccwpck_require__(65326); +/** Bind setup to the exact HEAD, branch, index, tracked edits and publishable untracked files. */ +function verificationCandidateIdentity(workspace, environment) { + const hash = (0, node_crypto_1.createHash)('sha256'); + const git = (...args) => { + const result = (0, node_child_process_1.spawnSync)('git', ['--no-replace-objects', ...args], { cwd: workspace, env: environment, shell: false, + windowsHide: true, timeout: 10000, maxBuffer: 16 * 1024 * 1024 }); + if (result.error || result.status !== 0) + throw new verification_setup_policy_1.VerificationSetupError('candidate-changed'); + hash.update(result.stdout).update('\0'); + return result.stdout; + }; + git('rev-parse', '--verify', 'HEAD'); + git('rev-parse', '--symbolic-full-name', 'HEAD'); + git('ls-files', '--stage', '-z'); + git('diff', '--no-ext-diff', '--no-textconv', '--binary', 'HEAD', '--'); + const paths = git('ls-files', '--others', '--exclude-standard', '-z').toString('utf8').split('\0').filter(Boolean); + let total = 0; + if (paths.length > 256) + throw new verification_setup_policy_1.VerificationSetupError('candidate-changed'); + for (const path of paths) { + const file = (0, node_path_1.join)(workspace, path); + const stat = (0, node_fs_1.lstatSync)(file); + total += stat.size; + if (total > 16 * 1024 * 1024) + throw new verification_setup_policy_1.VerificationSetupError('candidate-changed'); + hash.update(path).update('\0'); + if (stat.isSymbolicLink()) + hash.update((0, node_fs_1.readlinkSync)(file)); + else if (stat.isFile()) + hash.update((0, node_fs_1.readFileSync)(file)); + else + throw new verification_setup_policy_1.VerificationSetupError('candidate-changed'); + hash.update('\0'); + } + return hash.digest('hex'); +} + + +/***/ }), + +/***/ 64378: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.stageNativeVerificationAction = stageNativeVerificationAction; +exports.restoreNativeVerificationAction = restoreNativeVerificationAction; +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const verification_setup_policy_1 = __nccwpck_require__(65326); +const native_verification_protocol_1 = __nccwpck_require__(88709); +/** A temporary reviewed action overlays the candidate path only while the operation is paused. */ +function stageNativeVerificationAction(setup, session) { + const target = (0, node_path_1.join)(setup.workspace, verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY); + const backup = (0, node_path_1.join)(session, 'candidate-action'); + const created = []; + let parent = setup.workspace; + for (const part of verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY.split('/').slice(0, -1)) { + parent = (0, node_path_1.join)(parent, part); + if ((0, node_fs_1.existsSync)(parent)) { + if ((0, node_fs_1.lstatSync)(parent).isSymbolicLink() || !(0, node_fs_1.lstatSync)(parent).isDirectory() || (0, node_fs_1.realpathSync)(parent) !== parent) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + } + else { + (0, node_fs_1.mkdirSync)(parent, { mode: 0o700 }); + created.push(parent); + } + } + const targetStat = (0, node_fs_1.lstatSync)(target, { throwIfNoEntry: false }); + const existed = !!targetStat; + if (targetStat && (targetStat.isSymbolicLink() || !targetStat.isDirectory())) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + const record = { target, backup, existed, created }; + (0, native_verification_protocol_1.writeNativeRecord)(session, 'overlay.json', record); + if (existed) + (0, node_fs_1.renameSync)(target, backup); + (0, node_fs_1.mkdirSync)(target, { mode: 0o700 }); + for (const file of setup.files) { + const destination = (0, node_path_1.join)(target, file.path); + const remainder = (0, node_path_1.relative)(target, destination); + if (!remainder || remainder.startsWith(`..${node_path_1.sep}`) || remainder === '..') + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(destination), { recursive: true, mode: 0o700 }); + (0, node_fs_1.writeFileSync)(destination, Buffer.from(file.base64, 'base64'), { mode: file.executable ? 0o700 : 0o600, flag: 'wx' }); + } +} +function restoreNativeVerificationAction(session, workspace) { + const record = (0, native_verification_protocol_1.readNativeRecord)(session, 'overlay.json'); + if (!record) + return; + if (record.target !== (0, node_path_1.join)(workspace, verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY) || record.backup !== (0, node_path_1.join)(session, 'candidate-action') + || !Array.isArray(record.created) || record.created.some(path => ![(0, node_path_1.join)(workspace, '.github'), (0, node_path_1.join)(workspace, '.github/actions')].includes(path))) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + for (const parent of [(0, node_path_1.join)(workspace, '.github'), (0, node_path_1.join)(workspace, '.github/actions')]) { + if (!(0, node_fs_1.existsSync)(parent) || (0, node_fs_1.lstatSync)(parent).isSymbolicLink() || !(0, node_fs_1.lstatSync)(parent).isDirectory() || (0, node_fs_1.realpathSync)(parent) !== parent) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + } + // A backup is the authoritative original. A second cleanup must never remove it. + if (record.existed && !(0, node_fs_1.existsSync)(record.backup)) + return; + (0, node_fs_1.rmSync)(record.target, { recursive: true, force: true }); + if (record.existed) + (0, node_fs_1.renameSync)(record.backup, record.target); + for (const directory of [...record.created].reverse()) { + try { + (0, node_fs_1.rmdirSync)(directory); + } + catch { /* Other candidate content remains. */ } + } + (0, node_fs_1.rmSync)((0, node_path_1.join)(session, 'overlay.json'), { force: true }); +} + + +/***/ }), + +/***/ 22290: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.nativeVerificationProfilePaths = nativeVerificationProfilePaths; +exports.registerNativeVerificationProfile = registerNativeVerificationProfile; +exports.nativeVerificationRetainsProfile = nativeVerificationRetainsProfile; +exports.removeNativeVerificationProfiles = removeNativeVerificationProfiles; +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const verification_setup_policy_1 = __nccwpck_require__(65326); +const native_verification_protocol_1 = __nccwpck_require__(88709); +function nativeVerificationProfilePaths(root) { + const stored = (0, native_verification_protocol_1.readNativeRecord)(root, 'profiles.json', 65536); + const paths = stored === undefined ? [] : stored; + if (!Array.isArray(paths) || paths.length > 20 || new Set(paths).size !== paths.length || paths.some(path => typeof path !== 'string' + || (0, node_path_1.dirname)(path) !== (0, node_path_1.dirname)(root) || !/^copilot-verify-runtime-[A-Za-z0-9]+$/u.test((0, node_path_1.basename)(path)) || path === root)) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + return paths; +} +/** Native actions may have post steps. Preserve dependency-only profiles until those steps finish. */ +function registerNativeVerificationProfile(root, profile) { + const previous = nativeVerificationProfilePaths(root); + if (previous.length >= 20 || previous.includes(profile) || (0, node_path_1.dirname)(profile) !== (0, node_path_1.dirname)(root) + || profile === root || !/^copilot-verify-runtime-[A-Za-z0-9]+$/u.test((0, node_path_1.basename)(profile)) + || (0, node_fs_1.realpathSync)(profile) !== profile || !(0, node_fs_1.lstatSync)(profile).isDirectory() || (0, node_fs_1.lstatSync)(profile).isSymbolicLink()) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + (0, node_fs_1.writeFileSync)((0, node_path_1.join)(profile, 'native-session-owner'), root, { mode: 0o600, flag: 'wx' }); + (0, native_verification_protocol_1.writeNativeRecord)(root, 'profiles.json', [...previous, profile]); +} +function nativeVerificationRetainsProfile(profile) { + const root = process.env.COPILOT_NATIVE_WORKER_SESSION; + return !!root && nativeVerificationProfilePaths(root).includes(profile); +} +function removeNativeVerificationProfiles(root) { + for (const path of nativeVerificationProfilePaths(root)) { + let stat; + try { + stat = (0, node_fs_1.lstatSync)(path); + } + catch (error) { + if (error.code === 'ENOENT') + continue; + throw error; + } + if (!stat.isDirectory() || stat.isSymbolicLink() || (0, node_fs_1.realpathSync)(path) !== path || (0, node_fs_1.readFileSync)((0, node_path_1.join)(path, 'native-session-owner'), 'utf8') !== root) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + (0, node_fs_1.rmSync)(path, { recursive: true, force: true }); + } +} + + +/***/ }), + +/***/ 88709: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.nativePause = void 0; +exports.writeNativeRecord = writeNativeRecord; +exports.readNativeRecord = readNativeRecord; +exports.nativeSession = nativeSession; +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const node_crypto_1 = __nccwpck_require__(6005); +const verification_setup_policy_1 = __nccwpck_require__(65326); +function writeNativeRecord(root, name, value) { + const temporary = (0, node_path_1.join)(root, `${(0, node_crypto_1.randomUUID)()}.tmp`); + (0, node_fs_1.writeFileSync)(temporary, JSON.stringify(value), { mode: 0o600, flag: 'wx' }); + (0, node_fs_1.renameSync)(temporary, (0, node_path_1.join)(root, name)); +} +function readNativeRecord(root, name, limit = 16384) { + const file = (0, node_path_1.join)(root, name); + let descriptor; + try { + const before = (0, node_fs_1.lstatSync)(file, { bigint: true }); + if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.size > BigInt(limit)) + throw new Error(); + descriptor = (0, node_fs_1.openSync)(file, node_fs_1.constants.O_RDONLY | (node_fs_1.constants.O_NOFOLLOW ?? 0)); + const after = (0, node_fs_1.fstatSync)(descriptor, { bigint: true }); + if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.mtimeNs !== before.mtimeNs) + throw new Error(); + const data = (0, node_fs_1.readFileSync)(descriptor); + const final = (0, node_fs_1.fstatSync)(descriptor, { bigint: true }); + if (data.length > limit || final.size !== after.size || final.mtimeNs !== after.mtimeNs) + throw new Error(); + return JSON.parse(data.toString('utf8')); + } + catch (error) { + if (error.code === 'ENOENT') + return undefined; + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + } + finally { + if (descriptor !== undefined) + (0, node_fs_1.closeSync)(descriptor); + } +} +function nativeSession(root, environment = process.env) { + if (!/^copilot-verify-runtime-[A-Za-z0-9]+$/u.test((0, node_path_1.basename)(root)) || (0, node_fs_1.realpathSync)(root) !== root + || !(0, node_fs_1.lstatSync)(root).isDirectory() || (0, node_fs_1.lstatSync)(root).isSymbolicLink()) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + if (process.platform !== 'win32' && ((0, node_fs_1.lstatSync)(root).mode & 0o077) !== 0) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + const session = readNativeRecord(root, 'session.json'); + if (session?.schemaVersion !== 1 || session.workspace !== (0, node_fs_1.realpathSync)(process.cwd()) + || session.run !== (environment.GITHUB_RUN_ID ?? '') || session.attempt !== (environment.GITHUB_RUN_ATTEMPT ?? '') + || session.job !== (environment.GITHUB_JOB ?? '') || !Number.isSafeInteger(session.pid) || session.pid <= 0) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + return session; +} +const nativePause = (milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds)); +exports.nativePause = nativePause; + + /***/ }), /***/ 35849: @@ -102907,65 +103162,6 @@ function checkRuntime(requirement, environment, run, platform, job) { } -/***/ }), - -/***/ 43966: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { - -"use strict"; - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.runVerificationSetupProcess = runVerificationSetupProcess; -const node_child_process_1 = __nccwpck_require__(17718); -const agent_trusted_system_tools_1 = __nccwpck_require__(76049); -const verification_setup_policy_1 = __nccwpck_require__(65326); -/** All output is counted and discarded. Fixed production limits; short injected limits are for deterministic tests. */ -function runVerificationSetupProcess(node, script, cwd, env, sourceSha, limits = verification_setup_policy_1.VERIFICATION_SETUP_LIMITS) { - return new Promise((resolve, reject) => { - const child = (0, node_child_process_1.spawn)(node, [script], { cwd, env, shell: false, windowsHide: true, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] }); - let failure; - let outputBytes = 0; - const terminate = () => { - if (!child.pid) - return; - try { - if (process.platform === 'win32') - (0, node_child_process_1.spawnSync)((0, agent_trusted_system_tools_1.trustedWindowsSystemTool)('taskkill.exe'), ['/PID', String(child.pid), '/T', '/F'], { env, windowsHide: true, timeout: 5000, stdio: 'ignore', shell: false }); - else - process.kill(-child.pid, 'SIGKILL'); - } - catch { - child.kill('SIGKILL'); - } - }; - const timer = setTimeout(() => { failure ?? (failure = 'timeout'); terminate(); }, limits.timeoutMs); - const cancel = () => { failure ?? (failure = 'cancelled'); terminate(); }; - process.once('SIGINT', cancel); - process.once('SIGTERM', cancel); - const capture = (data) => { - outputBytes += data.length; - if (outputBytes > limits.outputBytes) { - failure ?? (failure = 'output-limit'); - terminate(); - } - }; - child.stdout.on('data', capture); - child.stderr.on('data', capture); - child.on('error', () => { failure ?? (failure = 'process-failed'); }); - child.on('close', code => { - clearTimeout(timer); - process.removeListener('SIGINT', cancel); - process.removeListener('SIGTERM', cancel); - terminate(); - if (failure || code !== 0) - reject(new verification_setup_policy_1.VerificationSetupError(failure ?? 'process-failed', sourceSha, code ?? undefined)); - else - resolve(); - }); - }); -} - - /***/ }), /***/ 23358: @@ -103045,37 +103241,74 @@ function applyVerificationSetupResult(file, environment, roots) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.prepareRepositoryVerification = prepareRepositoryVerification; +const native_verification_profiles_1 = __nccwpck_require__(22290); const node_fs_1 = __nccwpck_require__(87561); const node_path_1 = __nccwpck_require__(49411); +const node_crypto_1 = __nccwpck_require__(6005); const verification_setup_policy_1 = __nccwpck_require__(65326); const verification_setup_result_1 = __nccwpck_require__(23358); -const verification_setup_process_1 = __nccwpck_require__(43966); const logging_ports_1 = __nccwpck_require__(6152); -const verification_setup_policy_2 = __nccwpck_require__(65326); -/** Called once per verification command, on its fresh private profile, before runtime probes. */ +const native_verification_protocol_1 = __nccwpck_require__(88709); +const native_verification_overlay_1 = __nccwpck_require__(64378); +const native_verification_candidate_1 = __nccwpck_require__(18883); +/** The runner executes YAML. This adapter pauses the admitted call stack at its verification boundary. */ async function prepareRepositoryVerification(setup, environment) { if (setup.kind === 'disabled') return; if (setup.kind === 'blocked') throw new verification_setup_policy_1.VerificationSetupError(setup.reason, setup.sourceSha); + const session = process.env.COPILOT_NATIVE_WORKER_SESSION; + if (!session) + throw new verification_setup_policy_1.VerificationSetupError('native-workflow-required', setup.sourceSha); try { + (0, native_verification_protocol_1.nativeSession)(session); + const requests = (0, native_verification_protocol_1.readNativeRecord)(session, 'request-count.json')?.count ?? 0; + if (requests >= verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.requests) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + (0, native_verification_protocol_1.writeNativeRecord)(session, 'request-count.json', { count: requests + 1 }); if ((0, node_fs_1.realpathSync)(process.cwd()) !== setup.workspace) - throw new verification_setup_policy_1.VerificationSetupError('workspace-changed', setup.sourceSha); - const profile = (0, node_fs_1.realpathSync)(environment.HOME); - const script = (0, node_path_1.join)(profile, 'verification-setup.mjs'); - const result = (0, node_path_1.join)(profile, 'verification-setup-result.json'); - (0, node_fs_1.writeFileSync)(script, setup.script, { mode: 0o600, flag: 'wx' }); - await (0, verification_setup_process_1.runVerificationSetupProcess)(setup.node, script, setup.workspace, { - ...environment, - COPILOT_VERIFICATION_SETUP_PROTOCOL: '1', - COPILOT_VERIFICATION_SETUP_RESULT: result, - COPILOT_VERIFICATION_SETUP_SOURCE_SHA: setup.sourceSha, - }, setup.sourceSha); - (0, verification_setup_result_1.applyVerificationSetupResult)(result, environment, [setup.workspace, profile, ...(setup.toolRoot ? [setup.toolRoot] : [])]); - (0, logging_ports_1.logInfo)(`Repository setup prepared (${verification_setup_policy_2.VERIFICATION_SETUP_PATH}; trusted source SHA: ${setup.sourceSha}).`); + throw new verification_setup_policy_1.VerificationSetupError('workspace-changed'); + (0, native_verification_profiles_1.registerNativeVerificationProfile)(session, environment.HOME); + const identity = (0, native_verification_candidate_1.verificationCandidateIdentity)(setup.workspace, environment); + (0, native_verification_overlay_1.stageNativeVerificationAction)(setup, session); + const id = (0, node_crypto_1.randomUUID)(); + (0, native_verification_protocol_1.writeNativeRecord)(session, 'request.json', { id, sourceSha: setup.sourceSha, environment }); + (0, logging_ports_1.logInfo)(`Preparing repository verification (${verification_setup_policy_1.VERIFICATION_SETUP_PATH}; trusted source SHA: ${setup.sourceSha}).`); + const deadline = Date.now() + verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.timeoutMs; + let response; + while (!(response = (0, native_verification_protocol_1.readNativeRecord)(session, `response-${id}.json`))) { + if ((0, native_verification_protocol_1.readNativeRecord)(session, 'cancel.json')) + throw new verification_setup_policy_1.VerificationSetupError('cancelled'); + if (Date.now() >= deadline) + throw new verification_setup_policy_1.VerificationSetupError('timeout'); + await (0, native_verification_protocol_1.nativePause)(100); + } + (0, native_verification_overlay_1.restoreNativeVerificationAction)(session, setup.workspace); + if (response.id !== id || response.success !== true) + throw new verification_setup_policy_1.VerificationSetupError('process-failed'); + if ((0, native_verification_candidate_1.verificationCandidateIdentity)(setup.workspace, environment) !== identity) + throw new verification_setup_policy_1.VerificationSetupError('candidate-changed'); + if (typeof response.path !== 'string' || response.path.length > 16384 || response.javaHome !== undefined && typeof response.javaHome !== 'string') + throw new verification_setup_policy_1.VerificationSetupError('result-invalid'); + const original = new Set((environment.PATH ?? '').split(node_path_1.delimiter)); + const previousProfiles = (0, native_verification_profiles_1.nativeVerificationProfilePaths)(session).filter(profile => profile !== environment.HOME); + const fromPreviousProfile = (path) => previousProfiles.some(profile => { + const part = (0, node_path_1.relative)(profile, path); + return part === '' || part !== '..' && !part.startsWith(`..${node_path_1.sep}`) && !(0, node_path_1.isAbsolute)(part); + }); + const additions = [...new Set(response.path.split(node_path_1.delimiter).filter(path => path && !original.has(path) && !fromPreviousProfile(path)))]; + const result = (0, node_path_1.join)(environment.HOME, 'native-setup-result.json'); + (0, node_fs_1.writeFileSync)(result, JSON.stringify({ schemaVersion: 1, path: additions, + environment: response.javaHome && response.javaHome !== environment.JAVA_HOME ? { JAVA_HOME: response.javaHome } : {} }), { mode: 0o600, flag: 'wx' }); + (0, verification_setup_result_1.applyVerificationSetupResult)(result, environment, [setup.workspace, environment.HOME, ...(setup.toolRoot ? [setup.toolRoot] : [])]); + (0, logging_ports_1.logInfo)('Repository setup completed; runtime preflight and verification may proceed.'); } catch (error) { - throw new verification_setup_policy_1.VerificationSetupError(error instanceof verification_setup_policy_1.VerificationSetupError ? error.reason : 'process-failed', setup.sourceSha, error instanceof verification_setup_policy_1.VerificationSetupError ? error.exitCode : undefined); + throw new verification_setup_policy_1.VerificationSetupError(error instanceof verification_setup_policy_1.VerificationSetupError ? error.reason : 'handoff-invalid', setup.sourceSha); + } + finally { + (0, native_verification_overlay_1.restoreNativeVerificationAction)(session, setup.workspace); + (0, node_fs_1.rmSync)((0, node_path_1.join)(session, 'request.json'), { force: true }); } } @@ -103092,10 +103325,11 @@ exports.captureVerificationSetup = captureVerificationSetup; const node_child_process_1 = __nccwpck_require__(17718); const node_crypto_1 = __nccwpck_require__(6005); const node_fs_1 = __nccwpck_require__(87561); +const js_yaml_1 = __nccwpck_require__(783); const verification_setup_ports_1 = __nccwpck_require__(26509); const verification_setup_policy_1 = __nccwpck_require__(65326); const untrusted_command_environment_1 = __nccwpck_require__(2304); -/** No network, agent, worktree-file reads, or credentials. Acquisition errors are deferred until verification. */ +/** Only immutable Git objects are read. Errors remain deferred on non-verifying routes. */ function captureVerificationSetup(enabled, mainBranch, root = process.cwd()) { if (!enabled) return verification_setup_ports_1.DISABLED_VERIFICATION_SETUP; @@ -103111,27 +103345,53 @@ function captureVerificationSetup(enabled, mainBranch, root = process.cwd()) { const workspace = (0, node_fs_1.realpathSync)(root); const git = (...args) => { const result = (0, node_child_process_1.spawnSync)('git', ['--no-replace-objects', ...args], { cwd: workspace, env: environment, - shell: false, windowsHide: true, timeout: 5000, maxBuffer: verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.scriptBytes + 4096 }); + shell: false, windowsHide: true, timeout: 5000, maxBuffer: verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.snapshotBytes + 4096 }); if (result.error || result.status !== 0) throw new Error(); return result.stdout; }; - const candidateSha = git('rev-parse', '--verify', `refs/remotes/origin/${mainBranch}^{commit}`).toString('utf8').trim(); - if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(candidateSha)) + const sha = git('rev-parse', '--verify', `refs/remotes/origin/${mainBranch}^{commit}`).toString('utf8').trim(); + if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(sha)) throw new Error(); - sourceSha = candidateSha; - const entry = git('ls-tree', '-z', sourceSha, '--', verification_setup_policy_1.VERIFICATION_SETUP_PATH).toString('utf8'); - const match = /^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64})\t\.copilot\/verification-setup\.mjs\0$/u.exec(entry); - if (!match) + sourceSha = sha; + const entries = git('ls-tree', '-rz', sourceSha, '--', verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY).toString('utf8').split('\0').filter(Boolean); + if (!entries.length || entries.length > verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.files) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + let total = 0; + const files = entries.map(entry => { + const match = /^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64})\t(.+)$/u.exec(entry); + if (!match || !match[3].startsWith(`${verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY}/`)) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + const path = match[3].slice(verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY.length + 1); + if (!/^[A-Za-z0-9_./-]{1,200}$/u.test(path) || path.split('/').some(part => !part || part === '.' || part === '..' || part.endsWith('.') || /^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])(?:\.|$)/iu.test(part))) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + const bytes = git('cat-file', 'blob', match[2]); + total += bytes.length; + const digest = (0, node_crypto_1.createHash)(match[2].length === 40 ? 'sha1' : 'sha256').update(`blob ${bytes.length}\0`).update(bytes).digest('hex'); + if (bytes.length > verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.scriptBytes || total > verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.snapshotBytes || digest !== match[2]) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + return Object.freeze({ path, base64: bytes.toString('base64'), executable: match[1] === '100755' }); + }); + const manifest = files.find(file => file.path === 'action.yml'); + if (!manifest) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + const bytes = Buffer.from(manifest.base64, 'base64'); + const text = bytes.toString('utf8'); + if (!bytes.length || !bytes.equals(Buffer.from(text)) || text.includes('\0')) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + let action; + try { + action = (0, js_yaml_1.load)(text); + } + catch { + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + } + if (action?.runs?.using !== 'composite' || !Array.isArray(action.runs.steps) || !action.runs.steps.length || action.runs.steps.length > 32) throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); - const bytes = git('cat-file', 'blob', match[2]); - const digest = (0, node_crypto_1.createHash)(match[2].length === 40 ? 'sha1' : 'sha256').update(`blob ${bytes.length}\0`).update(bytes).digest('hex'); - const script = bytes.toString('utf8'); - if (!bytes.length || bytes.length > verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.scriptBytes || digest !== match[2] - || !Buffer.from(script).equals(bytes) || script.includes('\0')) + if (action.runs.steps.some(step => !step || typeof step !== 'object' || 'uses' in step && typeof step.uses === 'string' && step.uses.startsWith('./'))) throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); const toolRoot = environment.RUNNER_TOOL_CACHE ? (0, node_fs_1.realpathSync)(environment.RUNNER_TOOL_CACHE) : undefined; - snapshot = Object.freeze({ kind: 'ready', script, sourceSha, workspace, node: process.execPath, ...(toolRoot ? { toolRoot } : {}) }); + snapshot = Object.freeze({ kind: 'ready', files: Object.freeze(files), sourceSha, workspace, ...(toolRoot ? { toolRoot } : {}) }); } catch (error) { snapshot = Object.freeze({ kind: 'blocked', reason: error instanceof verification_setup_policy_1.VerificationSetupError ? error.reason : 'source-unavailable', ...(sourceSha ? { sourceSha } : {}) }); diff --git a/build/github_action/index.js b/build/github_action/index.js index b3e296ff8..5f78df7ea 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -36897,6 +36897,7 @@ var __importStar = (this && this.__importStar) || (function () { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runGitHubAction = runGitHubAction; exports.runGitHubActionEntry = runGitHubActionEntry; +const native_verification_controller_1 = __nccwpck_require__(71046); const issue_completion_composition_root_1 = __nccwpck_require__(63719); const core = __importStar(__nccwpck_require__(75855)); const github = __importStar(__nccwpck_require__(78227)); @@ -37106,7 +37107,7 @@ function earlyGithubActionErrorMessage() { // Only auto-run when executed as the action entry (not when imported by tests) /* istanbul ignore next -- the bundled production entry is covered by the Action smoke path. */ if (typeof process.env.JEST_WORKER_ID === 'undefined') { - void runGitHubActionEntry(); + void runGitHubActionEntry(() => (0, native_verification_controller_1.runNativeVerificationEntry)(runGitHubActionEntry)); } @@ -47803,7 +47804,7 @@ The setup runner profile selects placement; it does not install the project's la Declare reviewed version prefixes in \`.copilot/verification-prerequisites.json\` (schemaVersion 1, runtimes such as java 17, node 24, pnpm 10). Copilot checks declared versions in the credential-free verification environment before each configured verification command; a missing or incompatible runtime blocks publication. Without this optional declaration, existing verification behavior remains and no language is inferred. Java provisioning may use \`actions/setup-java\`; \`JAVA_HOME\` and \`PATH\` survive isolation, while agent credentials and tool-option injection variables do not. Do not add an unreviewed bootstrap or weaken verification to work around a missing runtime. Read [verification prerequisites and same-job setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites). -The optional repository-owned setup helper prepares each candidate before verification without editing generated workflows. Review \`.copilot/verification-setup.mjs\` onto trusted main before enabling \`VERIFICATION_SETUP=true\` (Action input \`verification-setup\`, disabled by default). Copilot executes captured trusted bytes before each command in a fresh credential-free profile; candidate changes cannot replace them. Setup failure blocks verification and commit/push. Read [repository-owned setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup). +The optional repository-owned composite action prepares each candidate before verification. Review \`.github/actions/copilot-verification-setup/action.yml\` and its assets onto trusted main, update generated workflows, then enable \`VERIFICATION_SETUP=true\` (Action input \`verification-setup\`, disabled by default). GitHub Actions executes the trusted snapshot before each command with a fresh private HOME; candidate changes cannot replace it. Native post steps finish before profile cleanup. Setup failure blocks verification and commit/push. The local CLI cannot execute composite actions and fails closed when opted in. Read [repository-owned setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup). For the full contributor flow, read [repository collaboration](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-collaboration.mdx). For drift and recovery, read [guidance troubleshooting](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-guidance-troubleshooting.mdx). `; @@ -51141,17 +51142,21 @@ function verificationRuntimeMatches(required, observed) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.VerificationSetupError = exports.VERIFICATION_SETUP_HELP = exports.VERIFICATION_SETUP_LIMITS = exports.VERIFICATION_SETUP_PATH = void 0; +exports.VerificationSetupError = exports.VERIFICATION_SETUP_HELP = exports.VERIFICATION_SETUP_LIMITS = exports.VERIFICATION_SETUP_PATH = exports.VERIFICATION_SETUP_DIRECTORY = void 0; exports.parseVerificationSetupResult = parseVerificationSetupResult; -exports.VERIFICATION_SETUP_PATH = '.copilot/verification-setup.mjs'; -exports.VERIFICATION_SETUP_LIMITS = Object.freeze({ timeoutMs: 300000, outputBytes: 65536, resultBytes: 16384, scriptBytes: 65536 }); +exports.VERIFICATION_SETUP_DIRECTORY = '.github/actions/copilot-verification-setup'; +exports.VERIFICATION_SETUP_PATH = `${exports.VERIFICATION_SETUP_DIRECTORY}/action.yml`; +exports.VERIFICATION_SETUP_LIMITS = Object.freeze({ timeoutMs: 300000, resultBytes: 16384, scriptBytes: 65536, snapshotBytes: 262144, files: 32, requests: 20 }); exports.VERIFICATION_SETUP_HELP = 'https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup'; /** Closed facts only. Provider/helper output and raw exception causes never cross this boundary. */ class VerificationSetupError extends Error { constructor(reason, sourceSha, exitCode) { const sha = sourceSha && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(sourceSha) ? sourceSha : 'unavailable'; const exit = Number.isInteger(exitCode) && exitCode >= 0 && exitCode <= 255 ? ` Exit status: ${exitCode}.` : ''; - super(`Repository setup blocked verification (${reason}). Review ${exports.VERIFICATION_SETUP_PATH} on the trusted main branch and rerun. Trusted source SHA: ${sha}.${exit} Verification, commit and push are withheld. Help: ${exports.VERIFICATION_SETUP_HELP}`); + const action = reason === 'native-workflow-required' + ? 'Update the generated Copilot workflows and run this operation in GitHub Actions; the local CLI cannot execute composite actions.' + : `Review ${exports.VERIFICATION_SETUP_PATH} on the trusted main branch and rerun.`; + super(`Repository setup blocked verification (${reason}). ${action} Trusted source SHA: ${sha}.${exit} Verification, commit and push are withheld. Help: ${exports.VERIFICATION_SETUP_HELP}`); this.reason = reason; this.exitCode = exitCode; this.name = 'VerificationSetupError'; @@ -87595,6 +87600,7 @@ var __importStar = (this && this.__importStar) || (function () { })(); Object.defineProperty(exports, "__esModule", ({ value: true })); exports.GitCommitAdapter = void 0; +const native_verification_profiles_1 = __nccwpck_require__(22290); const exec = __importStar(__nccwpck_require__(18538)); const git_authentication_environment_1 = __nccwpck_require__(16535); const untrusted_command_environment_1 = __nccwpck_require__(2304); @@ -87635,7 +87641,8 @@ class GitCommitAdapter { }); } finally { - runtime.cleanup(); + if (!(0, native_verification_profiles_1.nativeVerificationRetainsProfile)(runtime.environment.HOME)) + runtime.cleanup(); } } async configureAuthor(name, email) { @@ -88331,6 +88338,613 @@ class LoggerWorkflowPollingObserverAdapter { exports.LoggerWorkflowPollingObserverAdapter = LoggerWorkflowPollingObserverAdapter; +/***/ }), + +/***/ 18883: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.verificationCandidateIdentity = verificationCandidateIdentity; +const node_child_process_1 = __nccwpck_require__(17718); +const node_crypto_1 = __nccwpck_require__(6005); +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const verification_setup_policy_1 = __nccwpck_require__(65326); +/** Bind setup to the exact HEAD, branch, index, tracked edits and publishable untracked files. */ +function verificationCandidateIdentity(workspace, environment) { + const hash = (0, node_crypto_1.createHash)('sha256'); + const git = (...args) => { + const result = (0, node_child_process_1.spawnSync)('git', ['--no-replace-objects', ...args], { cwd: workspace, env: environment, shell: false, + windowsHide: true, timeout: 10000, maxBuffer: 16 * 1024 * 1024 }); + if (result.error || result.status !== 0) + throw new verification_setup_policy_1.VerificationSetupError('candidate-changed'); + hash.update(result.stdout).update('\0'); + return result.stdout; + }; + git('rev-parse', '--verify', 'HEAD'); + git('rev-parse', '--symbolic-full-name', 'HEAD'); + git('ls-files', '--stage', '-z'); + git('diff', '--no-ext-diff', '--no-textconv', '--binary', 'HEAD', '--'); + const paths = git('ls-files', '--others', '--exclude-standard', '-z').toString('utf8').split('\0').filter(Boolean); + let total = 0; + if (paths.length > 256) + throw new verification_setup_policy_1.VerificationSetupError('candidate-changed'); + for (const path of paths) { + const file = (0, node_path_1.join)(workspace, path); + const stat = (0, node_fs_1.lstatSync)(file); + total += stat.size; + if (total > 16 * 1024 * 1024) + throw new verification_setup_policy_1.VerificationSetupError('candidate-changed'); + hash.update(path).update('\0'); + if (stat.isSymbolicLink()) + hash.update((0, node_fs_1.readlinkSync)(file)); + else if (stat.isFile()) + hash.update((0, node_fs_1.readFileSync)(file)); + else + throw new verification_setup_policy_1.VerificationSetupError('candidate-changed'); + hash.update('\0'); + } + return hash.digest('hex'); +} + + +/***/ }), + +/***/ 71046: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.drainNativeWorkerLog = drainNativeWorkerLog; +exports.awaitNativeBoundary = awaitNativeBoundary; +exports.continueNativeVerification = continueNativeVerification; +exports.cleanupNativeVerification = cleanupNativeVerification; +exports.runNativeVerificationEntry = runNativeVerificationEntry; +const native_verification_redaction_1 = __nccwpck_require__(27632); +const native_verification_profiles_1 = __nccwpck_require__(22290); +const core = __importStar(__nccwpck_require__(75855)); +const node_child_process_1 = __nccwpck_require__(17718); +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const agent_private_install_root_1 = __nccwpck_require__(44640); +const agent_trusted_system_tools_1 = __nccwpck_require__(76049); +const github_action_input_1 = __nccwpck_require__(57581); +const verification_setup_policy_1 = __nccwpck_require__(65326); +const native_verification_protocol_1 = __nccwpck_require__(88709); +const native_verification_overlay_1 = __nccwpck_require__(64378); +function emitWorkerOutput(root) { + const file = (0, node_path_1.join)(root, 'worker-output'); + if (process.env.GITHUB_OUTPUT && (0, node_fs_1.existsSync)(file)) + (0, node_fs_1.appendFileSync)(process.env.GITHUB_OUTPUT, requireRead(file)); + const summary = (0, node_path_1.join)(root, 'worker-summary'); + if (process.env.GITHUB_STEP_SUMMARY && (0, node_fs_1.existsSync)(summary) && !(0, native_verification_protocol_1.readNativeRecord)(root, 'summary-emitted.json')) { + (0, node_fs_1.appendFileSync)(process.env.GITHUB_STEP_SUMMARY, requireRead(summary, 1048576)); + (0, native_verification_protocol_1.writeNativeRecord)(root, 'summary-emitted.json', { emitted: true }); + } +} +function requireRead(file, limit = 262144) { + const fd = (0, node_fs_1.openSync)(file, 'r'); + try { + const size = (0, node_fs_1.statSync)(file).size; + if (size > limit) + throw new verification_setup_policy_1.VerificationSetupError('output-limit'); + const data = Buffer.alloc(size); + (0, node_fs_1.readSync)(fd, data, 0, data.length, 0); + return data; + } + finally { + (0, node_fs_1.closeSync)(fd); + } +} +/** Drain complete lines so runner masking and workflow commands keep their native framing. */ +function drainNativeWorkerLog(root, flush = false) { + const file = (0, node_path_1.join)(root, 'worker-log'); + if (!(0, node_fs_1.existsSync)(file)) + return; + const cursor = (0, native_verification_protocol_1.readNativeRecord)(root, 'log-cursor.json')?.offset ?? 0; + const fd = (0, node_fs_1.openSync)(file, 'r'); + let offset = cursor; + try { + const buffer = Buffer.alloc(65536); + let pending = Buffer.alloc(0); + let size; + while ((size = (0, node_fs_1.readSync)(fd, buffer, 0, buffer.length, offset + pending.length))) { + pending = Buffer.concat([pending, buffer.subarray(0, size)]); + const lastLine = pending.lastIndexOf(10); + if (lastLine >= 0) { + const complete = pending.subarray(0, lastLine + 1); + process.stdout.write(complete); + offset += complete.length; + pending = pending.subarray(lastLine + 1); + } + // Existing action logs may contain long lines; retain framing without unbounded buffering. + if (pending.length > 1048576) + throw new verification_setup_policy_1.VerificationSetupError('output-limit'); + } + if (flush && pending.length) { + process.stdout.write(pending); + offset += pending.length; + } + (0, native_verification_protocol_1.writeNativeRecord)(root, 'log-cursor.json', { offset }); + } + finally { + (0, node_fs_1.closeSync)(fd); + } +} +async function awaitNativeBoundary(root, timeoutMs = 7200000) { + const deadline = Date.now() + timeoutMs; + while (true) { + drainNativeWorkerLog(root); + const done = (0, native_verification_protocol_1.readNativeRecord)(root, 'done.json'); + if (done) { + drainNativeWorkerLog(root, true); + emitWorkerOutput(root); + core.setOutput('verification-setup-pending', 'false'); + if (done.exitCode !== 0) + core.setFailed('Copilot execution failed; inspect the preceding operation result.'); + return; + } + const request = (0, native_verification_protocol_1.readNativeRecord)(root, 'request.json', 32768); + if (request) { + if (!/^[a-f0-9-]{36}$/u.test(request.id) || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(request.sourceSha) + || !request.environment || typeof request.environment.HOME !== 'string') + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + core.setOutput('verification-request', request.id); + for (const key of ['HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'PATH', 'JAVA_HOME', 'TMP', 'TEMP', 'TMPDIR']) { + const value = request.environment[key] ?? ''; + if (typeof value !== 'string' || /[\r\n\0]/u.test(value)) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + core.setOutput(`verification-${key.toLowerCase()}`, value); + } + core.setOutput('verification-setup-pending', 'true'); + return; + } + if (Date.now() >= deadline) + throw new verification_setup_policy_1.VerificationSetupError('timeout'); + const session = (0, native_verification_protocol_1.nativeSession)(root); + try { + process.kill(session.pid, 0); + } + catch { + throw new verification_setup_policy_1.VerificationSetupError('process-failed'); + } + await (0, native_verification_protocol_1.nativePause)(100); + } +} +async function continueNativeVerification(root, id, outcome, environment = process.env) { + (0, native_verification_protocol_1.nativeSession)(root); + const request = (0, native_verification_protocol_1.readNativeRecord)(root, 'request.json', 32768); + if (!request || request.id !== id || !/^[a-f0-9-]{36}$/u.test(id) + || (0, node_fs_1.existsSync)((0, node_path_1.join)(root, `response-${id}.json`))) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + const response = { id, success: outcome === 'success', path: environment.PATH ?? '', + ...(environment.JAVA_HOME ? { javaHome: environment.JAVA_HOME } : {}) }; + (0, node_fs_1.rmSync)((0, node_path_1.join)(root, 'request.json')); + (0, native_verification_protocol_1.writeNativeRecord)(root, `response-${id}.json`, response); + await awaitNativeBoundary(root); +} +async function cleanupNativeVerification(root, waitMs = 10000, deferProfileCleanup = false) { + if (!(0, node_fs_1.existsSync)(root)) + return; + const session = (0, native_verification_protocol_1.nativeSession)(root); + if (!(0, native_verification_protocol_1.readNativeRecord)(root, 'done.json')) { + (0, native_verification_protocol_1.writeNativeRecord)(root, 'cancel.json', { cancelled: true }); + const deadline = Date.now() + waitMs; + while (!(0, native_verification_protocol_1.readNativeRecord)(root, 'done.json') && Date.now() < deadline) + await (0, native_verification_protocol_1.nativePause)(100); + if (!(0, native_verification_protocol_1.readNativeRecord)(root, 'done.json')) { + try { + if (process.platform === 'win32') + (0, node_child_process_1.spawnSync)((0, agent_trusted_system_tools_1.trustedWindowsSystemTool)('taskkill.exe'), ['/PID', String(session.pid), '/T', '/F'], { windowsHide: true, shell: false, timeout: 5000, stdio: 'ignore' }); + else + process.kill(-session.pid, 'SIGKILL'); + } + catch { /* Worker may have exited while cancellation completed. */ } + } + } + (0, native_verification_overlay_1.restoreNativeVerificationAction)(root, session.workspace); + drainNativeWorkerLog(root, true); + if (!deferProfileCleanup) { + (0, native_verification_profiles_1.removeNativeVerificationProfiles)(root); + (0, node_fs_1.rmSync)(root, { recursive: true, force: true }); + } +} +async function startNativeVerification() { + for (const secret of (0, native_verification_redaction_1.nativeWorkerSecrets)(process.env)) + core.setSecret(secret); + const root = (0, node_fs_1.realpathSync)((0, agent_private_install_root_1.createPrivateVerificationRuntimeRoot)()); + for (const name of ['worker-output', 'worker-env', 'worker-path', 'worker-log', 'worker-summary']) + (0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, name), '', { flag: 'wx', mode: 0o600 }); + const log = (0, node_fs_1.openSync)((0, node_path_1.join)(root, 'worker-log'), 'a'); + try { + const worker = (0, node_child_process_1.spawn)(process.execPath, [process.argv[1]], { cwd: process.cwd(), detached: true, windowsHide: true, + stdio: ['ignore', log, log], env: { ...process.env, COPILOT_NATIVE_PHASE: '', COPILOT_NATIVE_WORKER_SESSION: root, + GITHUB_STEP_SUMMARY: (0, node_path_1.join)(root, 'worker-summary'), GITHUB_OUTPUT: (0, node_path_1.join)(root, 'worker-output'), GITHUB_ENV: (0, node_path_1.join)(root, 'worker-env'), GITHUB_PATH: (0, node_path_1.join)(root, 'worker-path') } }); + await new Promise((resolve, reject) => { worker.once('spawn', resolve); worker.once('error', reject); }); + worker.unref(); + (0, native_verification_protocol_1.writeNativeRecord)(root, 'session.json', { schemaVersion: 1, workspace: (0, node_fs_1.realpathSync)(process.cwd()), run: process.env.GITHUB_RUN_ID ?? '', + attempt: process.env.GITHUB_RUN_ATTEMPT ?? '', job: process.env.GITHUB_JOB ?? '', pid: worker.pid }); + core.saveState('native-verification-ticket', root); + core.setOutput('verification-ticket', root); + await awaitNativeBoundary(root); + } + catch (error) { + if ((0, node_fs_1.existsSync)((0, node_path_1.join)(root, 'session.json'))) + await cleanupNativeVerification(root); + else + (0, node_fs_1.rmSync)(root, { recursive: true, force: true }); + throw error; + } + finally { + (0, node_fs_1.closeSync)(log); + } +} +/** Native phases are runner-only control messages; no credentials or agent operations are replayed. */ +async function runNativeVerificationEntry(run) { + const post = core.getState('copilot-native-post'); + if (post === 'true') { + const root = core.getState('native-verification-ticket'); + if (root) + await cleanupNativeVerification(root); + return; + } + core.saveState('copilot-native-post', 'true'); + const worker = process.env.COPILOT_NATIVE_WORKER_SESSION; + if (worker) { + const deadline = Date.now() + 30000; + while (!(0, native_verification_protocol_1.readNativeRecord)(worker, 'session.json')) { + if (Date.now() >= deadline) + throw new verification_setup_policy_1.VerificationSetupError('timeout'); + await (0, native_verification_protocol_1.nativePause)(10); + } + if ((0, native_verification_protocol_1.nativeSession)(worker).pid !== process.pid) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + const restoreLog = (0, native_verification_redaction_1.redactNativeWorkerLogs)(); + try { + await run(); + } + finally { + restoreLog(); + (0, native_verification_protocol_1.writeNativeRecord)(worker, 'done.json', { exitCode: Number(process.exitCode ?? 0) }); + } + return; + } + const phase = process.env.COPILOT_NATIVE_PHASE; + if (!phase || phase === 'prepare' && (0, github_action_input_1.getGithubActionInput)('verification-setup') !== 'true') { + await run(); + return; + } + if (process.env.GITHUB_ACTIONS !== 'true') + throw new verification_setup_policy_1.VerificationSetupError('native-workflow-required'); + if (phase === 'prepare') { + await startNativeVerification(); + return; + } + const root = process.env.COPILOT_NATIVE_TICKET; + if (!root) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + if (phase === 'continue') { + await continueNativeVerification(root, process.env.COPILOT_NATIVE_REQUEST ?? '', process.env.COPILOT_NATIVE_OUTCOME ?? ''); + return; + } + if (phase === 'finish') { + (0, native_verification_protocol_1.nativeSession)(root); + const done = (0, native_verification_protocol_1.readNativeRecord)(root, 'done.json'); + if (done) { + emitWorkerOutput(root); + if (done.exitCode !== 0) + core.setFailed('Copilot execution failed.'); + } + else + core.setFailed(`Repository setup did not complete within ${verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.requests} native verification boundaries. Pending verification and subsequent publication are blocked; inspect earlier operation results for completed effects.`); + await cleanupNativeVerification(root, 10000, true); + return; + } + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); +} + + +/***/ }), + +/***/ 64378: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.stageNativeVerificationAction = stageNativeVerificationAction; +exports.restoreNativeVerificationAction = restoreNativeVerificationAction; +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const verification_setup_policy_1 = __nccwpck_require__(65326); +const native_verification_protocol_1 = __nccwpck_require__(88709); +/** A temporary reviewed action overlays the candidate path only while the operation is paused. */ +function stageNativeVerificationAction(setup, session) { + const target = (0, node_path_1.join)(setup.workspace, verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY); + const backup = (0, node_path_1.join)(session, 'candidate-action'); + const created = []; + let parent = setup.workspace; + for (const part of verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY.split('/').slice(0, -1)) { + parent = (0, node_path_1.join)(parent, part); + if ((0, node_fs_1.existsSync)(parent)) { + if ((0, node_fs_1.lstatSync)(parent).isSymbolicLink() || !(0, node_fs_1.lstatSync)(parent).isDirectory() || (0, node_fs_1.realpathSync)(parent) !== parent) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + } + else { + (0, node_fs_1.mkdirSync)(parent, { mode: 0o700 }); + created.push(parent); + } + } + const targetStat = (0, node_fs_1.lstatSync)(target, { throwIfNoEntry: false }); + const existed = !!targetStat; + if (targetStat && (targetStat.isSymbolicLink() || !targetStat.isDirectory())) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + const record = { target, backup, existed, created }; + (0, native_verification_protocol_1.writeNativeRecord)(session, 'overlay.json', record); + if (existed) + (0, node_fs_1.renameSync)(target, backup); + (0, node_fs_1.mkdirSync)(target, { mode: 0o700 }); + for (const file of setup.files) { + const destination = (0, node_path_1.join)(target, file.path); + const remainder = (0, node_path_1.relative)(target, destination); + if (!remainder || remainder.startsWith(`..${node_path_1.sep}`) || remainder === '..') + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(destination), { recursive: true, mode: 0o700 }); + (0, node_fs_1.writeFileSync)(destination, Buffer.from(file.base64, 'base64'), { mode: file.executable ? 0o700 : 0o600, flag: 'wx' }); + } +} +function restoreNativeVerificationAction(session, workspace) { + const record = (0, native_verification_protocol_1.readNativeRecord)(session, 'overlay.json'); + if (!record) + return; + if (record.target !== (0, node_path_1.join)(workspace, verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY) || record.backup !== (0, node_path_1.join)(session, 'candidate-action') + || !Array.isArray(record.created) || record.created.some(path => ![(0, node_path_1.join)(workspace, '.github'), (0, node_path_1.join)(workspace, '.github/actions')].includes(path))) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + for (const parent of [(0, node_path_1.join)(workspace, '.github'), (0, node_path_1.join)(workspace, '.github/actions')]) { + if (!(0, node_fs_1.existsSync)(parent) || (0, node_fs_1.lstatSync)(parent).isSymbolicLink() || !(0, node_fs_1.lstatSync)(parent).isDirectory() || (0, node_fs_1.realpathSync)(parent) !== parent) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + } + // A backup is the authoritative original. A second cleanup must never remove it. + if (record.existed && !(0, node_fs_1.existsSync)(record.backup)) + return; + (0, node_fs_1.rmSync)(record.target, { recursive: true, force: true }); + if (record.existed) + (0, node_fs_1.renameSync)(record.backup, record.target); + for (const directory of [...record.created].reverse()) { + try { + (0, node_fs_1.rmdirSync)(directory); + } + catch { /* Other candidate content remains. */ } + } + (0, node_fs_1.rmSync)((0, node_path_1.join)(session, 'overlay.json'), { force: true }); +} + + +/***/ }), + +/***/ 22290: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.nativeVerificationProfilePaths = nativeVerificationProfilePaths; +exports.registerNativeVerificationProfile = registerNativeVerificationProfile; +exports.nativeVerificationRetainsProfile = nativeVerificationRetainsProfile; +exports.removeNativeVerificationProfiles = removeNativeVerificationProfiles; +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const verification_setup_policy_1 = __nccwpck_require__(65326); +const native_verification_protocol_1 = __nccwpck_require__(88709); +function nativeVerificationProfilePaths(root) { + const stored = (0, native_verification_protocol_1.readNativeRecord)(root, 'profiles.json', 65536); + const paths = stored === undefined ? [] : stored; + if (!Array.isArray(paths) || paths.length > 20 || new Set(paths).size !== paths.length || paths.some(path => typeof path !== 'string' + || (0, node_path_1.dirname)(path) !== (0, node_path_1.dirname)(root) || !/^copilot-verify-runtime-[A-Za-z0-9]+$/u.test((0, node_path_1.basename)(path)) || path === root)) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + return paths; +} +/** Native actions may have post steps. Preserve dependency-only profiles until those steps finish. */ +function registerNativeVerificationProfile(root, profile) { + const previous = nativeVerificationProfilePaths(root); + if (previous.length >= 20 || previous.includes(profile) || (0, node_path_1.dirname)(profile) !== (0, node_path_1.dirname)(root) + || profile === root || !/^copilot-verify-runtime-[A-Za-z0-9]+$/u.test((0, node_path_1.basename)(profile)) + || (0, node_fs_1.realpathSync)(profile) !== profile || !(0, node_fs_1.lstatSync)(profile).isDirectory() || (0, node_fs_1.lstatSync)(profile).isSymbolicLink()) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + (0, node_fs_1.writeFileSync)((0, node_path_1.join)(profile, 'native-session-owner'), root, { mode: 0o600, flag: 'wx' }); + (0, native_verification_protocol_1.writeNativeRecord)(root, 'profiles.json', [...previous, profile]); +} +function nativeVerificationRetainsProfile(profile) { + const root = process.env.COPILOT_NATIVE_WORKER_SESSION; + return !!root && nativeVerificationProfilePaths(root).includes(profile); +} +function removeNativeVerificationProfiles(root) { + for (const path of nativeVerificationProfilePaths(root)) { + let stat; + try { + stat = (0, node_fs_1.lstatSync)(path); + } + catch (error) { + if (error.code === 'ENOENT') + continue; + throw error; + } + if (!stat.isDirectory() || stat.isSymbolicLink() || (0, node_fs_1.realpathSync)(path) !== path || (0, node_fs_1.readFileSync)((0, node_path_1.join)(path, 'native-session-owner'), 'utf8') !== root) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + (0, node_fs_1.rmSync)(path, { recursive: true, force: true }); + } +} + + +/***/ }), + +/***/ 88709: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.nativePause = void 0; +exports.writeNativeRecord = writeNativeRecord; +exports.readNativeRecord = readNativeRecord; +exports.nativeSession = nativeSession; +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const node_crypto_1 = __nccwpck_require__(6005); +const verification_setup_policy_1 = __nccwpck_require__(65326); +function writeNativeRecord(root, name, value) { + const temporary = (0, node_path_1.join)(root, `${(0, node_crypto_1.randomUUID)()}.tmp`); + (0, node_fs_1.writeFileSync)(temporary, JSON.stringify(value), { mode: 0o600, flag: 'wx' }); + (0, node_fs_1.renameSync)(temporary, (0, node_path_1.join)(root, name)); +} +function readNativeRecord(root, name, limit = 16384) { + const file = (0, node_path_1.join)(root, name); + let descriptor; + try { + const before = (0, node_fs_1.lstatSync)(file, { bigint: true }); + if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.size > BigInt(limit)) + throw new Error(); + descriptor = (0, node_fs_1.openSync)(file, node_fs_1.constants.O_RDONLY | (node_fs_1.constants.O_NOFOLLOW ?? 0)); + const after = (0, node_fs_1.fstatSync)(descriptor, { bigint: true }); + if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.mtimeNs !== before.mtimeNs) + throw new Error(); + const data = (0, node_fs_1.readFileSync)(descriptor); + const final = (0, node_fs_1.fstatSync)(descriptor, { bigint: true }); + if (data.length > limit || final.size !== after.size || final.mtimeNs !== after.mtimeNs) + throw new Error(); + return JSON.parse(data.toString('utf8')); + } + catch (error) { + if (error.code === 'ENOENT') + return undefined; + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + } + finally { + if (descriptor !== undefined) + (0, node_fs_1.closeSync)(descriptor); + } +} +function nativeSession(root, environment = process.env) { + if (!/^copilot-verify-runtime-[A-Za-z0-9]+$/u.test((0, node_path_1.basename)(root)) || (0, node_fs_1.realpathSync)(root) !== root + || !(0, node_fs_1.lstatSync)(root).isDirectory() || (0, node_fs_1.lstatSync)(root).isSymbolicLink()) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + if (process.platform !== 'win32' && ((0, node_fs_1.lstatSync)(root).mode & 0o077) !== 0) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + const session = readNativeRecord(root, 'session.json'); + if (session?.schemaVersion !== 1 || session.workspace !== (0, node_fs_1.realpathSync)(process.cwd()) + || session.run !== (environment.GITHUB_RUN_ID ?? '') || session.attempt !== (environment.GITHUB_RUN_ATTEMPT ?? '') + || session.job !== (environment.GITHUB_JOB ?? '') || !Number.isSafeInteger(session.pid) || session.pid <= 0) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + return session; +} +const nativePause = (milliseconds) => new Promise(resolve => setTimeout(resolve, milliseconds)); +exports.nativePause = nativePause; + + +/***/ }), + +/***/ 27632: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.nativeWorkerSecrets = nativeWorkerSecrets; +exports.redactNativeWorkerLogs = redactNativeWorkerLogs; +const node_string_decoder_1 = __nccwpck_require__(76915); +const SECRET_KEY = /(?:TOKEN|SECRET|API_KEY|PASSWORD|AUTH_JSON)/iu; +function nativeWorkerSecrets(environment) { + const values = Object.entries(environment).filter(([key, value]) => SECRET_KEY.test(key) && value).map(([, value]) => value); + if (environment.INPUT_VARS_JSON) { + try { + const inputs = JSON.parse(environment.INPUT_VARS_JSON); + for (const [key, value] of Object.entries(inputs)) + if (SECRET_KEY.test(key) && typeof value === 'string') + values.push(value); + } + catch { /* The existing input parser reports invalid configuration without raw values. */ } + } + return [...new Set(values.flatMap(value => [value, ...value.split(/[\r\n]/u)]).filter(value => value.length >= 3))].sort((a, b) => b.length - a.length); +} +/** Secrets are removed before bytes reach a private log file, not merely masked in the runner UI. */ +function redactNativeWorkerLogs(environment = process.env) { + const secrets = nativeWorkerSecrets(environment); + const restores = []; + for (const stream of [process.stdout, process.stderr]) { + const original = stream.write; + const decoder = new node_string_decoder_1.StringDecoder('utf8'); + let pending = ''; + const emit = (line) => { + const command = /^::add-mask::(.*?)(?:\r?\n)?$/u.exec(line); + if (command) { + const value = command[1].replace(/%0D/gu, '\r').replace(/%0A/gu, '\n').replace(/%25/gu, '%'); + secrets.push(...value.split(/[\r\n]/u).filter(part => part.length >= 3)); + return; + } + for (const secret of secrets) + line = line.split(secret).join('[REDACTED]'); + original.call(stream, line); + }; + stream.write = ((chunk, encoding, callback) => { + pending += typeof chunk === 'string' ? chunk : decoder.write(Buffer.from(chunk)); + let end; + while ((end = pending.indexOf('\n')) >= 0) { + const line = pending.slice(0, end + 1); + pending = pending.slice(end + 1); + emit(line); + } + if (pending.length > 1048576) + throw new Error('Copilot worker log line exceeded its fixed limit.'); + if (typeof encoding === 'function') + encoding(); + else + callback?.(); + return true; + }); + restores.push(() => { pending += decoder.end(); if (pending) + emit(pending); stream.write = original; }); + } + return () => { for (const restore of restores) + restore(); }; +} + + /***/ }), /***/ 35849: @@ -89129,65 +89743,6 @@ function checkRuntime(requirement, environment, run, platform, job) { } -/***/ }), - -/***/ 43966: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { - -"use strict"; - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.runVerificationSetupProcess = runVerificationSetupProcess; -const node_child_process_1 = __nccwpck_require__(17718); -const agent_trusted_system_tools_1 = __nccwpck_require__(76049); -const verification_setup_policy_1 = __nccwpck_require__(65326); -/** All output is counted and discarded. Fixed production limits; short injected limits are for deterministic tests. */ -function runVerificationSetupProcess(node, script, cwd, env, sourceSha, limits = verification_setup_policy_1.VERIFICATION_SETUP_LIMITS) { - return new Promise((resolve, reject) => { - const child = (0, node_child_process_1.spawn)(node, [script], { cwd, env, shell: false, windowsHide: true, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] }); - let failure; - let outputBytes = 0; - const terminate = () => { - if (!child.pid) - return; - try { - if (process.platform === 'win32') - (0, node_child_process_1.spawnSync)((0, agent_trusted_system_tools_1.trustedWindowsSystemTool)('taskkill.exe'), ['/PID', String(child.pid), '/T', '/F'], { env, windowsHide: true, timeout: 5000, stdio: 'ignore', shell: false }); - else - process.kill(-child.pid, 'SIGKILL'); - } - catch { - child.kill('SIGKILL'); - } - }; - const timer = setTimeout(() => { failure ?? (failure = 'timeout'); terminate(); }, limits.timeoutMs); - const cancel = () => { failure ?? (failure = 'cancelled'); terminate(); }; - process.once('SIGINT', cancel); - process.once('SIGTERM', cancel); - const capture = (data) => { - outputBytes += data.length; - if (outputBytes > limits.outputBytes) { - failure ?? (failure = 'output-limit'); - terminate(); - } - }; - child.stdout.on('data', capture); - child.stderr.on('data', capture); - child.on('error', () => { failure ?? (failure = 'process-failed'); }); - child.on('close', code => { - clearTimeout(timer); - process.removeListener('SIGINT', cancel); - process.removeListener('SIGTERM', cancel); - terminate(); - if (failure || code !== 0) - reject(new verification_setup_policy_1.VerificationSetupError(failure ?? 'process-failed', sourceSha, code ?? undefined)); - else - resolve(); - }); - }); -} - - /***/ }), /***/ 23358: @@ -89267,37 +89822,74 @@ function applyVerificationSetupResult(file, environment, roots) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.prepareRepositoryVerification = prepareRepositoryVerification; +const native_verification_profiles_1 = __nccwpck_require__(22290); const node_fs_1 = __nccwpck_require__(87561); const node_path_1 = __nccwpck_require__(49411); +const node_crypto_1 = __nccwpck_require__(6005); const verification_setup_policy_1 = __nccwpck_require__(65326); const verification_setup_result_1 = __nccwpck_require__(23358); -const verification_setup_process_1 = __nccwpck_require__(43966); const logging_ports_1 = __nccwpck_require__(6152); -const verification_setup_policy_2 = __nccwpck_require__(65326); -/** Called once per verification command, on its fresh private profile, before runtime probes. */ +const native_verification_protocol_1 = __nccwpck_require__(88709); +const native_verification_overlay_1 = __nccwpck_require__(64378); +const native_verification_candidate_1 = __nccwpck_require__(18883); +/** The runner executes YAML. This adapter pauses the admitted call stack at its verification boundary. */ async function prepareRepositoryVerification(setup, environment) { if (setup.kind === 'disabled') return; if (setup.kind === 'blocked') throw new verification_setup_policy_1.VerificationSetupError(setup.reason, setup.sourceSha); + const session = process.env.COPILOT_NATIVE_WORKER_SESSION; + if (!session) + throw new verification_setup_policy_1.VerificationSetupError('native-workflow-required', setup.sourceSha); try { + (0, native_verification_protocol_1.nativeSession)(session); + const requests = (0, native_verification_protocol_1.readNativeRecord)(session, 'request-count.json')?.count ?? 0; + if (requests >= verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.requests) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + (0, native_verification_protocol_1.writeNativeRecord)(session, 'request-count.json', { count: requests + 1 }); if ((0, node_fs_1.realpathSync)(process.cwd()) !== setup.workspace) - throw new verification_setup_policy_1.VerificationSetupError('workspace-changed', setup.sourceSha); - const profile = (0, node_fs_1.realpathSync)(environment.HOME); - const script = (0, node_path_1.join)(profile, 'verification-setup.mjs'); - const result = (0, node_path_1.join)(profile, 'verification-setup-result.json'); - (0, node_fs_1.writeFileSync)(script, setup.script, { mode: 0o600, flag: 'wx' }); - await (0, verification_setup_process_1.runVerificationSetupProcess)(setup.node, script, setup.workspace, { - ...environment, - COPILOT_VERIFICATION_SETUP_PROTOCOL: '1', - COPILOT_VERIFICATION_SETUP_RESULT: result, - COPILOT_VERIFICATION_SETUP_SOURCE_SHA: setup.sourceSha, - }, setup.sourceSha); - (0, verification_setup_result_1.applyVerificationSetupResult)(result, environment, [setup.workspace, profile, ...(setup.toolRoot ? [setup.toolRoot] : [])]); - (0, logging_ports_1.logInfo)(`Repository setup prepared (${verification_setup_policy_2.VERIFICATION_SETUP_PATH}; trusted source SHA: ${setup.sourceSha}).`); + throw new verification_setup_policy_1.VerificationSetupError('workspace-changed'); + (0, native_verification_profiles_1.registerNativeVerificationProfile)(session, environment.HOME); + const identity = (0, native_verification_candidate_1.verificationCandidateIdentity)(setup.workspace, environment); + (0, native_verification_overlay_1.stageNativeVerificationAction)(setup, session); + const id = (0, node_crypto_1.randomUUID)(); + (0, native_verification_protocol_1.writeNativeRecord)(session, 'request.json', { id, sourceSha: setup.sourceSha, environment }); + (0, logging_ports_1.logInfo)(`Preparing repository verification (${verification_setup_policy_1.VERIFICATION_SETUP_PATH}; trusted source SHA: ${setup.sourceSha}).`); + const deadline = Date.now() + verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.timeoutMs; + let response; + while (!(response = (0, native_verification_protocol_1.readNativeRecord)(session, `response-${id}.json`))) { + if ((0, native_verification_protocol_1.readNativeRecord)(session, 'cancel.json')) + throw new verification_setup_policy_1.VerificationSetupError('cancelled'); + if (Date.now() >= deadline) + throw new verification_setup_policy_1.VerificationSetupError('timeout'); + await (0, native_verification_protocol_1.nativePause)(100); + } + (0, native_verification_overlay_1.restoreNativeVerificationAction)(session, setup.workspace); + if (response.id !== id || response.success !== true) + throw new verification_setup_policy_1.VerificationSetupError('process-failed'); + if ((0, native_verification_candidate_1.verificationCandidateIdentity)(setup.workspace, environment) !== identity) + throw new verification_setup_policy_1.VerificationSetupError('candidate-changed'); + if (typeof response.path !== 'string' || response.path.length > 16384 || response.javaHome !== undefined && typeof response.javaHome !== 'string') + throw new verification_setup_policy_1.VerificationSetupError('result-invalid'); + const original = new Set((environment.PATH ?? '').split(node_path_1.delimiter)); + const previousProfiles = (0, native_verification_profiles_1.nativeVerificationProfilePaths)(session).filter(profile => profile !== environment.HOME); + const fromPreviousProfile = (path) => previousProfiles.some(profile => { + const part = (0, node_path_1.relative)(profile, path); + return part === '' || part !== '..' && !part.startsWith(`..${node_path_1.sep}`) && !(0, node_path_1.isAbsolute)(part); + }); + const additions = [...new Set(response.path.split(node_path_1.delimiter).filter(path => path && !original.has(path) && !fromPreviousProfile(path)))]; + const result = (0, node_path_1.join)(environment.HOME, 'native-setup-result.json'); + (0, node_fs_1.writeFileSync)(result, JSON.stringify({ schemaVersion: 1, path: additions, + environment: response.javaHome && response.javaHome !== environment.JAVA_HOME ? { JAVA_HOME: response.javaHome } : {} }), { mode: 0o600, flag: 'wx' }); + (0, verification_setup_result_1.applyVerificationSetupResult)(result, environment, [setup.workspace, environment.HOME, ...(setup.toolRoot ? [setup.toolRoot] : [])]); + (0, logging_ports_1.logInfo)('Repository setup completed; runtime preflight and verification may proceed.'); } catch (error) { - throw new verification_setup_policy_1.VerificationSetupError(error instanceof verification_setup_policy_1.VerificationSetupError ? error.reason : 'process-failed', setup.sourceSha, error instanceof verification_setup_policy_1.VerificationSetupError ? error.exitCode : undefined); + throw new verification_setup_policy_1.VerificationSetupError(error instanceof verification_setup_policy_1.VerificationSetupError ? error.reason : 'handoff-invalid', setup.sourceSha); + } + finally { + (0, native_verification_overlay_1.restoreNativeVerificationAction)(session, setup.workspace); + (0, node_fs_1.rmSync)((0, node_path_1.join)(session, 'request.json'), { force: true }); } } @@ -89314,10 +89906,11 @@ exports.captureVerificationSetup = captureVerificationSetup; const node_child_process_1 = __nccwpck_require__(17718); const node_crypto_1 = __nccwpck_require__(6005); const node_fs_1 = __nccwpck_require__(87561); +const js_yaml_1 = __nccwpck_require__(783); const verification_setup_ports_1 = __nccwpck_require__(26509); const verification_setup_policy_1 = __nccwpck_require__(65326); const untrusted_command_environment_1 = __nccwpck_require__(2304); -/** No network, agent, worktree-file reads, or credentials. Acquisition errors are deferred until verification. */ +/** Only immutable Git objects are read. Errors remain deferred on non-verifying routes. */ function captureVerificationSetup(enabled, mainBranch, root = process.cwd()) { if (!enabled) return verification_setup_ports_1.DISABLED_VERIFICATION_SETUP; @@ -89333,27 +89926,53 @@ function captureVerificationSetup(enabled, mainBranch, root = process.cwd()) { const workspace = (0, node_fs_1.realpathSync)(root); const git = (...args) => { const result = (0, node_child_process_1.spawnSync)('git', ['--no-replace-objects', ...args], { cwd: workspace, env: environment, - shell: false, windowsHide: true, timeout: 5000, maxBuffer: verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.scriptBytes + 4096 }); + shell: false, windowsHide: true, timeout: 5000, maxBuffer: verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.snapshotBytes + 4096 }); if (result.error || result.status !== 0) throw new Error(); return result.stdout; }; - const candidateSha = git('rev-parse', '--verify', `refs/remotes/origin/${mainBranch}^{commit}`).toString('utf8').trim(); - if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(candidateSha)) + const sha = git('rev-parse', '--verify', `refs/remotes/origin/${mainBranch}^{commit}`).toString('utf8').trim(); + if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(sha)) throw new Error(); - sourceSha = candidateSha; - const entry = git('ls-tree', '-z', sourceSha, '--', verification_setup_policy_1.VERIFICATION_SETUP_PATH).toString('utf8'); - const match = /^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64})\t\.copilot\/verification-setup\.mjs\0$/u.exec(entry); - if (!match) + sourceSha = sha; + const entries = git('ls-tree', '-rz', sourceSha, '--', verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY).toString('utf8').split('\0').filter(Boolean); + if (!entries.length || entries.length > verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.files) throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); - const bytes = git('cat-file', 'blob', match[2]); - const digest = (0, node_crypto_1.createHash)(match[2].length === 40 ? 'sha1' : 'sha256').update(`blob ${bytes.length}\0`).update(bytes).digest('hex'); - const script = bytes.toString('utf8'); - if (!bytes.length || bytes.length > verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.scriptBytes || digest !== match[2] - || !Buffer.from(script).equals(bytes) || script.includes('\0')) + let total = 0; + const files = entries.map(entry => { + const match = /^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64})\t(.+)$/u.exec(entry); + if (!match || !match[3].startsWith(`${verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY}/`)) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + const path = match[3].slice(verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY.length + 1); + if (!/^[A-Za-z0-9_./-]{1,200}$/u.test(path) || path.split('/').some(part => !part || part === '.' || part === '..' || part.endsWith('.') || /^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])(?:\.|$)/iu.test(part))) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + const bytes = git('cat-file', 'blob', match[2]); + total += bytes.length; + const digest = (0, node_crypto_1.createHash)(match[2].length === 40 ? 'sha1' : 'sha256').update(`blob ${bytes.length}\0`).update(bytes).digest('hex'); + if (bytes.length > verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.scriptBytes || total > verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.snapshotBytes || digest !== match[2]) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + return Object.freeze({ path, base64: bytes.toString('base64'), executable: match[1] === '100755' }); + }); + const manifest = files.find(file => file.path === 'action.yml'); + if (!manifest) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + const bytes = Buffer.from(manifest.base64, 'base64'); + const text = bytes.toString('utf8'); + if (!bytes.length || !bytes.equals(Buffer.from(text)) || text.includes('\0')) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + let action; + try { + action = (0, js_yaml_1.load)(text); + } + catch { + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + } + if (action?.runs?.using !== 'composite' || !Array.isArray(action.runs.steps) || !action.runs.steps.length || action.runs.steps.length > 32) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + if (action.runs.steps.some(step => !step || typeof step !== 'object' || 'uses' in step && typeof step.uses === 'string' && step.uses.startsWith('./'))) throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); const toolRoot = environment.RUNNER_TOOL_CACHE ? (0, node_fs_1.realpathSync)(environment.RUNNER_TOOL_CACHE) : undefined; - snapshot = Object.freeze({ kind: 'ready', script, sourceSha, workspace, node: process.execPath, ...(toolRoot ? { toolRoot } : {}) }); + snapshot = Object.freeze({ kind: 'ready', files: Object.freeze(files), sourceSha, workspace, ...(toolRoot ? { toolRoot } : {}) }); } catch (error) { snapshot = Object.freeze({ kind: 'blocked', reason: error instanceof verification_setup_policy_1.VerificationSetupError ? error.reason : 'source-unavailable', ...(sourceSha ? { sourceSha } : {}) }); @@ -92149,6 +92768,14 @@ module.exports = require("node:stream"); /***/ }), +/***/ 76915: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:string_decoder"); + +/***/ }), + /***/ 31764: /***/ ((module) => { diff --git a/docs/configuration.mdx b/docs/configuration.mdx index 83793eb09..f2490172c 100644 --- a/docs/configuration.mdx +++ b/docs/configuration.mdx @@ -63,7 +63,7 @@ PR bot approval uses the versioned `pullRequestApproval` setup object and `PR_AP ## Branch Management - `issue-managed-branches`: Create and verify a linked branch after `in-progress` (default: "true") - - `verification-setup`: Run the trusted main-branch repository setup helper before each configured verification command (default: "false"). See [repository-owned setup](/security-operations/operations/verification#repository-owned-setup). + - `verification-setup`: Run the trusted main-branch composite action before each configured verification command in generated GitHub Actions callbacks (default: "false"). See [repository-owned setup](/security-operations/operations/verification#repository-owned-setup). - `pre-branch-sdd`: Require a validated SDD before implementation for eligible issues (default: "false") - `branch-management-emoji`: Emoji for branched issues (default: "🧑‍💻") diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index bac0ffe85..dbe8a494e 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -636,18 +636,30 @@ SDK contract, replay, drift and presentation tests enforce these boundaries. ## Repository verification setup -The optional [repository-owned helper](/security-operations/operations/verification#repository-owned-setup) -is captured once at comment-route composition from the configured trusted main -commit. The immutable semantic contract contains only regular-file bytes, source -SHA and trusted execution roots. Acquisition failures are deferred; actor, fork, -native-head and workspace guards still decide whether verification is reached. - -Both branch synchronization and shared automated commit preflight request the same -`untrusted + verifyRuntimePrerequisites` Git-port intent. Its adapter creates a fresh -private environment, runs the snapshot, validates the bounded closed result, probes -runtime prerequisites, executes the command and cleans up. The pure policy owns -schema/limits/sanitized diagnostics; infrastructure owns Git, filesystem, process -timeouts and cancellation. Agent credential profile cleanup precedes this phase. -No entrypoint duplicates the process runner, and no command/candidate reuses setup -readiness. Structural workflow/configuration tests keep all six source/distributed -event workflows consistent; local real-Git tests enforce the same-ref push gate. +The optional [repository-owned composite action](/security-operations/operations/verification#repository-owned-setup) +is captured with all reviewed auxiliary files at comment-route composition from +an immutable trusted main commit. Acquisition errors remain deferred. Existing +actor, fork, native-head and workspace gates decide whether verification is reached. + +Branch synchronization and shared automated commit preflight request the same +`untrusted + verifyRuntimePrerequisites` port intent. The Git adapter creates a +fresh private HOME. Its infrastructure pauses the admitted same-job worker exactly +before preflight, stages the trusted action and lets GitHub Actions execute the +native YAML. Continuation matches the operation/request/run/job, restores candidate +action files, validates candidate identity and bounded runtime directories, then +runs the existing probes and verifier. The agent and publication are not replayed. + +Pure policy owns limits, schemas and sanitized errors. Snapshot, overlay, protocol, +profile and controller adapters own Git/filesystem/process details; application +use cases retain mutation policy. Native action post steps can use dependency-only +profiles until the original Action's final post cleanup removes them. They are +never reused by later commands. Native step failure/cancellation blocks publication +and triggers existing recovery. A final cleanup guard stops a leftover worker. + +The workflow generator emits twenty conditional setup/continuation slots and a +completion step in each actual mutation callback. Structural validation compares +the complete native contract, including timeout and runtime-only setup environment. +All six configuration callbacks forward the opt-in, while only issue/review-comment +mutation compositions execute it. Push analysis does not become a verifier. +Native platform acceptance runs real `uses`/`run` on Linux, Windows and macOS; +local real-Git integration covers exact managed-ref push and abort behavior. diff --git a/docs/security-operations/operations/troubleshooting.mdx b/docs/security-operations/operations/troubleshooting.mdx index 989d29d92..6b60450d7 100644 --- a/docs/security-operations/operations/troubleshooting.mdx +++ b/docs/security-operations/operations/troubleshooting.mdx @@ -586,24 +586,26 @@ instead of deleting configuration. ## Repository setup blocks verification -A `Repository setup blocked verification` detail means the helper did not authorize -this command environment. Verification and commit/push are withheld; branch sync -uses its existing merge abort. Inspect the closed cause and pinned trusted SHA in -the current run, then correct `.copilot/verification-setup.mjs` on `main-branch`: +`Repository setup blocked verification` means preparation did not establish a safe +command environment. Verification and commit/push are withheld; branch sync uses +its existing merge abort. Inspect the native setup step and closed cause in the +run, then repair the reviewed action on `main-branch`. | Cause | Recovery | | --- | --- | -| `source-unavailable` | Make the configured trusted origin ref available in the full checkout. | -| `helper-invalid` | Review a regular standalone Node ESM helper, at most 64 KiB, onto trusted main. | -| `workspace-changed` | Retry from the expected candidate checkout. | -| `process-failed` | Correct the helper or its locked dependency installation; inspect only private operator diagnostics. | -| `timeout` / `output-limit` | Fit setup within 300 seconds and 64 KiB combined output; no limit override or automatic retry exists. | +| `source-unavailable` | Make the trusted origin ref available in the full checkout. | +| `helper-invalid` | Review the fixed native composite and regular auxiliary files; respect file/step bounds and avoid nested local `uses`. | +| `native-workflow-required` | Update generated callbacks and run in GitHub Actions. The local CLI cannot execute composite actions. | +| `workspace-changed` / `candidate-changed` | Keep setup dependency-only; do not change publishable source, index, branch or lockfiles. Retry from a fresh candidate. | +| `process-failed` | Inspect the native action's logs and fix its reviewed dependency installation. | +| `timeout` | Fit each native setup within five minutes; no limit override or automatic retry exists. | +| `output-limit` | A Copilot worker log line/output exceeded its framing bound; native setup logs use normal Actions limits. | | `cancelled` | Retry the authorized operation after cancellation is resolved. | -| `result-invalid` | Use protocol v1, at most 16 KiB, only approved existing PATH directories and JAVA_HOME. | - -The raw helper output is intentionally withheld. A helper committed only on the -candidate branch does not replace the captured main version. A missing helper with -`VERIFICATION_SETUP=true` blocks checks; the disabled default preserves legacy -behavior. See [repository-owned setup](/security-operations/operations/verification#repository-owned-setup) -for Python/pnpm examples, credential boundaries and update preservation. A separate -passing CI job does not prepare this command environment. +| `handoff-invalid` | Rerun through updated generated callbacks; never reuse a session from another run/job or request. | +| `result-invalid` | Export only supported, existing PATH/JAVA_HOME directories under the candidate, private HOME or trusted tool cache. | + +The consumer file is `.github/actions/copilot-verification-setup/action.yml`. +Candidate edits do not replace the trusted snapshot. Enabling without a reviewed +action blocks verification; the disabled default preserves existing behavior. +See [repository-owned setup](/security-operations/operations/verification#repository-owned-setup) +for native Python/pnpm examples, profile lifetime, trust boundaries and migration. diff --git a/docs/security-operations/operations/verification.mdx b/docs/security-operations/operations/verification.mdx index 07c344ebf..11bd4c1bb 100644 --- a/docs/security-operations/operations/verification.mdx +++ b/docs/security-operations/operations/verification.mdx @@ -34,7 +34,7 @@ consumer project's language runtimes or dependencies. A passing application CI job does not prepare the separate `issue_comment`, PR-comment or automated-fix job. Each job that runs `bugbot-fix-verify-commands` must prepare its own tools, including for `/copilot sync-branch --no-agent`. Use the [repository-owned setup -helper](#repository-owned-setup) to prepare the candidate without editing generated +action](#repository-owned-setup) to prepare the candidate without editing generated workflows, or retain your reviewed same-job bootstrap. For a Kotlin/JVM project, commit a reviewed @@ -121,119 +121,150 @@ already-aligned branches do not execute verification or prove runtime readiness. ## Repository-owned setup -To prepare project dependencies without editing generated workflows: - -1. Review a standalone `.copilot/verification-setup.mjs` onto your configured - `main-branch` (normally production). Candidate-branch helpers cannot activate - themselves. Keep runtime versions and dependency lockfiles under review. -2. Set Repository Variable `VERIFICATION_SETUP` to `true`. Generated source and - consumer event workflows forward it as the boolean `verification-setup` Action - input. For setup config files, use `repository: { verificationSetup: true }`. - The default is `false`; paths, commands, arguments and limits are fixed. -3. Retry the authorized modification or `/copilot sync-branch --no-agent`. - Copilot runs the trusted snapshot on the actual candidate after merge/fix - changes, immediately before **each** configured command and its runtime probes. - -Setup captures immutable regular-file bytes and the source commit SHA before an -agent can modify the workspace. The configured `origin/` ref must be -available locally; generated comment workflows use a full checkout. The snapshot -runs with the Action's Node executable, `shell: false`, no user arguments and cwd -at the candidate root. It is copied into a private directory, so relative module -imports do not load siblings from your repository. Use Node built-ins and explicit -subprocess argument arrays. The standalone helper is limited to 64 KiB. - -The helper receives a fresh private HOME and the existing allowlisted verification -environment, with these additional non-secret protocol fields: - -| Variable | Value | -| --- | --- | -| `COPILOT_VERIFICATION_SETUP_PROTOCOL` | `1` | -| `COPILOT_VERIFICATION_SETUP_SOURCE_SHA` | Pinned trusted source commit | -| `COPILOT_VERIFICATION_SETUP_RESULT` | Optional private JSON output-file path | - -No PAT, model/cloud credentials, Git write credentials, agent auth-store variables -or prior agent credential profile are supplied. Agent profiles are removed before -verification begins. This retains the trusted-runner boundary; it is not an OS -sandbox against malicious maintainer code. Review installers as executable code. -Do not rely on authenticated caches, ambient user-home settings or a separate CI job. - -A helper may finish without a result, leaving the environment unchanged. To add -prepared tools, write this closed result format to the private result path: +Each repository can prepare its dependencies in a native composite action, without +editing generated Copilot workflows. GitHub Actions executes the YAML's `uses` and +`run` steps in the same job where Copilot verifies the candidate. -```json -{"schemaVersion":1,"path":["/absolute/approved/tools/bin"],"environment":{"JAVA_HOME":"/absolute/approved/jdk"}} +```mermaid +flowchart LR + A[Copilot prepares changes] --> B[Repository composite installs dependencies] + B --> C[Runtime preflight and verification] + C --> D[Existing publication guards] ``` -`path` and `environment` are optional. Only up to eight unique absolute PATH -directories (2,048 characters each) and `JAVA_HOME` are supported. Every directory -must already exist inside the canonical candidate workspace, private HOME or -captured trusted `RUNNER_TOOL_CACHE`. Escaping symlinks, traversal, control -characters, PATH delimiter injection, null values and unknown fields are rejected. -The result must be a regular, nonlinked JSON file of at most 16 KiB. No arbitrary -environment variables are accepted. stdout/stderr never set policy and are never -published raw. Protocol metadata is not passed to the verification command. - -For Python on a Linux/macOS runner with the declared Python version installed, -this helper creates a fresh private virtual environment and installs the candidate's -reviewed requirements lockfile, including pytest. The lockfile must pin all -transitive dependencies and hashes. The command can remain `python3 -m pytest`: - -```javascript -import { execFileSync } from 'node:child_process'; -import { writeFileSync } from 'node:fs'; -import { join } from 'node:path'; - -const venv = join(process.env.HOME, 'venv'); -execFileSync('python3', ['-m', 'venv', venv], { stdio: 'inherit' }); -const bin = join(venv, 'bin'); -execFileSync(join(bin, 'python3'), [ - '-m', 'pip', 'install', '--require-hashes', '-r', 'requirements-test.lock', -], { stdio: 'inherit' }); -writeFileSync(process.env.COPILOT_VERIFICATION_SETUP_RESULT, - JSON.stringify({ schemaVersion: 1, path: [bin] })); +Text equivalent: prepare the candidate, install its dependencies, verify it, then +apply the existing publication guards. A separate CI job does not prepare this job. + +1. Review `.github/actions/copilot-verification-setup/action.yml` and its auxiliary + scripts onto your configured `main-branch` (normally production). Candidate or + model changes cannot activate a different setup implementation. +2. Update the generated comment workflows with `copilot setup`. They contain the + native setup and continuation steps; an older callback cannot execute YAML. +3. Enable Repository Variable `VERIFICATION_SETUP=true`, or plan it with setup + configuration `repository: { verificationSetup: true }`. The corresponding + boolean Action input is `verification-setup`; its default is `false`. +4. Retry the authorized modification or `/copilot sync-branch --no-agent`. + +Copilot captures the complete action directory from an immutable trusted Git SHA +before candidate changes. The trusted origin ref must be available locally; +generated comment workflows use a full checkout. The runner executes that snapshot +**after merge/fix changes and before each configured verifier**, reading the actual +candidate manifests. The admitted operation continues once; the agent is not rerun. +Copilot temporarily overlays the action directory with the reviewed snapshot and +restores the candidate's original files before verification or publication. + +For Python, the consumer file can contain: + +```yaml +name: Prepare project verification +runs: + using: composite + steps: + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + - name: Install the candidate's test dependencies + run: | + python - <<'PYTHON' + import os, pathlib, subprocess, venv + home = pathlib.Path(os.environ['HOME']) / 'venv' + venv.create(home, with_pip=True) + tools = home / ('Scripts' if os.name == 'nt' else 'bin') + python = tools / ('python.exe' if os.name == 'nt' else 'python') + subprocess.check_call([str(python), '-m', 'pip', 'install', + '--require-hashes', '-r', 'requirements-test.lock']) + with open(os.environ['GITHUB_PATH'], 'a') as paths: + print(tools, file=paths) + PYTHON + shell: bash ``` -For Node/pnpm on a Linux/macOS runner, explicitly provision the pinned pnpm version -instead of relying on an implicit Corepack download. This alternative requires -`npm` on the reviewed runner and uses the candidate's `pnpm-lock.yaml`: - -```javascript -import { execFileSync } from 'node:child_process'; -import { writeFileSync } from 'node:fs'; -import { join } from 'node:path'; - -const tools = join(process.env.HOME, 'tools'); -execFileSync('npm', ['install', '--prefix', tools, '--ignore-scripts', - '--no-audit', '--no-fund', 'pnpm@10.12.4'], { stdio: 'inherit' }); -execFileSync(process.execPath, [join(tools, 'node_modules/pnpm/bin/pnpm.cjs'), - 'install', '--frozen-lockfile', '--ignore-scripts'], { stdio: 'inherit' }); -writeFileSync(process.env.COPILOT_VERIFICATION_SETUP_RESULT, - JSON.stringify({ schemaVersion: 1, path: [join(tools, 'node_modules/.bin')] })); +Keep pytest and all transitive dependencies pinned with hashes in the candidate's +`requirements-test.lock`. The verifier can remain `python -m pytest`. Adapt runtime +versions and dependency commands to your project; these examples do not infer them. + +For Node/pnpm: + +```yaml +name: Prepare project verification +runs: + using: composite + steps: + - uses: actions/setup-node@v7 + with: + node-version: '24.x' + package-manager-cache: false + - uses: pnpm/action-setup@v5 + with: + version: 10.12.4 + dest: ${{ env.HOME }}/pnpm + standalone: false + - run: pnpm install --frozen-lockfile --ignore-scripts + shell: bash ``` -These are alternative project helpers, not universal installers. Adapt your -platform's executable names and reviewed runtime versions. Windows Python uses -`Scripts` and `python.exe`; invoke package-manager scripts through Node rather -than passing `.cmd` names to shell-free Node subprocesses. Keep generated caches -ignored, never modify tracked source/lockfiles during verification, and validate -lockfiles before reusing any workspace cache. Private tools are recreated for each -command; there is no readiness cache across commands or candidates. - -Each invocation has a fixed **300-second timeout**, **64 KiB combined stdout/stderr -ceiling**, and **16 KiB result ceiling**. There are no overrides or automatic -retries. Setup failure, timeout, cancellation or invalid output stops preflight and -verification, withholds commit/push and follows existing sync abort behavior. The -existing maximum of twenty verification commands remains. Diagnostics contain -only a closed cause, fixed helper path, pinned SHA and safe exit status; success -logs the path and source SHA. Existing operation comments and run navigation carry -the result without per-command comments. +The action may include reviewed auxiliary scripts, referenced through +`${{ github.action_path }}`. Use native external `uses` actions and `run` steps; +nested local `uses: ./...` actions are rejected because their deferred post steps +could otherwise load restored candidate code. Review external action versions as +part of the executable setup. The composite inherits the job's operating system; +its metadata cannot declare jobs, runners or a matrix. + +Each command gets a **fresh private HOME**. These dependency-only profiles stay +private until native post-job actions finish, then Copilot's final post cleanup +removes them. They are never reused by another command. Keep dependencies that +must survive restoration under the private HOME, an ignored candidate directory +or the trusted runner tool cache, rather than inside the temporary action folder. + +Native setup exports tools through normal Actions mechanisms such as `GITHUB_PATH` +and `JAVA_HOME`; no custom JSON helper protocol is required. Copilot accepts up to +eight new unique absolute PATH directories, each at most 2,048 characters, and +JAVA_HOME. They must exist inside the candidate workspace, its private HOME or the +captured `RUNNER_TOOL_CACHE`; unsafe links, traversal and escaping paths are rejected. +The internal closed runtime result is bounded to 16 KiB. Other `GITHUB_ENV` values +are not forwarded to verification. Use complete executable paths or supported +runtime directories when a tool needs additional environment configuration. + +Copilot's PAT, provider/model inputs, agent auth stores and evidence token are not +passed to the consumer action or verifier. The native runner and external actions +retain normal GitHub Actions facilities and the job's scoped `GITHUB_TOKEN` context. +This is the existing trusted-runner boundary, not an OS sandbox against malicious +maintainer code or service-level credentials. Review setup as executable code and +avoid printing secrets. Agent credential profiles close before verification. + +A native setup step has a fixed **five-minute timeout**; the worker also rejects +readiness after 300 seconds. There are at most twenty setup boundaries per operation, +one for each configured command. No automatic setup retry or limit override exists. +The action directory is bounded to 32 regular files, 64 KiB each and 256 KiB total; +its metadata must be `action.yml`, with `runs.using: composite` and 1–32 steps. +Use portable ASCII asset paths (letters, digits, `_`, `.`, `-` and `/`), at most +200 characters; parent/empty segments and Windows reserved filenames are rejected. +Native setup logs are visible in the run; there is no 64-KiB helper-output ceiling. +Copilot's own error/result comments contain only a closed cause, fixed path and +pinned trusted SHA, never copied raw setup output. + +Setup must install dependencies without changing publishable code or lockfiles. +Copilot compares HEAD, branch, index, tracked edits and publishable untracked file +contents before allowing the verifier to continue. Observations are bounded to +16 MiB, 256 untracked files and 16 MiB aggregate untracked contents. Ignore dependency +caches and keep source changes within these bounds. Failure, changed candidate, +invalid runtime output, timeout or cancellation withholds verification and push; +branch synchronization follows its existing abort/recovery. Read-only questions/status, dry runs, already-aligned branches and empty command -lists never execute the helper. Acquisition failures are deferred until actual -verification. With the input disabled, existing prerequisites and behavior remain. -Updating Copilot preserves this consumer-owned file. To roll back the opt-in, set -`VERIFICATION_SETUP` to `false`; keep the same verification commands and provision -their dependencies through your reviewed runner/bootstrap. +lists execute no setup. Snapshot failures remain deferred until verification. +Setup/install/update preserves this consumer-owned directory and all its assets. + +Native composite execution is available in the generated **GitHub Actions** +callbacks. The local CLI cannot execute `uses` steps: opted-in local verification +fails with `native-workflow-required`. Run the authorized operation in Actions, +or keep the opt-in disabled and provision your local dependencies normally. +Direct custom callers must implement the documented generated native callback +structure; setting the flag on an old single Action step is insufficient. + +To roll back, set `VERIFICATION_SETUP=false` and provision dependencies before +Copilot in the same job. The unreleased `.copilot/verification-setup.mjs` prototype +is superseded; it is not executed or installed by this contract. ## Reused credential evidence diff --git a/scripts/coverage-budgets.json b/scripts/coverage-budgets.json index acfff75f8..ed140a380 100644 --- a/scripts/coverage-budgets.json +++ b/scripts/coverage-budgets.json @@ -916,8 +916,13 @@ "files": [ "src/infrastructure/verification_setup_snapshot.ts", "src/infrastructure/verification_setup_result.ts", - "src/infrastructure/verification_setup_process.ts", - "src/infrastructure/verification_setup_runner.ts" + "src/infrastructure/verification_setup_runner.ts", + "src/infrastructure/native_verification_protocol.ts", + "src/infrastructure/native_verification_candidate.ts", + "src/infrastructure/native_verification_overlay.ts", + "src/infrastructure/native_verification_controller.ts", + "src/infrastructure/native_verification_profiles.ts", + "src/infrastructure/native_verification_redaction.ts" ], "mode": "aggregate", "thresholdProfile": "default" diff --git a/scripts/generate-native-verification-workflows.cjs b/scripts/generate-native-verification-workflows.cjs new file mode 100644 index 000000000..cf3811fcb --- /dev/null +++ b/scripts/generate-native-verification-workflows.cjs @@ -0,0 +1,58 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const yaml = require('js-yaml'); + +const MAX_REQUESTS = 20; +const ACTION = './.github/actions/copilot-verification-setup'; +const MARKER = ' # copilot:native-verification:start'; +const CALLBACKS = ['copilot_issue_comment.yml', 'copilot_pull_request_comment.yml']; + +function phaseSteps(action, inputAlias = 'copilot-inputs', id = 'copilot') { + const steps = []; + let previous = id; + for (let count = 1; count <= MAX_REQUESTS; count++) { + const setup = `verification_setup_${count}`; + const continuation = `verification_continue_${count}`; + const pending = `steps.${previous}.outputs.verification-setup-pending == 'true'`; + const output = key => `\${{ steps.${previous}.outputs.verification-${key} }}`; + steps.push({ name: `Prepare repository verification ${count}`, id: setup, + if: `\${{ !cancelled() && steps.${previous}.outcome == 'success' && ${pending} }}`, uses: ACTION, 'timeout-minutes': 5, + env: Object.fromEntries(['HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'PATH', 'JAVA_HOME', 'TMP', 'TEMP', 'TMPDIR'].map(key => [key, output(key.toLowerCase())])) }); + steps.push({ name: `Continue repository verification ${count}`, id: continuation, + if: `\${{ always() && ${pending} }}`, uses: action, with: { alias: inputAlias }, + env: { COPILOT_NATIVE_PHASE: 'continue', COPILOT_NATIVE_TICKET: `\${{ steps.${id}.outputs.verification-ticket }}`, + COPILOT_NATIVE_REQUEST: `\${{ steps.${previous}.outputs.verification-request }}`, + COPILOT_NATIVE_OUTCOME: `\${{ steps.${setup}.outcome }}` } }); + previous = continuation; + } + steps.push({ name: 'Complete repository verification', id: 'verification_complete', + if: `\${{ always() && steps.${id}.outputs.verification-ticket != '' }}`, uses: action, with: { alias: inputAlias }, + env: { COPILOT_NATIVE_PHASE: 'finish', COPILOT_NATIVE_TICKET: `\${{ steps.${id}.outputs.verification-ticket }}` } }); + return steps; +} + +function renderSteps(action, alias = 'copilot-inputs', id = 'copilot') { + return yaml.dump(phaseSteps(action, alias, id), { lineWidth: -1, noRefs: true, quotingType: '"' }) + .replace(/with:\n alias: ([^\n]+)/g, 'with: *$1') + .split('\n').map(line => line ? ` ${line}` : '').join('\n'); +} + +function generate(file) { + let text = fs.readFileSync(file, 'utf8').split(MARKER)[0].trimEnd() + '\n'; + const action = file.startsWith('setup/') ? 'vypdev/copilot@v3' : './'; + const start = ` - uses: ${action}\n`; + text = text.replace(/(?: id: copilot\n)+/g, ''); + if (text.includes(start)) text = text.replace(start, ` - uses: ${action}\n id: copilot\n`); + text = text.replace(' with:\n debug:', ' with: &copilot-inputs\n debug:'); + // Distributed templates start with agent inputs rather than debug. + const index = text.indexOf(' id: copilot\n'); + const before = text.slice(0, index); + let operation = text.slice(index); + operation = operation.replace(' with:\n', ' with: &copilot-inputs\n'); + if (!operation.includes(' COPILOT_NATIVE_PHASE: prepare')) operation = operation.replace(' env:\n', ' env:\n COPILOT_NATIVE_PHASE: prepare\n'); + text = before + operation; + fs.writeFileSync(file, `${text}\n${MARKER}\n${renderSteps(action)} # copilot:native-verification:end\n`); +} + +if (require.main === module) for (const directory of ['.github/workflows', 'setup/workflows']) for (const file of CALLBACKS) generate(path.join(directory, file)); +module.exports = { MAX_REQUESTS, ACTION, CALLBACKS, phaseSteps, renderSteps }; diff --git a/scripts/validate-workflow-contract.cjs b/scripts/validate-workflow-contract.cjs index 010d93940..47073e9cf 100644 --- a/scripts/validate-workflow-contract.cjs +++ b/scripts/validate-workflow-contract.cjs @@ -1402,8 +1402,25 @@ function assertIssueBranchConfigurationInputs(file, workflow) { } } +function assertNativeVerificationWorkflow(file, workflow) { + const contract = require('./generate-native-verification-workflows.cjs'); + if (!contract.CALLBACKS.includes(path.basename(file))) return; + const relativeFile = relativeWorkflow(file); + const action = relativeFile.startsWith('setup/workflows/') ? DISTRIBUTED_COPILOT_ACTION : './'; + const jobs = Object.values(workflow.jobs ?? {}).filter(job => (job.steps ?? []).some(step => step.id === 'copilot')); + if (jobs.length !== 1) throw new Error(`${relativeFile} must have exactly one native verification operation.`); + const steps = jobs[0].steps; + const start = steps.findIndex(step => step.id === 'copilot'); + const original = steps[start]; + if (original.uses !== action || original.env?.COPILOT_NATIVE_PHASE !== 'prepare') throw new Error(`${relativeFile} must prepare the admitted native operation.`); + const expected = contract.phaseSteps(action).map(step => ({ ...step, ...(step.with ? { with: original.with } : {}) })); + const actual = steps.slice(start + 1); + if (JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error(`${relativeFile} must retain the exact bounded native setup/continuation contract, runtime-only environment, timeout and completion cleanup.`); +} + function validateWorkflow(file, workflow) { if (!workflow || typeof workflow !== 'object') throw new Error('workflow document is empty.'); + assertNativeVerificationWorkflow(file, workflow); assertManualRepoWiseWorkflow(file, workflow); assertDirectEventTriggers(file, workflow); assertApprovalObserverWorkflow(file, workflow); @@ -1449,6 +1466,7 @@ function main() { if (require.main === module) main(); module.exports = { + assertNativeVerificationWorkflow, QUEUE_WAIT_MINUTES, MIN_QUEUE_JOB_TIMEOUT_MINUTES, DEPLOYMENT_VALIDATION_TIMEOUT_MINUTES, diff --git a/setup/workflows/copilot_commit.yml b/setup/workflows/copilot_commit.yml index bd3dd0718..c5421cf99 100644 --- a/setup/workflows/copilot_commit.yml +++ b/setup/workflows/copilot_commit.yml @@ -43,7 +43,7 @@ jobs: # Before Copilot, provision project runtimes/dependencies in this same job when # repository verification is configured. A separate CI job does not prepare it. # Declare reviewed versions in .copilot/verification-prerequisites.json. - # Optional trusted-main setup: VERIFICATION_SETUP=true and .copilot/verification-setup.mjs. + # Optional trusted-main setup: VERIFICATION_SETUP=true and .github/actions/copilot-verification-setup/action.yml. # https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites - uses: vypdev/copilot@v3 with: diff --git a/setup/workflows/copilot_issue.yml b/setup/workflows/copilot_issue.yml index 06b17345e..05ce55b50 100644 --- a/setup/workflows/copilot_issue.yml +++ b/setup/workflows/copilot_issue.yml @@ -21,7 +21,7 @@ jobs: # Before Copilot, provision project runtimes/dependencies in this same job when # repository verification is configured. A separate CI job does not prepare it. # Declare reviewed versions in .copilot/verification-prerequisites.json. - # Optional trusted-main setup: VERIFICATION_SETUP=true and .copilot/verification-setup.mjs. + # Optional trusted-main setup: VERIFICATION_SETUP=true and .github/actions/copilot-verification-setup/action.yml. # https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites - uses: vypdev/copilot@v3 with: diff --git a/setup/workflows/copilot_issue_comment.yml b/setup/workflows/copilot_issue_comment.yml index 08cfa87de..0cd6df82b 100644 --- a/setup/workflows/copilot_issue_comment.yml +++ b/setup/workflows/copilot_issue_comment.yml @@ -23,10 +23,11 @@ jobs: # Before Copilot, provision project runtimes/dependencies in this same job when # repository verification is configured. A separate CI job does not prepare it. # Declare reviewed versions in .copilot/verification-prerequisites.json. - # Optional trusted-main setup: VERIFICATION_SETUP=true and .copilot/verification-setup.mjs. + # Optional trusted-main setup: VERIFICATION_SETUP=true and .github/actions/copilot-verification-setup/action.yml. # https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites - uses: vypdev/copilot@v3 - with: + id: copilot + with: &copilot-inputs repository-locale: ${{ vars.REPOSITORY_LOCALE || 'en-US' }} issues-locale: ${{ vars.ISSUES_LOCALE || '' }} pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || '' }} @@ -99,6 +100,7 @@ jobs: project-ids: ${{ vars.PROJECT_IDS }} token: ${{ secrets.PAT }} env: + COPILOT_NATIVE_PHASE: prepare AGENT_PROVIDER: ${{ vars.AGENT_PROVIDER || 'codex' }} AGENT_MODEL_PROVIDER: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} AGENT_MODEL: ${{ vars.AGENT_MODEL }} @@ -114,3 +116,514 @@ jobs: CURSOR_API_KEY: ${{ (vars.AGENT_PROVIDER == 'cursor' || vars.FINDINGS_PROVIDER == 'cursor' || vars.FIXER_PROVIDER == 'cursor' || vars.PLANNER_PROVIDER == 'cursor' || vars.REVIEWER_PROVIDER == 'cursor' || vars.TESTER_PROVIDER == 'cursor') && secrets.CURSOR_API_KEY || '' }} CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }} COPILOT_EVIDENCE_TOKEN: ${{ github.token }} + + # copilot:native-verification:start + - name: Prepare repository verification 1 + id: verification_setup_1 + if: ${{ !cancelled() && steps.copilot.outcome == 'success' && steps.copilot.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.copilot.outputs.verification-home }} + USERPROFILE: ${{ steps.copilot.outputs.verification-userprofile }} + APPDATA: ${{ steps.copilot.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.copilot.outputs.verification-localappdata }} + PATH: ${{ steps.copilot.outputs.verification-path }} + JAVA_HOME: ${{ steps.copilot.outputs.verification-java_home }} + TMP: ${{ steps.copilot.outputs.verification-tmp }} + TEMP: ${{ steps.copilot.outputs.verification-temp }} + TMPDIR: ${{ steps.copilot.outputs.verification-tmpdir }} + - name: Continue repository verification 1 + id: verification_continue_1 + if: ${{ always() && steps.copilot.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.copilot.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_1.outcome }} + - name: Prepare repository verification 2 + id: verification_setup_2 + if: ${{ !cancelled() && steps.verification_continue_1.outcome == 'success' && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_1.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_1.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_1.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_1.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_1.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_1.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_1.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_1.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_1.outputs.verification-tmpdir }} + - name: Continue repository verification 2 + id: verification_continue_2 + if: ${{ always() && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_1.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_2.outcome }} + - name: Prepare repository verification 3 + id: verification_setup_3 + if: ${{ !cancelled() && steps.verification_continue_2.outcome == 'success' && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_2.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_2.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_2.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_2.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_2.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_2.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_2.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_2.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_2.outputs.verification-tmpdir }} + - name: Continue repository verification 3 + id: verification_continue_3 + if: ${{ always() && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_2.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_3.outcome }} + - name: Prepare repository verification 4 + id: verification_setup_4 + if: ${{ !cancelled() && steps.verification_continue_3.outcome == 'success' && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_3.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_3.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_3.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_3.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_3.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_3.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_3.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_3.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_3.outputs.verification-tmpdir }} + - name: Continue repository verification 4 + id: verification_continue_4 + if: ${{ always() && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_3.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_4.outcome }} + - name: Prepare repository verification 5 + id: verification_setup_5 + if: ${{ !cancelled() && steps.verification_continue_4.outcome == 'success' && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_4.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_4.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_4.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_4.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_4.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_4.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_4.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_4.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_4.outputs.verification-tmpdir }} + - name: Continue repository verification 5 + id: verification_continue_5 + if: ${{ always() && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_4.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_5.outcome }} + - name: Prepare repository verification 6 + id: verification_setup_6 + if: ${{ !cancelled() && steps.verification_continue_5.outcome == 'success' && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_5.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_5.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_5.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_5.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_5.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_5.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_5.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_5.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_5.outputs.verification-tmpdir }} + - name: Continue repository verification 6 + id: verification_continue_6 + if: ${{ always() && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_5.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_6.outcome }} + - name: Prepare repository verification 7 + id: verification_setup_7 + if: ${{ !cancelled() && steps.verification_continue_6.outcome == 'success' && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_6.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_6.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_6.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_6.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_6.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_6.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_6.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_6.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_6.outputs.verification-tmpdir }} + - name: Continue repository verification 7 + id: verification_continue_7 + if: ${{ always() && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_6.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_7.outcome }} + - name: Prepare repository verification 8 + id: verification_setup_8 + if: ${{ !cancelled() && steps.verification_continue_7.outcome == 'success' && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_7.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_7.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_7.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_7.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_7.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_7.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_7.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_7.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_7.outputs.verification-tmpdir }} + - name: Continue repository verification 8 + id: verification_continue_8 + if: ${{ always() && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_7.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_8.outcome }} + - name: Prepare repository verification 9 + id: verification_setup_9 + if: ${{ !cancelled() && steps.verification_continue_8.outcome == 'success' && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_8.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_8.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_8.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_8.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_8.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_8.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_8.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_8.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_8.outputs.verification-tmpdir }} + - name: Continue repository verification 9 + id: verification_continue_9 + if: ${{ always() && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_8.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_9.outcome }} + - name: Prepare repository verification 10 + id: verification_setup_10 + if: ${{ !cancelled() && steps.verification_continue_9.outcome == 'success' && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_9.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_9.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_9.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_9.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_9.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_9.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_9.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_9.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_9.outputs.verification-tmpdir }} + - name: Continue repository verification 10 + id: verification_continue_10 + if: ${{ always() && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_9.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_10.outcome }} + - name: Prepare repository verification 11 + id: verification_setup_11 + if: ${{ !cancelled() && steps.verification_continue_10.outcome == 'success' && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_10.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_10.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_10.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_10.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_10.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_10.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_10.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_10.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_10.outputs.verification-tmpdir }} + - name: Continue repository verification 11 + id: verification_continue_11 + if: ${{ always() && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_10.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_11.outcome }} + - name: Prepare repository verification 12 + id: verification_setup_12 + if: ${{ !cancelled() && steps.verification_continue_11.outcome == 'success' && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_11.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_11.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_11.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_11.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_11.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_11.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_11.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_11.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_11.outputs.verification-tmpdir }} + - name: Continue repository verification 12 + id: verification_continue_12 + if: ${{ always() && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_11.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_12.outcome }} + - name: Prepare repository verification 13 + id: verification_setup_13 + if: ${{ !cancelled() && steps.verification_continue_12.outcome == 'success' && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_12.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_12.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_12.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_12.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_12.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_12.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_12.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_12.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_12.outputs.verification-tmpdir }} + - name: Continue repository verification 13 + id: verification_continue_13 + if: ${{ always() && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_12.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_13.outcome }} + - name: Prepare repository verification 14 + id: verification_setup_14 + if: ${{ !cancelled() && steps.verification_continue_13.outcome == 'success' && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_13.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_13.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_13.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_13.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_13.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_13.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_13.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_13.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_13.outputs.verification-tmpdir }} + - name: Continue repository verification 14 + id: verification_continue_14 + if: ${{ always() && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_13.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_14.outcome }} + - name: Prepare repository verification 15 + id: verification_setup_15 + if: ${{ !cancelled() && steps.verification_continue_14.outcome == 'success' && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_14.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_14.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_14.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_14.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_14.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_14.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_14.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_14.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_14.outputs.verification-tmpdir }} + - name: Continue repository verification 15 + id: verification_continue_15 + if: ${{ always() && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_14.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_15.outcome }} + - name: Prepare repository verification 16 + id: verification_setup_16 + if: ${{ !cancelled() && steps.verification_continue_15.outcome == 'success' && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_15.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_15.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_15.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_15.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_15.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_15.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_15.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_15.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_15.outputs.verification-tmpdir }} + - name: Continue repository verification 16 + id: verification_continue_16 + if: ${{ always() && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_15.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_16.outcome }} + - name: Prepare repository verification 17 + id: verification_setup_17 + if: ${{ !cancelled() && steps.verification_continue_16.outcome == 'success' && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_16.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_16.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_16.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_16.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_16.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_16.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_16.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_16.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_16.outputs.verification-tmpdir }} + - name: Continue repository verification 17 + id: verification_continue_17 + if: ${{ always() && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_16.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_17.outcome }} + - name: Prepare repository verification 18 + id: verification_setup_18 + if: ${{ !cancelled() && steps.verification_continue_17.outcome == 'success' && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_17.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_17.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_17.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_17.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_17.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_17.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_17.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_17.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_17.outputs.verification-tmpdir }} + - name: Continue repository verification 18 + id: verification_continue_18 + if: ${{ always() && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_17.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_18.outcome }} + - name: Prepare repository verification 19 + id: verification_setup_19 + if: ${{ !cancelled() && steps.verification_continue_18.outcome == 'success' && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_18.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_18.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_18.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_18.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_18.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_18.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_18.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_18.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_18.outputs.verification-tmpdir }} + - name: Continue repository verification 19 + id: verification_continue_19 + if: ${{ always() && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_18.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_19.outcome }} + - name: Prepare repository verification 20 + id: verification_setup_20 + if: ${{ !cancelled() && steps.verification_continue_19.outcome == 'success' && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_19.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_19.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_19.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_19.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_19.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_19.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_19.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_19.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_19.outputs.verification-tmpdir }} + - name: Continue repository verification 20 + id: verification_continue_20 + if: ${{ always() && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_19.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_20.outcome }} + - name: Complete repository verification + id: verification_complete + if: ${{ always() && steps.copilot.outputs.verification-ticket != '' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: finish + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + # copilot:native-verification:end diff --git a/setup/workflows/copilot_pull_request.yml b/setup/workflows/copilot_pull_request.yml index 76e3078fd..1127ec283 100644 --- a/setup/workflows/copilot_pull_request.yml +++ b/setup/workflows/copilot_pull_request.yml @@ -39,7 +39,7 @@ jobs: # Before Copilot, provision project runtimes/dependencies in this same job when # repository verification is configured. A separate CI job does not prepare it. # Declare reviewed versions in .copilot/verification-prerequisites.json. - # Optional trusted-main setup: VERIFICATION_SETUP=true and .copilot/verification-setup.mjs. + # Optional trusted-main setup: VERIFICATION_SETUP=true and .github/actions/copilot-verification-setup/action.yml. # https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites - uses: vypdev/copilot@v3 with: diff --git a/setup/workflows/copilot_pull_request_comment.yml b/setup/workflows/copilot_pull_request_comment.yml index b95c3634e..a9dc43339 100644 --- a/setup/workflows/copilot_pull_request_comment.yml +++ b/setup/workflows/copilot_pull_request_comment.yml @@ -23,10 +23,11 @@ jobs: # Before Copilot, provision project runtimes/dependencies in this same job when # repository verification is configured. A separate CI job does not prepare it. # Declare reviewed versions in .copilot/verification-prerequisites.json. - # Optional trusted-main setup: VERIFICATION_SETUP=true and .copilot/verification-setup.mjs. + # Optional trusted-main setup: VERIFICATION_SETUP=true and .github/actions/copilot-verification-setup/action.yml. # https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites - uses: vypdev/copilot@v3 - with: + id: copilot + with: &copilot-inputs repository-locale: ${{ vars.REPOSITORY_LOCALE || 'en-US' }} issues-locale: ${{ vars.ISSUES_LOCALE || '' }} pull-requests-locale: ${{ vars.PULL_REQUESTS_LOCALE || '' }} @@ -99,6 +100,7 @@ jobs: project-ids: ${{ vars.PROJECT_IDS }} token: ${{ secrets.PAT }} env: + COPILOT_NATIVE_PHASE: prepare AGENT_PROVIDER: ${{ vars.AGENT_PROVIDER || 'codex' }} AGENT_MODEL_PROVIDER: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} AGENT_MODEL: ${{ vars.AGENT_MODEL }} @@ -114,3 +116,514 @@ jobs: CURSOR_API_KEY: ${{ (vars.AGENT_PROVIDER == 'cursor' || vars.FINDINGS_PROVIDER == 'cursor' || vars.FIXER_PROVIDER == 'cursor' || vars.PLANNER_PROVIDER == 'cursor' || vars.REVIEWER_PROVIDER == 'cursor' || vars.TESTER_PROVIDER == 'cursor') && secrets.CURSOR_API_KEY || '' }} CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }} COPILOT_EVIDENCE_TOKEN: ${{ github.token }} + + # copilot:native-verification:start + - name: Prepare repository verification 1 + id: verification_setup_1 + if: ${{ !cancelled() && steps.copilot.outcome == 'success' && steps.copilot.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.copilot.outputs.verification-home }} + USERPROFILE: ${{ steps.copilot.outputs.verification-userprofile }} + APPDATA: ${{ steps.copilot.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.copilot.outputs.verification-localappdata }} + PATH: ${{ steps.copilot.outputs.verification-path }} + JAVA_HOME: ${{ steps.copilot.outputs.verification-java_home }} + TMP: ${{ steps.copilot.outputs.verification-tmp }} + TEMP: ${{ steps.copilot.outputs.verification-temp }} + TMPDIR: ${{ steps.copilot.outputs.verification-tmpdir }} + - name: Continue repository verification 1 + id: verification_continue_1 + if: ${{ always() && steps.copilot.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.copilot.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_1.outcome }} + - name: Prepare repository verification 2 + id: verification_setup_2 + if: ${{ !cancelled() && steps.verification_continue_1.outcome == 'success' && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_1.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_1.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_1.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_1.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_1.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_1.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_1.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_1.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_1.outputs.verification-tmpdir }} + - name: Continue repository verification 2 + id: verification_continue_2 + if: ${{ always() && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_1.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_2.outcome }} + - name: Prepare repository verification 3 + id: verification_setup_3 + if: ${{ !cancelled() && steps.verification_continue_2.outcome == 'success' && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_2.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_2.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_2.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_2.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_2.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_2.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_2.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_2.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_2.outputs.verification-tmpdir }} + - name: Continue repository verification 3 + id: verification_continue_3 + if: ${{ always() && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_2.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_3.outcome }} + - name: Prepare repository verification 4 + id: verification_setup_4 + if: ${{ !cancelled() && steps.verification_continue_3.outcome == 'success' && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_3.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_3.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_3.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_3.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_3.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_3.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_3.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_3.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_3.outputs.verification-tmpdir }} + - name: Continue repository verification 4 + id: verification_continue_4 + if: ${{ always() && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_3.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_4.outcome }} + - name: Prepare repository verification 5 + id: verification_setup_5 + if: ${{ !cancelled() && steps.verification_continue_4.outcome == 'success' && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_4.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_4.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_4.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_4.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_4.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_4.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_4.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_4.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_4.outputs.verification-tmpdir }} + - name: Continue repository verification 5 + id: verification_continue_5 + if: ${{ always() && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_4.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_5.outcome }} + - name: Prepare repository verification 6 + id: verification_setup_6 + if: ${{ !cancelled() && steps.verification_continue_5.outcome == 'success' && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_5.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_5.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_5.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_5.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_5.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_5.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_5.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_5.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_5.outputs.verification-tmpdir }} + - name: Continue repository verification 6 + id: verification_continue_6 + if: ${{ always() && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_5.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_6.outcome }} + - name: Prepare repository verification 7 + id: verification_setup_7 + if: ${{ !cancelled() && steps.verification_continue_6.outcome == 'success' && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_6.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_6.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_6.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_6.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_6.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_6.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_6.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_6.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_6.outputs.verification-tmpdir }} + - name: Continue repository verification 7 + id: verification_continue_7 + if: ${{ always() && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_6.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_7.outcome }} + - name: Prepare repository verification 8 + id: verification_setup_8 + if: ${{ !cancelled() && steps.verification_continue_7.outcome == 'success' && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_7.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_7.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_7.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_7.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_7.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_7.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_7.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_7.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_7.outputs.verification-tmpdir }} + - name: Continue repository verification 8 + id: verification_continue_8 + if: ${{ always() && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_7.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_8.outcome }} + - name: Prepare repository verification 9 + id: verification_setup_9 + if: ${{ !cancelled() && steps.verification_continue_8.outcome == 'success' && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_8.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_8.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_8.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_8.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_8.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_8.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_8.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_8.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_8.outputs.verification-tmpdir }} + - name: Continue repository verification 9 + id: verification_continue_9 + if: ${{ always() && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_8.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_9.outcome }} + - name: Prepare repository verification 10 + id: verification_setup_10 + if: ${{ !cancelled() && steps.verification_continue_9.outcome == 'success' && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_9.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_9.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_9.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_9.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_9.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_9.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_9.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_9.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_9.outputs.verification-tmpdir }} + - name: Continue repository verification 10 + id: verification_continue_10 + if: ${{ always() && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_9.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_10.outcome }} + - name: Prepare repository verification 11 + id: verification_setup_11 + if: ${{ !cancelled() && steps.verification_continue_10.outcome == 'success' && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_10.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_10.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_10.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_10.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_10.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_10.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_10.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_10.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_10.outputs.verification-tmpdir }} + - name: Continue repository verification 11 + id: verification_continue_11 + if: ${{ always() && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_10.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_11.outcome }} + - name: Prepare repository verification 12 + id: verification_setup_12 + if: ${{ !cancelled() && steps.verification_continue_11.outcome == 'success' && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_11.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_11.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_11.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_11.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_11.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_11.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_11.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_11.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_11.outputs.verification-tmpdir }} + - name: Continue repository verification 12 + id: verification_continue_12 + if: ${{ always() && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_11.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_12.outcome }} + - name: Prepare repository verification 13 + id: verification_setup_13 + if: ${{ !cancelled() && steps.verification_continue_12.outcome == 'success' && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_12.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_12.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_12.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_12.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_12.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_12.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_12.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_12.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_12.outputs.verification-tmpdir }} + - name: Continue repository verification 13 + id: verification_continue_13 + if: ${{ always() && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_12.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_13.outcome }} + - name: Prepare repository verification 14 + id: verification_setup_14 + if: ${{ !cancelled() && steps.verification_continue_13.outcome == 'success' && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_13.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_13.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_13.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_13.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_13.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_13.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_13.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_13.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_13.outputs.verification-tmpdir }} + - name: Continue repository verification 14 + id: verification_continue_14 + if: ${{ always() && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_13.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_14.outcome }} + - name: Prepare repository verification 15 + id: verification_setup_15 + if: ${{ !cancelled() && steps.verification_continue_14.outcome == 'success' && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_14.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_14.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_14.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_14.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_14.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_14.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_14.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_14.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_14.outputs.verification-tmpdir }} + - name: Continue repository verification 15 + id: verification_continue_15 + if: ${{ always() && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_14.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_15.outcome }} + - name: Prepare repository verification 16 + id: verification_setup_16 + if: ${{ !cancelled() && steps.verification_continue_15.outcome == 'success' && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_15.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_15.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_15.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_15.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_15.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_15.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_15.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_15.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_15.outputs.verification-tmpdir }} + - name: Continue repository verification 16 + id: verification_continue_16 + if: ${{ always() && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_15.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_16.outcome }} + - name: Prepare repository verification 17 + id: verification_setup_17 + if: ${{ !cancelled() && steps.verification_continue_16.outcome == 'success' && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_16.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_16.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_16.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_16.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_16.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_16.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_16.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_16.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_16.outputs.verification-tmpdir }} + - name: Continue repository verification 17 + id: verification_continue_17 + if: ${{ always() && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_16.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_17.outcome }} + - name: Prepare repository verification 18 + id: verification_setup_18 + if: ${{ !cancelled() && steps.verification_continue_17.outcome == 'success' && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_17.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_17.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_17.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_17.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_17.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_17.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_17.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_17.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_17.outputs.verification-tmpdir }} + - name: Continue repository verification 18 + id: verification_continue_18 + if: ${{ always() && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_17.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_18.outcome }} + - name: Prepare repository verification 19 + id: verification_setup_19 + if: ${{ !cancelled() && steps.verification_continue_18.outcome == 'success' && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_18.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_18.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_18.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_18.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_18.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_18.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_18.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_18.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_18.outputs.verification-tmpdir }} + - name: Continue repository verification 19 + id: verification_continue_19 + if: ${{ always() && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_18.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_19.outcome }} + - name: Prepare repository verification 20 + id: verification_setup_20 + if: ${{ !cancelled() && steps.verification_continue_19.outcome == 'success' && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} + uses: ./.github/actions/copilot-verification-setup + timeout-minutes: 5 + env: + HOME: ${{ steps.verification_continue_19.outputs.verification-home }} + USERPROFILE: ${{ steps.verification_continue_19.outputs.verification-userprofile }} + APPDATA: ${{ steps.verification_continue_19.outputs.verification-appdata }} + LOCALAPPDATA: ${{ steps.verification_continue_19.outputs.verification-localappdata }} + PATH: ${{ steps.verification_continue_19.outputs.verification-path }} + JAVA_HOME: ${{ steps.verification_continue_19.outputs.verification-java_home }} + TMP: ${{ steps.verification_continue_19.outputs.verification-tmp }} + TEMP: ${{ steps.verification_continue_19.outputs.verification-temp }} + TMPDIR: ${{ steps.verification_continue_19.outputs.verification-tmpdir }} + - name: Continue repository verification 20 + id: verification_continue_20 + if: ${{ always() && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_19.outputs.verification-request }} + COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_20.outcome }} + - name: Complete repository verification + id: verification_complete + if: ${{ always() && steps.copilot.outputs.verification-ticket != '' }} + uses: vypdev/copilot@v3 + with: *copilot-inputs + env: + COPILOT_NATIVE_PHASE: finish + COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} + # copilot:native-verification:end diff --git a/setup/workflows/copilot_pull_request_review_state.yml b/setup/workflows/copilot_pull_request_review_state.yml index e74e89338..8942d1d7f 100644 --- a/setup/workflows/copilot_pull_request_review_state.yml +++ b/setup/workflows/copilot_pull_request_review_state.yml @@ -27,7 +27,7 @@ jobs: # Before Copilot, provision project runtimes/dependencies in this same job when # repository verification is configured. A separate CI job does not prepare it. # Declare reviewed versions in .copilot/verification-prerequisites.json. - # Optional trusted-main setup: VERIFICATION_SETUP=true and .copilot/verification-setup.mjs. + # Optional trusted-main setup: VERIFICATION_SETUP=true and .github/actions/copilot-verification-setup/action.yml. # https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites - uses: vypdev/copilot@v3 with: diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 939837df2..eebafa586 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -24,7 +24,7 @@ debt or convert unknown historic intent into a design decision. | `managed-issue-lifecycle` | As-built baseline | Convert typed issues into traceable work branches, project state, and lifecycle state | [Managed issue and branch lifecycle](./managed-issue-and-branch-lifecycle.md) | 71 paths · 2026-10-20 | | `comment-automation` | Implemented | Admit only explicit commands or exact mentions, then route them while protecting repository mutations | [Comment automation and authorization](./comment-automation-and-authorization.md) | 79 paths · 2026-10-11 | | `bugbot-analysis-and-autofix` | Implemented | Select one canonical PR, exhaustively analyze its bounded diff partitions, publish stable findings atomically, and apply authorized verified fixes | [Bugbot analysis, finding publication, and autofix](./bugbot-analysis-publication-and-autofix.md) + 2 companion | 109 paths · 2026-10-13 | -| `branch-synchronization` | Implemented | Observe parent drift with one localized status card and transition-only notifications, then safely merge a parent branch into a linked working branch | [Branch synchronization and conflict recovery](./branch-synchronization-and-conflict-recovery.md) + 2 companion | 101 paths · 2026-10-21 | +| `branch-synchronization` | Implemented | Observe parent drift with one localized status card and transition-only notifications, then safely merge a parent branch into a linked working branch | [Branch synchronization and conflict recovery](./branch-synchronization-and-conflict-recovery.md) + 2 companion | 113 paths · 2026-10-21 | | `pull-request-lifecycle` | Implemented | Enrich linked and unlinked pull requests with safe issue linkage, projects, metadata, reviewers, concise descriptions, and distinct workflow evidence | [Pull request lifecycle and enrichment](./pull-request-lifecycle-and-enrichment.md) | 77 paths · 2026-10-20 | | `agent-runtime` | Implemented | Resolve, provision, authenticate, authorize, and execute only the agent roles reachable by a run | [Agent runtime, provider, model, and role routing](./agent-runtime-provider-and-model-routing.md) + 1 companion | 99 paths · 2026-10-17 | | `cli-and-single-actions` | As-built baseline | Expose bounded local commands and workflow-dispatched operations through the shared application core | [CLI and single-action execution](./cli-and-single-action-execution.md) | 44 paths · 2026-10-19 | @@ -195,8 +195,8 @@ debt or convert unknown historic intent into a design decision. - Specifications: [`specs/branch-synchronization-and-conflict-recovery.md`](./branch-synchronization-and-conflict-recovery.md) · [`specs/repository-verification-runtime-prerequisites.md`](./repository-verification-runtime-prerequisites.md) · [`specs/repository-verification-setup-hooks.md`](./repository-verification-setup-hooks.md) - Workflows: [`.github/workflows/copilot_branch_sync.yml`](../.github/workflows/copilot_branch_sync.yml) · [`setup/workflows/copilot_branch_sync.yml`](../setup/workflows/copilot_branch_sync.yml) · [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`setup/workflows/copilot_issue.yml`](../setup/workflows/copilot_issue.yml) · [`setup/workflows/copilot_issue_comment.yml`](../setup/workflows/copilot_issue_comment.yml) · [`setup/workflows/copilot_pull_request_comment.yml`](../setup/workflows/copilot_pull_request_comment.yml) - Entrypoints: [`src/application/usecases/branch_sync/sync_branch_use_case.ts`](../src/application/usecases/branch_sync/sync_branch_use_case.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) -- Core code: [`src/domain/branch_sync_command.ts`](../src/domain/branch_sync_command.ts) · [`src/application/ports/branch_sync_ports.ts`](../src/application/ports/branch_sync_ports.ts) · [`src/application/policies/branch_sync_message_catalog.ts`](../src/application/policies/branch_sync_message_catalog.ts) · [`src/application/policies/branch_sync_notification_policy.ts`](../src/application/policies/branch_sync_notification_policy.ts) · [`src/application/usecases/branch_sync/branch_sync_execution_policy.ts`](../src/application/usecases/branch_sync/branch_sync_execution_policy.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/application/usecases/steps/common/transition_notification_workflow.ts`](../src/application/usecases/steps/common/transition_notification_workflow.ts) · [`src/data/repository/branch_sync/branch_dependency_policy.ts`](../src/data/repository/branch_sync/branch_dependency_policy.ts) · [`src/data/repository/branch_sync/branch_dependency_repository.ts`](../src/data/repository/branch_sync/branch_dependency_repository.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/infrastructure/composition/push_single_action_capability_port_binding.ts`](../src/infrastructure/composition/push_single_action_capability_port_binding.ts) · [`src/infrastructure/branch_sync_workspace_adapter.ts`](../src/infrastructure/branch_sync_workspace_adapter.ts) · [`src/prompts/branch_sync_conflicts.ts`](../src/prompts/branch_sync_conflicts.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`src/infrastructure/branch_sync_sdd_catalog.ts`](../src/infrastructure/branch_sync_sdd_catalog.ts) · [`src/domain/branch_sync_validation.ts`](../src/domain/branch_sync_validation.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/infrastructure/branch_sync_communication_budget.ts`](../src/infrastructure/branch_sync_communication_budget.ts) · [`src/application/usecases/actions/retire_branch_sync_use_case.ts`](../src/application/usecases/actions/retire_branch_sync_use_case.ts) · [`src/application/ports/issue_completion_ports.ts`](../src/application/ports/issue_completion_ports.ts) · [`src/application/usecases/pull_request_workflow.ts`](../src/application/usecases/pull_request_workflow.ts) · [`src/application/usecases/pull_request_workflow_context.ts`](../src/application/usecases/pull_request_workflow_context.ts) · [`src/infrastructure/composition/pull_request_use_case_composition_root.ts`](../src/infrastructure/composition/pull_request_use_case_composition_root.ts) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) · [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`scripts/validate-workflow-contract.cjs`](../scripts/validate-workflow-contract.cjs) · [`src/application/ports/reply_request_read_ports.ts`](../src/application/ports/reply_request_read_ports.ts) · [`src/domain/reply_request_digest.ts`](../src/domain/reply_request_digest.ts) · [`src/infrastructure/composition/reply_request_composition_root.ts`](../src/infrastructure/composition/reply_request_composition_root.ts) · [`src/data/repository/reply_request_repository.ts`](../src/data/repository/reply_request_repository.ts) · [`src/application/policies/verification_runtime_policy.ts`](../src/application/policies/verification_runtime_policy.ts) · [`src/infrastructure/verification_runtime_manifest.ts`](../src/infrastructure/verification_runtime_manifest.ts) · [`src/infrastructure/verification_runtime_preflight.ts`](../src/infrastructure/verification_runtime_preflight.ts) · [`src/infrastructure/untrusted_command_environment.ts`](../src/infrastructure/untrusted_command_environment.ts) · [`src/application/ports/git_ports.ts`](../src/application/ports/git_ports.ts) · [`src/application/ports/bugbot_git_ports.ts`](../src/application/ports/bugbot_git_ports.ts) · [`src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts`](../src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts) · [`src/application/usecases/steps/commit/bugbot/verify_command_runner.ts`](../src/application/usecases/steps/commit/bugbot/verify_command_runner.ts) · [`src/domain/closed_issue_reconciliation.ts`](../src/domain/closed_issue_reconciliation.ts) · [`src/application/errors/closed_issue_reconciliation_error.ts`](../src/application/errors/closed_issue_reconciliation_error.ts) · [`src/application/policies/verification_setup_policy.ts`](../src/application/policies/verification_setup_policy.ts) · [`src/application/ports/verification_setup_ports.ts`](../src/application/ports/verification_setup_ports.ts) · [`src/infrastructure/verification_setup_snapshot.ts`](../src/infrastructure/verification_setup_snapshot.ts) · [`src/infrastructure/verification_setup_result.ts`](../src/infrastructure/verification_setup_result.ts) · [`src/infrastructure/verification_setup_process.ts`](../src/infrastructure/verification_setup_process.ts) · [`src/infrastructure/verification_setup_runner.ts`](../src/infrastructure/verification_setup_runner.ts) -- Tests: [`src/application/policies/__tests__/branch_sync_notification_policy.test.ts`](../src/application/policies/__tests__/branch_sync_notification_policy.test.ts) · [`src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts) · [`src/application/usecases/branch_sync/__tests__/sync_branch_use_case.test.ts`](../src/application/usecases/branch_sync/__tests__/sync_branch_use_case.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/application/usecases/steps/common/__tests__/transition_notification_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/transition_notification_workflow.test.ts) · [`src/data/repository/branch_sync/__tests__/branch_dependency_policy.test.ts`](../src/data/repository/branch_sync/__tests__/branch_dependency_policy.test.ts) · [`src/data/repository/branch_sync/__tests__/branch_dependency_repository.test.ts`](../src/data/repository/branch_sync/__tests__/branch_dependency_repository.test.ts) · [`src/infrastructure/__tests__/branch_sync_workspace_adapter.test.ts`](../src/infrastructure/__tests__/branch_sync_workspace_adapter.test.ts) · [`src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts) · [`src/application/usecases/branch_sync/__tests__/branch_sync_execution_policy.test.ts`](../src/application/usecases/branch_sync/__tests__/branch_sync_execution_policy.test.ts) · [`src/application/errors/__tests__/application_error.test.ts`](../src/application/errors/__tests__/application_error.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/infrastructure/__tests__/git_commit_adapter.test.ts`](../src/infrastructure/__tests__/git_commit_adapter.test.ts) · [`src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts`](../src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts) · [`src/infrastructure/__tests__/branch_sync_generated_bundle_validation.test.ts`](../src/infrastructure/__tests__/branch_sync_generated_bundle_validation.test.ts) · [`src/application/policies/__tests__/branch_sync_lifecycle_recovery.test.ts`](../src/application/policies/__tests__/branch_sync_lifecycle_recovery.test.ts) · [`src/infrastructure/__tests__/branch_sync_resolved_workspace.test.ts`](../src/infrastructure/__tests__/branch_sync_resolved_workspace.test.ts) · [`src/infrastructure/__tests__/branch_sync_communication_budget.test.ts`](../src/infrastructure/__tests__/branch_sync_communication_budget.test.ts) · [`src/application/usecases/actions/__tests__/retire_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/retire_branch_sync_use_case.test.ts) · [`src/application/usecases/__tests__/pull_request_use_case.test.ts`](../src/application/usecases/__tests__/pull_request_use_case.test.ts) · [`src/actions/__tests__/common_action.test.ts`](../src/actions/__tests__/common_action.test.ts) · [`src/actions/__tests__/github_action.test.ts`](../src/actions/__tests__/github_action.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) · [`src/data/repository/__tests__/reply_request_repository.test.ts`](../src/data/repository/__tests__/reply_request_repository.test.ts) · [`src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts`](../src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts) · [`src/application/policies/__tests__/verification_runtime_policy.test.ts`](../src/application/policies/__tests__/verification_runtime_policy.test.ts) · [`src/infrastructure/__tests__/verification_runtime_preflight.test.ts`](../src/infrastructure/__tests__/verification_runtime_preflight.test.ts) · [`src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts`](../src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts) · [`src/infrastructure/__tests__/untrusted_command_environment.test.ts`](../src/infrastructure/__tests__/untrusted_command_environment.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts) · [`src/tooling/__tests__/verification_runtime_guidance.test.ts`](../src/tooling/__tests__/verification_runtime_guidance.test.ts) · [`src/application/policies/__tests__/verification_setup_policy.test.ts`](../src/application/policies/__tests__/verification_setup_policy.test.ts) · [`src/infrastructure/__tests__/verification_setup_snapshot.test.ts`](../src/infrastructure/__tests__/verification_setup_snapshot.test.ts) · [`src/infrastructure/__tests__/verification_setup_runner.test.ts`](../src/infrastructure/__tests__/verification_setup_runner.test.ts) · [`src/infrastructure/__tests__/verification_setup_configuration.test.ts`](../src/infrastructure/__tests__/verification_setup_configuration.test.ts) · [`src/infrastructure/__tests__/verification_setup_sync.test.ts`](../src/infrastructure/__tests__/verification_setup_sync.test.ts) +- Core code: [`src/domain/branch_sync_command.ts`](../src/domain/branch_sync_command.ts) · [`src/application/ports/branch_sync_ports.ts`](../src/application/ports/branch_sync_ports.ts) · [`src/application/policies/branch_sync_message_catalog.ts`](../src/application/policies/branch_sync_message_catalog.ts) · [`src/application/policies/branch_sync_notification_policy.ts`](../src/application/policies/branch_sync_notification_policy.ts) · [`src/application/usecases/branch_sync/branch_sync_execution_policy.ts`](../src/application/usecases/branch_sync/branch_sync_execution_policy.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/application/usecases/steps/common/transition_notification_workflow.ts`](../src/application/usecases/steps/common/transition_notification_workflow.ts) · [`src/data/repository/branch_sync/branch_dependency_policy.ts`](../src/data/repository/branch_sync/branch_dependency_policy.ts) · [`src/data/repository/branch_sync/branch_dependency_repository.ts`](../src/data/repository/branch_sync/branch_dependency_repository.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/infrastructure/composition/push_single_action_capability_port_binding.ts`](../src/infrastructure/composition/push_single_action_capability_port_binding.ts) · [`src/infrastructure/branch_sync_workspace_adapter.ts`](../src/infrastructure/branch_sync_workspace_adapter.ts) · [`src/prompts/branch_sync_conflicts.ts`](../src/prompts/branch_sync_conflicts.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`src/infrastructure/branch_sync_sdd_catalog.ts`](../src/infrastructure/branch_sync_sdd_catalog.ts) · [`src/domain/branch_sync_validation.ts`](../src/domain/branch_sync_validation.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/infrastructure/branch_sync_communication_budget.ts`](../src/infrastructure/branch_sync_communication_budget.ts) · [`src/application/usecases/actions/retire_branch_sync_use_case.ts`](../src/application/usecases/actions/retire_branch_sync_use_case.ts) · [`src/application/ports/issue_completion_ports.ts`](../src/application/ports/issue_completion_ports.ts) · [`src/application/usecases/pull_request_workflow.ts`](../src/application/usecases/pull_request_workflow.ts) · [`src/application/usecases/pull_request_workflow_context.ts`](../src/application/usecases/pull_request_workflow_context.ts) · [`src/infrastructure/composition/pull_request_use_case_composition_root.ts`](../src/infrastructure/composition/pull_request_use_case_composition_root.ts) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) · [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`scripts/validate-workflow-contract.cjs`](../scripts/validate-workflow-contract.cjs) · [`src/application/ports/reply_request_read_ports.ts`](../src/application/ports/reply_request_read_ports.ts) · [`src/domain/reply_request_digest.ts`](../src/domain/reply_request_digest.ts) · [`src/infrastructure/composition/reply_request_composition_root.ts`](../src/infrastructure/composition/reply_request_composition_root.ts) · [`src/data/repository/reply_request_repository.ts`](../src/data/repository/reply_request_repository.ts) · [`src/application/policies/verification_runtime_policy.ts`](../src/application/policies/verification_runtime_policy.ts) · [`src/infrastructure/verification_runtime_manifest.ts`](../src/infrastructure/verification_runtime_manifest.ts) · [`src/infrastructure/verification_runtime_preflight.ts`](../src/infrastructure/verification_runtime_preflight.ts) · [`src/infrastructure/untrusted_command_environment.ts`](../src/infrastructure/untrusted_command_environment.ts) · [`src/application/ports/git_ports.ts`](../src/application/ports/git_ports.ts) · [`src/application/ports/bugbot_git_ports.ts`](../src/application/ports/bugbot_git_ports.ts) · [`src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts`](../src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts) · [`src/application/usecases/steps/commit/bugbot/verify_command_runner.ts`](../src/application/usecases/steps/commit/bugbot/verify_command_runner.ts) · [`src/domain/closed_issue_reconciliation.ts`](../src/domain/closed_issue_reconciliation.ts) · [`src/application/errors/closed_issue_reconciliation_error.ts`](../src/application/errors/closed_issue_reconciliation_error.ts) · [`src/application/policies/verification_setup_policy.ts`](../src/application/policies/verification_setup_policy.ts) · [`src/application/ports/verification_setup_ports.ts`](../src/application/ports/verification_setup_ports.ts) · [`src/infrastructure/verification_setup_snapshot.ts`](../src/infrastructure/verification_setup_snapshot.ts) · [`src/infrastructure/verification_setup_result.ts`](../src/infrastructure/verification_setup_result.ts) · [`src/infrastructure/verification_setup_runner.ts`](../src/infrastructure/verification_setup_runner.ts) · [`src/infrastructure/native_verification_protocol.ts`](../src/infrastructure/native_verification_protocol.ts) · [`src/infrastructure/native_verification_candidate.ts`](../src/infrastructure/native_verification_candidate.ts) · [`src/infrastructure/native_verification_overlay.ts`](../src/infrastructure/native_verification_overlay.ts) · [`src/infrastructure/native_verification_controller.ts`](../src/infrastructure/native_verification_controller.ts) · [`src/infrastructure/native_verification_profiles.ts`](../src/infrastructure/native_verification_profiles.ts) · [`src/infrastructure/native_verification_redaction.ts`](../src/infrastructure/native_verification_redaction.ts) · [`scripts/generate-native-verification-workflows.cjs`](../scripts/generate-native-verification-workflows.cjs) +- Tests: [`src/application/policies/__tests__/branch_sync_notification_policy.test.ts`](../src/application/policies/__tests__/branch_sync_notification_policy.test.ts) · [`src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts) · [`src/application/usecases/branch_sync/__tests__/sync_branch_use_case.test.ts`](../src/application/usecases/branch_sync/__tests__/sync_branch_use_case.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/application/usecases/steps/common/__tests__/transition_notification_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/transition_notification_workflow.test.ts) · [`src/data/repository/branch_sync/__tests__/branch_dependency_policy.test.ts`](../src/data/repository/branch_sync/__tests__/branch_dependency_policy.test.ts) · [`src/data/repository/branch_sync/__tests__/branch_dependency_repository.test.ts`](../src/data/repository/branch_sync/__tests__/branch_dependency_repository.test.ts) · [`src/infrastructure/__tests__/branch_sync_workspace_adapter.test.ts`](../src/infrastructure/__tests__/branch_sync_workspace_adapter.test.ts) · [`src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts) · [`src/application/usecases/branch_sync/__tests__/branch_sync_execution_policy.test.ts`](../src/application/usecases/branch_sync/__tests__/branch_sync_execution_policy.test.ts) · [`src/application/errors/__tests__/application_error.test.ts`](../src/application/errors/__tests__/application_error.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/infrastructure/__tests__/git_commit_adapter.test.ts`](../src/infrastructure/__tests__/git_commit_adapter.test.ts) · [`src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts`](../src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts) · [`src/infrastructure/__tests__/branch_sync_generated_bundle_validation.test.ts`](../src/infrastructure/__tests__/branch_sync_generated_bundle_validation.test.ts) · [`src/application/policies/__tests__/branch_sync_lifecycle_recovery.test.ts`](../src/application/policies/__tests__/branch_sync_lifecycle_recovery.test.ts) · [`src/infrastructure/__tests__/branch_sync_resolved_workspace.test.ts`](../src/infrastructure/__tests__/branch_sync_resolved_workspace.test.ts) · [`src/infrastructure/__tests__/branch_sync_communication_budget.test.ts`](../src/infrastructure/__tests__/branch_sync_communication_budget.test.ts) · [`src/application/usecases/actions/__tests__/retire_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/retire_branch_sync_use_case.test.ts) · [`src/application/usecases/__tests__/pull_request_use_case.test.ts`](../src/application/usecases/__tests__/pull_request_use_case.test.ts) · [`src/actions/__tests__/common_action.test.ts`](../src/actions/__tests__/common_action.test.ts) · [`src/actions/__tests__/github_action.test.ts`](../src/actions/__tests__/github_action.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) · [`src/data/repository/__tests__/reply_request_repository.test.ts`](../src/data/repository/__tests__/reply_request_repository.test.ts) · [`src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts`](../src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts) · [`src/application/policies/__tests__/verification_runtime_policy.test.ts`](../src/application/policies/__tests__/verification_runtime_policy.test.ts) · [`src/infrastructure/__tests__/verification_runtime_preflight.test.ts`](../src/infrastructure/__tests__/verification_runtime_preflight.test.ts) · [`src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts`](../src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts) · [`src/infrastructure/__tests__/untrusted_command_environment.test.ts`](../src/infrastructure/__tests__/untrusted_command_environment.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts) · [`src/tooling/__tests__/verification_runtime_guidance.test.ts`](../src/tooling/__tests__/verification_runtime_guidance.test.ts) · [`src/application/policies/__tests__/verification_setup_policy.test.ts`](../src/application/policies/__tests__/verification_setup_policy.test.ts) · [`src/infrastructure/__tests__/verification_setup_snapshot.test.ts`](../src/infrastructure/__tests__/verification_setup_snapshot.test.ts) · [`src/infrastructure/__tests__/verification_setup_runner.test.ts`](../src/infrastructure/__tests__/verification_setup_runner.test.ts) · [`src/infrastructure/__tests__/verification_setup_configuration.test.ts`](../src/infrastructure/__tests__/verification_setup_configuration.test.ts) · [`src/infrastructure/__tests__/verification_setup_sync.test.ts`](../src/infrastructure/__tests__/verification_setup_sync.test.ts) · [`src/infrastructure/__tests__/native_verification_runner.test.ts`](../src/infrastructure/__tests__/native_verification_runner.test.ts) · [`src/infrastructure/__tests__/native_verification_protocol.test.ts`](../src/infrastructure/__tests__/native_verification_protocol.test.ts) · [`src/infrastructure/__tests__/native_verification_controller.test.ts`](../src/infrastructure/__tests__/native_verification_controller.test.ts) · [`src/infrastructure/__tests__/native_verification_redaction.test.ts`](../src/infrastructure/__tests__/native_verification_redaction.test.ts) · [`src/infrastructure/__tests__/native_verification_workflow.test.ts`](../src/infrastructure/__tests__/native_verification_workflow.test.ts) · [`src/infrastructure/__tests__/native_verification_profiles.test.ts`](../src/infrastructure/__tests__/native_verification_profiles.test.ts) - User documentation: [`docs/issues/branch-synchronization.mdx`](../docs/issues/branch-synchronization.mdx) · [`docs/issues/comment-commands.mdx`](../docs/issues/comment-commands.mdx) · [`docs/issues/notifications-and-auto-close.mdx`](../docs/issues/notifications-and-auto-close.mdx) · [`docs/development/architecture.mdx`](../docs/development/architecture.mdx) · [`docs/security-operations/operations/verification.mdx`](../docs/security-operations/operations/verification.mdx) · [`docs/security-operations/operations/troubleshooting.mdx`](../docs/security-operations/operations/troubleshooting.mdx) · [`docs/agents/repository-collaboration.mdx`](../docs/agents/repository-collaboration.mdx) ### `pull-request-lifecycle` — Pull request lifecycle and enrichment diff --git a/specs/catalog.json b/specs/catalog.json index 8dff921bf..56a550880 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -2006,8 +2006,14 @@ "src/application/ports/verification_setup_ports.ts", "src/infrastructure/verification_setup_snapshot.ts", "src/infrastructure/verification_setup_result.ts", - "src/infrastructure/verification_setup_process.ts", - "src/infrastructure/verification_setup_runner.ts" + "src/infrastructure/verification_setup_runner.ts", + "src/infrastructure/native_verification_protocol.ts", + "src/infrastructure/native_verification_candidate.ts", + "src/infrastructure/native_verification_overlay.ts", + "src/infrastructure/native_verification_controller.ts", + "src/infrastructure/native_verification_profiles.ts", + "src/infrastructure/native_verification_redaction.ts", + "scripts/generate-native-verification-workflows.cjs" ], "tests": [ "src/application/policies/__tests__/branch_sync_notification_policy.test.ts", @@ -2046,7 +2052,13 @@ "src/infrastructure/__tests__/verification_setup_snapshot.test.ts", "src/infrastructure/__tests__/verification_setup_runner.test.ts", "src/infrastructure/__tests__/verification_setup_configuration.test.ts", - "src/infrastructure/__tests__/verification_setup_sync.test.ts" + "src/infrastructure/__tests__/verification_setup_sync.test.ts", + "src/infrastructure/__tests__/native_verification_runner.test.ts", + "src/infrastructure/__tests__/native_verification_protocol.test.ts", + "src/infrastructure/__tests__/native_verification_controller.test.ts", + "src/infrastructure/__tests__/native_verification_redaction.test.ts", + "src/infrastructure/__tests__/native_verification_workflow.test.ts", + "src/infrastructure/__tests__/native_verification_profiles.test.ts" ], "documentation": [ "docs/issues/branch-synchronization.mdx", diff --git a/specs/repository-verification-setup-hooks.md b/specs/repository-verification-setup-hooks.md index 2f7ff5416..2a1a51f09 100644 --- a/specs/repository-verification-setup-hooks.md +++ b/specs/repository-verification-setup-hooks.md @@ -1,378 +1,282 @@ -# Repository-Owned Verification Setup Hooks +# Repository-owned native verification setup -- Status: Implementation candidate +- Status: Ready for implementation - Date: 2026-10-21 - Catalog capability ID: branch-synchronization - Last verified: 2026-10-21 - Owners: Copilot maintainers -- Scope: Define an optional repository-owned preparation helper for authorized operations that run repository verification, including branch synchronization and automated fixes. -- Related issues/PRs: [Issue #647](https://github.com/vypdev/copilot/issues/647); [verification runtime prerequisites](https://github.com/vypdev/copilot/blob/master/docs/security-operations/operations/verification.mdx#repository-verification-prerequisites) +- Scope: Prepare authorized repository verification through a trusted native composite action, without editing generated workflows. +- Related issues/PRs: [#647](https://github.com/vypdev/copilot/issues/647), [#652](https://github.com/vypdev/copilot/pull/652) - Required review gates: product UX, architecture, testing, documentation, security/operations -- Open decisions blocking readiness: None; this companion records the resolved contract for implementation review. +- Open decisions blocking readiness: none; the maintainer selected a same-job composite action on 2026-10-21. ## 1. Executive summary -Consumers may enable `verification-setup: true` and add a reviewed helper at `.copilot/verification-setup.mjs` on their trusted main/production branch. Copilot captures an immutable snapshot and invokes it in the credential-free environment immediately before each configured verification command, using the candidate tree being checked. The helper prepares only that repository’s required runtimes and dependencies. Without an enabled helper, existing prerequisite checks and verification behavior remain in force. Any enabled-helper validation, execution, timeout, or result failure blocks verification and commit/push. +A repository MAY opt into a consumer-owned `.github/actions/copilot-verification-setup/action.yml` using `runs.using: composite`. GitHub Actions MUST execute its native `uses`/`run` steps after Copilot prepares the candidate and before runtime preflight and each verification command. Copilot MUST retain its existing authorization, verification and publication gates. ```text -authorized operation -> candidate tree ready -> trusted helper snapshot prepares isolated verifier -> preflight and verification -> existing commit/push policy +Authorized operation -> prepare candidate -> native composite setup + -> credential-free runtime preflight -> verification -> publication guards -> push ``` -The safety rule is that candidate changes cannot replace the trusted helper, and setup success is never reused for a different command or candidate tree. +This replaces the unreleased Node ESM prototype in PR #652. There is no custom YAML interpreter and no reusable-workflow candidate transport. ## 2. Problem, current behavior, and evidence ### 2.1 Problem -Repository maintainers need project-specific runtimes and dependencies in the same job and isolated environment that executes Copilot verification. Generated workflows belong to Copilot; editing them for each repository creates update drift. A separate successful CI job or preparation job cannot provision the verifier’s environment. Synchronization may produce a temporary merge, and an agent may change manifests, so setup must match the actual candidate checked. +Consumer projects need different runtimes and dependencies. Editing generated Copilot workflows to install them creates maintenance drift. A separate application CI job does not provision the job doing a temporary branch merge or automated fix. ### 2.2 Current behavior -1. Copilot’s verification contract runs configured repository commands subject to existing authorization and prerequisite checks. -2. There is no specified repository-owned helper contract wired through all verification routes. -3. In the reported `vypdev/homeassistant-gateway` run, `python3 -m pytest` failed because `/usr/bin/python3` lacked `pytest`; the temporary merge was aborted without push. This establishes that run’s missing dependency, not a general runner or repository-access failure. +The existing `GitCommitAdapter` prepares a fresh credential-free environment for every verification command. Runtime prerequisites are probed before the command. `SyncBranchUseCase` validates the resulting merge and rereads remote heads before publication; the shared commit preflight validates safe workspace paths. These gates remain authoritative. ### 2.3 Evidence -- Code/workflow/documentation references: the catalog lists `.github/workflows/copilot_branch_sync.yml`, setup workflow templates for issue, issue-comment, pull-request-comment and branch-sync, and verification policy/preflight and shared commit verification code. These paths are current evidence, not a promise about future execution support. -- Incident: [consumer issue #19](https://github.com/vypdev/homeassistant-gateway/issues/19), [command comment](https://github.com/vypdev/homeassistant-gateway/issues/19#issuecomment-6311691282), [failed run](https://github.com/vypdev/homeassistant-gateway/actions/runs/39570398824), [job](https://github.com/vypdev/homeassistant-gateway/actions/runs/39570398824/job/119122814347). -- External primary sources: none required for this contract. -- Unknowns: implementation-specific workflow syntax and helper result representation must be settled consistently with repository conventions; the fixed observable limits below are resolved. +- Central boundaries: `SyncBranchUseCase.verifyPreparedMerge`, shared `runCommitAndPushPreflight`, `GitCommitAdapter.execute`. +- Only issue-comment and review-comment composition roots construct the mutation adapter. Issue/PR/push/review-state analysis callbacks may forward configuration but do not thereby execute verification. `CommitUseCase` analyzes push metadata and is not a mutation verifier. +- [Composite action metadata](https://docs.github.com/en/actions/reference/workflows-and-actions/metadata-syntax) defines native steps; [GitHub's tutorial](https://docs.github.com/en/actions/tutorials/create-actions/create-a-composite-action) documents local actions. +- Consumer incident: [homeassistant-gateway #19](https://github.com/vypdev/homeassistant-gateway/issues/19). -### 2.4 Retrospective classification (as-built baselines only) +### 2.4 Retrospective classification -Not applicable: this is a prospective specification and reconstructs no implemented baseline. +Not applicable: this is a prospective change. The earlier Node implementation is superseded, not a supported compatibility surface. ## 3. Actors, surfaces, and terminology -| Actor | Goal | Entry point | Visible surfaces | -|---|---|---|---| -| Consumer maintainer | Prepare project tools without editing generated workflows | Trusted branch helper file | Repository, verification logs, documentation | -| Issue/PR/commit actor | Request authorized implementation or autofix | Existing issue, comment, review, or commit route | Issue/PR comments and run result | -| Operator | Understand blocked verification and recovery | Workflow execution | Job summary and bounded logs | -| Copilot | Run setup and verification safely | Sync or shared commit-verification boundary | Check/run status | +| Actor | Goal | Surface | +|---|---|---| +| Repository maintainer | Review dependencies once and enable preparation | Consumer action, setup configuration, Repository Variable | +| Authorized contributor | Synchronize or request file changes | Existing issue/PR commands and result comments | +| Operator | Locate setup failure and recover | Named native Actions steps, existing result card, run logs | -“Trusted helper snapshot” means immutable regular-file bytes and SHA captured from the configured trusted main/production branch at composition/admission. “Candidate tree” means the exact workspace revision, including temporary synchronization merge or agent edits, whose manifests verification will check. “Preparation” means one helper invocation immediately before a single configured verification command and its runtime preflight. +The **trusted source** is the configured main/production remote branch captured at an immutable Git commit. The **candidate** is the actual workspace after merge/fixer changes. A **native boundary** pauses an already admitted operation; it does not authorize or replay one. -## 4. Goals +## 4. Goals, non-goals, and fixed invariants ### 4.1 Goals -1. Provide one documented optional repository-owned setup hook, invoked consistently for every authorized route that actually runs verification. -2. Prepare dependencies for the exact candidate tree in the same isolated execution environment as preflight and verification. -3. Preserve generated workflow ownership, verification policy, authorization and fail-closed commit/push behavior. +1. Let each repository declare native setup actions and shell steps. +2. Prepare the candidate's current dependencies before every configured verifier. +3. Preserve consumer YAML and auxiliary assets during setup/update. +4. Provide inspectable native steps and actionable failures. ### 4.2 Non-goals -1. Globally provisioning consumer dependencies on shared runners or editing generated event workflows per consumer. -2. Weakening verification, removing project checks, automatic setup retries, or claiming a universal OS sandbox for maintainer code. -3. Releasing Copilot as part of this feature. +Reusable workflows, job matrices owned by the consumer hook, YAML emulation, local Actions emulation, arbitrary helper paths and release/deployment changes are out of scope. ### 4.3 Fixed product/safety invariants -1. The helper is optional; absence preserves compatible existing behavior and prerequisite diagnostics. -2. An enabled but missing or invalid helper fails closed when verification is requested. -3. Only the trusted snapshot executes; candidate/model edits cannot replace it. -4. Setup failure, timeout, invalid output, or failed verification prevents commit/push. Sync uses existing abort/recovery behavior. -5. No setup runs for read-only queries, dry runs, aligned/no-op branches, or empty verification command lists. -6. No PAT, model/cloud credential, persisted Git write credential, agent auth-store variable/path, or agent credential profile is available to the helper. +- Setup MUST NOT grant modification authority or run for forks rejected by existing admission. +- Read-only, dry-run, aligned/no-op and empty-verifier operations MUST NOT execute setup. +- Candidate edits MUST NOT replace the captured action or its auxiliary files. +- Failure, timeout, malformed handoff, changed candidate or invalid runtime paths MUST withhold the command and publication. +- Each verification command MUST use a fresh private HOME; only validated runtime directories and JAVA_HOME return to its environment. ## 5. Current versus proposed product journey -| Stage | Current | Proposed | User/operator effect | -|---|---|---|---| -| Configure | No stable managed hook contract | Add or update helper on trusted branch | Consumer owns setup implementation; Copilot owns generated workflows | -| Admit operation | Existing route authorization | Capture helper bytes/SHA before workspace mutation | Provenance is inspectable and candidate cannot substitute code | -| Prepare candidate | Project tools may be absent | Invoke snapshot after merge/fix changes, per command | Dependencies match current manifests | -| Verify | Existing preflight and command policy | Same policy, after setup and preflight | Existing checks remain authoritative | -| Failure | Existing verification failure behavior | Sanitized setup stage/cause; no commit/push | Clear recovery without exposing helper output | +| Stage | Existing | Proposed | +|---|---|---| +| Adoption | Edit generated workflow or provision runner | Review consumer composite onto trusted main, update generated workflows, enable opt-in | +| Candidate preparation | Agent/temporary merge | Unchanged | +| Dependencies | Preinstalled in same job | Native consumer composite, in that same job and candidate workspace | +| Verification/publication | Credential-free checks and existing guards | Same checks and guards after successful setup | ```mermaid -flowchart LR - A[Authorized route] --> B[Capture trusted helper SHA] - B --> C[Merge or agent candidate changes] - C --> D[Clean isolated environment] - D --> E[Run helper for this candidate and command] - E --> F[Runtime preflight and verification] - F --> G[Existing commit/push gate] - E -->|failure or timeout| H[Block commit/push; existing recovery] +sequenceDiagram + participant C as Admitted Copilot operation + participant R as GitHub Actions runner + participant Y as Trusted consumer composite + C->>C: Prepare candidate and close agent profiles + C->>R: Pause at verification boundary + R->>Y: Execute pinned YAML and assets + Y-->>R: Native success or failure, runtime paths + R->>C: Continue the existing operation once + C->>C: Restore candidate action, validate candidate and runtime paths + C->>C: Preflight, verify, apply publication guards ``` -Text equivalent: authorized route captures trusted helper, prepares the changed candidate in isolation, verifies it, then applies the existing commit/push gate; setup failure blocks that gate. +Text equivalent: Copilot prepares the candidate, pauses, the runner executes trusted setup, and the same operation resumes only after restoring and validating the candidate. No agent is rerun. ## 6. Functional behavior and state model ### 6.1 Happy path -1. Admission preserves current actor-modification authorization, same-repository/native-head/workspace checks and fork rejection. -2. Before any agent workspace mutation, composition captures the regular-file helper from the configured trusted main/production branch SHA and retains its bytes/SHA privately. -3. Only when a route reaches actual verification does Copilot execute the snapshot from the candidate cwd after merge/fix changes. -4. Each configured command gets a fresh helper invocation, then runtime preflight and that command in the same isolated environment. Setup readiness is not reused. -5. On success, existing verification and commit/push rules continue unchanged. +1. Composition MUST capture immutable trusted Git objects before candidate mutation. The action directory is limited to 32 regular files, 64 KiB per file and 256 KiB total. Its root metadata MUST be `action.yml`, valid composite YAML with 1–32 steps. Asset paths MUST use portable ASCII letters/digits/underscore/dot/dash/slash, at most 200 characters, without empty/parent segments or Windows reserved names. Symlinks, submodules, unsafe paths and nested local `uses` actions are rejected. Native external actions and reviewed auxiliary scripts are supported. +2. At the existing verifier boundary, a private same-job worker MUST retain the admitted call stack. Generated workflows do not rerun the full operation to continue it. +3. The worker MUST bind HEAD, symbolic branch, index, tracked changes and publishable untracked contents. Git observations are bounded to 16 MiB, 256 untracked files and 16 MiB aggregate untracked contents; oversized candidates fail closed. Ignored dependency caches are excluded. +4. The candidate's action directory MUST be backed up privately and temporarily overlaid with the trusted snapshot. The native step runs from the candidate workspace with the verifier's fresh HOME. After setup, the candidate directory MUST be restored byte for byte before verification/publication. +5. The continuation MUST match the session, current request, run, attempt, job and workspace. Duplicate/stale responses fail closed. The candidate identity MUST still match. +6. Only new existing PATH directories within the workspace, private HOME or trusted tool cache, and an approved JAVA_HOME, MAY reach the verifier. The existing closed 16-KiB result validation and eight-directory ceiling remain in force. +7. Runtime preflight and the configured command MUST execute in that same fresh environment. Subsequent commands repeat setup with a new HOME and the current manifests. ### 6.2 Alternative paths -- No helper present and hook disabled: retain existing prerequisites and compatible behavior. -- Enabled helper absent, malformed, unsafe, or wrong type: fail closed when verification is requested. -- Snapshot acquisition failure on a route that never verifies: defer the error; read-only acquisition does not cause setup execution. -- Setup command failure or timeout: stop before verifier and commit/push; synchronization aborts through existing recovery. -- Empty command list, no-op/aligned branch, dry run or read-only request: do not invoke setup. +Disabled setup preserves existing behavior. Enabled setup without native workflow support, including local CLI verification, MUST fail closed with an explanation to update workflows and run in GitHub Actions. Snapshot errors are deferred on routes that do not verify. No automatic setup retry or shared readiness cache is permitted. ### 6.3 State machine -| State | Entered when | User-visible meaning | Allowed next states | Recovery/owner | -|---|---|---|---|---| -| Not requested | Route does not verify | No project preparation needed | Complete | Existing route owner | -| Snapshot captured | Trusted regular file pinned | Helper provenance fixed | Preparing, failed on later validation | Composition boundary | -| Preparing | Verification command is about to run | Project tools are being prepared | Ready, blocked | Setup runner | -| Ready | Setup and output validation succeed | This candidate/command may be checked | Verifying | Verification orchestration | -| Blocked | Setup/preflight/verification fails | No commit/push will occur | Recovered by existing operation flow | Operator/operation owner | -| Complete | Existing operation finishes | Existing result is final | Terminal | Existing route owner | - -Duplicate events follow existing route idempotency. A new candidate or command is a new preparation; stale readiness cannot be reused. Cancellation terminates setup and prevents downstream verification/commit. Partial synchronization remains governed by existing merge recovery. +| State | Meaning | Next/recovery | +|---|---|---| +| Disabled/skipped | No preparation requested | Existing operation | +| Prepared candidate | Authorized workspace ready | Pending native setup | +| Pending setup | Native runner owns current setup request | Ready, failed, canceled, timed out | +| Ready | Native success and candidate/runtime validation passed | Preflight and command | +| Blocked | No safe successful continuation | Existing abort/result publication; operator repairs and reruns | +| Complete | Existing operation and its result publication completed | Private session cleanup | ## 7. User-facing configuration -| Input | Type | Recommended default | Allowed values/range | Scope/persistence | -|---|---|---|---|---| -| `verification-setup` / `VERIFICATION_SETUP` / `repository.verificationSetup` | Boolean | `false` | Only true/false; no arbitrary helper paths or commands | Action input / Repository Variable / setup config | -| `.copilot/verification-setup.mjs` | Optional repository file | Absent/disabled | Standalone regular Node ESM file of at most 64 KiB in the trusted branch snapshot; Node argv contains only its private snapshot path | Repository-owned; snapshot SHA/bytes per admitted operation | -| Helper timeout | Fixed duration | 300 seconds | Not configurable | Per invocation | -| Captured output ceiling | Fixed limit | 64 KiB | Not configurable | Per invocation | -| Result file ceiling | Fixed limit | 16 KiB | Not configurable | Per invocation | - -The helper is standalone Node ESM, invoked with the trusted Action Node executable, `shell: false` and no user arguments. Its cwd is the exact candidate workspace. The private invocation environment adds only `COPILOT_VERIFICATION_SETUP_PROTOCOL=1`, `COPILOT_VERIFICATION_SETUP_SOURCE_SHA` and `COPILOT_VERIFICATION_SETUP_RESULT` (a private output file path). These metadata variables are not forwarded to verification commands. Relative module imports resolve in the private snapshot directory; use Node built-ins and explicit candidate-cwd subprocesses instead of sibling imports. - -The result file is optional. When present it is one regular, nonlinked UTF-8 JSON file, at most 16 KiB, with numeric `schemaVersion: 1`, optional `path` (at most eight unique absolute directories of at most 2,048 characters each) and optional `environment` containing only `JAVA_HOME`. Null values, unknown keys, non-directories, traversal, control characters, path delimiter injection and escaping symlinks are rejected. Approved roots are the canonical candidate workspace, private HOME and captured trusted `RUNNER_TOOL_CACHE`. PATH additions precede the existing PATH; JAVA_HOME is the only environment override. No result means no overlay. There is no stdout policy channel. - -Trusted acquisition uses local Git reads of `refs/remotes/origin/` at admission, regular Git blob mode and verified object digest/UTF-8 bytes. A full checkout must have the configured main ref available; no credentialed fetch occurs during acquisition. Acquisition failures are retained as closed facts and raised only if verification runs. Result reads are bounded and compare exact BigInt filesystem identities. Each setup invocation has a fresh profile; success logs only the fixed path and pinned SHA. Existing localized operation shells own presentation; the technical diagnostic is the same English static fallback used by runtime prerequisites. - -The helper MAY install tools and dependencies needed by that candidate, using declared runtime versions and lockfiles. It MUST NOT require shell interpolation. Node execution uses argument arrays. Provision pnpm explicitly; implicit Corepack downloads are not a supported prerequisite. Helper-owned workspace caches must validate lockfiles. Private-profile tools may be recreated for each command. +| Surface | Name/type | Default/range | Scope | +|---|---|---|---| +| Action | `verification-setup`, boolean | `false`; only true/false | Invocation | +| Repository Variable | `VERIFICATION_SETUP` | `false`; true/false | Generated workflow | +| Setup config | `repository.verificationSetup`, optional boolean | absent means false | Installation plan | +| Consumer file | Fixed composite directory above | No path override | Repository-owned, never installer-owned | -Only approved output paths are accepted: existing directories contained in the workspace, private profile, or trusted tool root. Reject unknown environment fields, unsafe links, and paths. Invalid cross-field/output combinations fail closed. Helper stdout/stderr is never emitted raw; diagnostics expose bounded stage, cause, pinned path/SHA and exit/status facts only. +The setup configuration plans the Variable; generated workflows forward its value. An explicit direct Action input applies to that invocation. Invalid values MUST fail validation. Runtimes are not inferred. Recommended adoption: review the action onto trusted main, update generated workflows with `copilot setup`, then enable the Variable. Alternative: remain disabled and provision dependencies before Copilot in the same job. -Recommended setup: use the repository’s declared Python version and lockfile to install test dependencies, and the declared Node/pnpm versions and lockfile for frontend gates. A meaningful alternative is a JVM toolchain and its project lock/dependency declaration. No arbitrary ref, URL, secret, timeout, or environment override is configurable. Updating Copilot preserves consumer helper contents; first adoption requires review onto the trusted source branch before activation. +The 20-command limit, five-minute native setup timeout, snapshot/result/path bounds, source trust and publication checks are intentionally non-configurable. The former Node helper protocol and 64-KiB suppressed-output ceiling are retired; native step logs use GitHub Actions semantics. -## 8. Clean Architecture +## 8. Clean Architecture design ### 8.1 Responsibilities and dependency direction -| Layer/boundary | Owns | Must not own/import | +| Boundary | Responsibility | Forbidden responsibility | |---|---|---| -| Domain/pure policy | Whether verification is requested; fixed limits and eligibility rules | GitHub SDK, process APIs | -| Application | Immutable snapshot contract and existing verification intent/order via the Git port | Concrete process/provider DTOs | -| Adapters/data | Trusted branch file retrieval and immutable bytes/SHA mapping | Authorization or product policy | -| Infrastructure/composition | Isolated environment creation, process execution, cleanup and route wiring | Decisions about whether a route is authorized | -| Entrypoints | Convert issue/PR/comment/review/commit/sync events to use-case input | Duplicated verification orchestration | -| Presentation | Sanitized status and diagnostic model | Mutations, secret handling, retry policy | - -```mermaid -flowchart LR - E[Workflow and CLI entrypoints] --> A[Application verification orchestration] - A --> D[Pure eligibility and limits policy] - A --> P[Snapshot and isolated process ports] - I[Git and process adapters] --> P - A --> V[Sanitized result model] - V --> R[Job summary and existing status renderer] -``` +| Pure policy/application ports | Closed limits/errors, immutable prepared snapshot | Runner APIs or process implementation | +| Existing application use cases | Admission, candidate preparation, verification and publication decisions | Native YAML interpretation or duplicate phase orchestration | +| Git adapter/setup runner | Pause precisely before probes/command, validate resume | New mutation authorization | +| Infrastructure snapshot/overlay/protocol/controller | Trusted objects, temporary native action, same-job process lifecycle | Provider business policy or model execution replay | +| Entrypoint/generated workflows | Dispatch native phases and native `uses` | Reimplement verifier/publication rules | +| Presentation | Existing localized result/error surfaces | Executable helper output as policy | ### 8.2 Contracts, state, and trust boundaries -- Pure decisions: operation requires verification, helper enabled/valid, and fixed limits; no-op/read-only exclusions. -- Application contracts: immutable trusted snapshot metadata, candidate identity, command identity, bounded setup result, and verification outcome. -- Semantic ports: read trusted regular file at pinned ref; execute bounded helper in isolated environment; clean environment; report sanitized outcome. -- Durable state/schema ownership: no new durable user state required; snapshot bytes/SHA remain private to the operation and are not sourced again from candidate files. -- Concurrency/idempotency strategy: per-operation snapshot, per-candidate/per-command invocation; no shared readiness cache and no retry. -- Trusted and untrusted inputs: trusted source branch snapshot is executable; event payloads, candidate tree, helper output and model edits are untrusted. Existing authorization and fork checks remain mandatory. -- Provider error mapping: distinguish acquisition, validation, setup exit, timeout, result validation, preflight and verification; expose bounded sanitized cause only. +The session contains bounded non-secret metadata and private operational logs. Worker stdout/stderr redact configured secrets and dynamic masking commands before bytes reach disk; PAT/model credentials are never serialized as log content. Credentials remain in the admitted worker's memory/environment as in the existing Action; child verifier environments remain allowlisted. Session provenance, request UUIDs and one-use responses prevent accidental replay across jobs/candidates. Root Action post-job cleanup owns cancellation and private directory removal. Dependency-only profiles remain private until nested native post steps finish, then the original Action post step removes them; no later verifier reuses them. No artifacts or remote checkpoints are created. + +The workflow's fixed native phases support at most 20 setup boundaries per operation. Worker log forwarding preserves complete runner-command lines, with a 1-MiB single-line memory bound; this is distinct from native setup logs. Worker outputs are bounded to 256 KiB. ### 8.3 Executable architecture constraints -Add contract checks proving source and distributed workflow templates route all verification paths through the shared preparation boundary, setup variable planning is consistent, and entrypoints do not duplicate process orchestration. Tests must structurally parse workflows/contracts rather than grep prose. Enforce application-to-semantic-port dependency direction and prevent domain imports of provider/process packages using the repository’s architecture checks. +Existing architecture/dependency tests remain mandatory. The workflow validator MUST compare all native setup/continue/finish steps with the canonical generator, including conditions, timeout, runtime-only env and consistent Action reference/inputs. Source and distributed callbacks MUST agree. All six existing configuration callbacks retain explicit opt-in forwarding. ## 9. UI/UX and content contract ### 9.1 Information hierarchy -Show current verification status, completed setup/verification facts, next transition, whether action is required, commit/push impact, and links before technical details. Setup output and secrets never appear in user-facing content. - -### 9.2 Representative primary view - -Blocked before irreversible effects: - -```markdown -# Verification could not continue - -> **Current status:** Repository setup failed before verification. -> -> **Action required:** Review `.copilot/verification-setup.mjs` on the trusted branch and rerun the operation. - -## Progress - -- [x] Candidate workspace prepared -- [x] Project verification not run -- [x] Commit and push withheld +Existing issue/PR result cards remain the primary operation status. Named Actions steps expose preparation and continuation; no new comment stream or labels are introduced. Users see impact, cause, action and retained state before technical details. -## What happens next +### 9.2 Representative primary views -Correct the setup helper or its declared dependencies, then retry the authorized operation. - -
-Technical details - -Setup stage: dependency preparation. Trusted helper SHA: ``. Exit status: ``. - -
-``` +| State | Representative content | +|---|---| +| Pending | Actions: “Prepare repository verification 1”. Operation remains in progress; no user action. | +| Action required | “Repository setup blocked verification (native-workflow-required). Update the generated Copilot workflows and run this operation in GitHub Actions; the local CLI cannot execute composite actions.” | +| Failed | “Repository setup blocked verification (process-failed). Review the trusted action and rerun. Verification, commit and push are withheld.” | +| Partially complete | Candidate prepared; setup failed; no remote publication. Existing sync abort restores the local merge. | +| Complete | “Repository setup completed; runtime preflight and verification may proceed.” Existing operation then reports its actual publication result. | ### 9.3 Issue, PR, and comment behavior -Use existing operation titles, durable markers, update behavior and notification budget. Setup is a step within verification, not a new issue/comment lifecycle. Existing issue/PR/comment/review/commit and sync surfaces link to their run. No new labels or checks are required; existing checks remain supplemental state. A blocked result must never imply a commit/push occurred. +No additional comments: the existing bounded result publication owns success/failure and links to the run. Native raw setup logs MUST NOT be copied into issue/PR content. Diagnostics identify the fixed path, closed cause and pinned source SHA. -### 9.4 Accessibility, localization, and responsive behavior +### 9.4 Accessibility and localization -Use the existing supported locale source and fallback; keep helper path and stage names stable while surrounding prose follows existing localization. The textual flow above is the diagram equivalent. Use concise headings, descriptive links, narrow-screen readable tables, theme-independent status text, and sanitize untrusted helper content, Markdown, mentions, commands, markers and URLs. Do not render raw helper output. +Existing localized result cards and plain-text error fallback remain in force. Native diagnostic step names are English operational labels. Text accompanies every diagram; no color-only states or new responsive layout is introduced. Helper stdout is never interpreted as commands to Copilot or Markdown publication content. ## 10. Failure, recovery, and cleanup -| Failure/partial state | User impact | Retained facts | Automatic retry | Required action | Cleanup | -|---|---|---|---|---|---| -| Enabled helper missing/invalid | Verification blocked | Trusted SHA/path and validation stage | No | Fix helper on trusted branch; rerun | Remove private snapshot after operation | -| Snapshot acquisition failure | If verification requested, operation blocks | Acquisition stage and route identity | No | Inspect trusted branch access/configuration | Discard partial snapshot | -| Helper exits nonzero | No verifier or commit/push | Exit status and pinned SHA | No | Fix helper/dependency declaration | Clean private profile and temporary outputs | -| Timeout at 300 seconds | No verifier or commit/push | Timeout stage and pinned SHA | No | Reduce setup work or correct lock/install behavior | Terminate process; clean outputs | -| Invalid/oversized output or unsafe path | No verifier or commit/push | Validation cause, bounded status | No | Correct helper contract | Remove rejected result and private state | -| Verification fails after setup | Existing verification failure | Setup succeeded for exact candidate/command | No new retry | Fix candidate/project checks | Existing operation cleanup; sync recovery | -| Temporary merge cannot verify | Sync does not push | Merge/recovery facts | Existing behavior only | Follow sync recovery guidance | Existing merge abort/cleanup | +| Failure | Impact and action | Cleanup/retry | +|---|---|---| +| Missing/invalid trusted action | No verifier/push; review trusted directory | No automatic retry | +| Native failure or timeout | No verifier/push; inspect native step logs and repair | Worker abort/finally restores action and existing sync recovery | +| Stale/cross-job/replayed response | No continuation; rerun fresh operation | Private session canceled | +| Candidate changed during setup | No verifier/push; make setup dependency-only | Original action restored; existing recovery | +| Invalid runtime output | No verifier/push; install under approved roots | Fresh HOME disposed | +| Job canceled/worker exits | No new publication; inspect retained operation result | Post action signals cancellation, waits up to 10 seconds, terminates remaining worker and restores overlay | -Errors follow impact -> cause -> action -> retained state. No automatic retry is added. Setup process cleanup and credential-profile cleanup occur on success, failure, timeout and cancellation. +Native setup steps have `timeout-minutes: 5`; the worker also rejects readiness after 300 seconds. The enclosing existing job timeout remains authoritative. A force-killed worker cannot promise application finally blocks ran: the Action restores its own overlay, and normal runner checkout cleanup/operator recovery owns remaining job-local candidate state. Already-completed remote effects MUST retain their existing truthful result semantics. ## 11. Security, permissions, and privacy -1. Preserve actor modification authorization, same-repository/native-head/workspace checks and fork rejection on every admitted route. -2. Capture immutable regular-file bytes and SHA from the configured trusted main/production branch before workspace mutation. Candidate/model changes cannot replace it. New helper adoption requires review on that trusted source branch. -3. Before setup, clean agent runtime credential profiles and use a fresh private HOME plus the existing allowlisted verification environment. Exclude PAT, model/cloud credentials, persisted Git write credentials, agent auth-store variables and paths. -4. Reject unknown environment fields, unsafe links and output paths outside existing approved workspace/private-profile/trusted-tool-root directories. Bound time and output as specified. Do not claim a universal OS sandbox against malicious maintainer code; retain the trusted-runner boundary. -5. Never log raw helper output, secrets, or credential-bearing environment. Diagnostics identify stage/cause, fixed path, pinned SHA and bounded exit/status only. +Native setup receives only runtime metadata from the private verifier environment, never Copilot's PAT/model inputs or evidence token. Generated workflows MUST keep provider secrets scoped to the initial Copilot Action step, not the job or consumer action. Checkout credentials remain unpersisted. Native actions can use runner facilities required for normal Actions execution; they are reviewed executable maintainer code. + +This preserves the trusted-runner boundary and makes no OS-sandbox claim: same-user processes and service-level credentials remain an operator trust concern. PATH/JAVA_HOME are validated after setup; arbitrary `GITHUB_ENV` keys do not enter the verifier. Native external action versions and dependency caches are maintained by the repository owner. Nested local `uses` is excluded because its deferred post scripts could otherwise refer to restored candidate assets. ## 12. Observability and operational UX -- User-facing state: whether setup was skipped, prepared, or blocked; whether verification and commit/push ran. -- Job Summary/operator evidence: operation correlation, route, candidate identity, helper pinned SHA, stage and bounded outcome; no raw output. -- Logs, metrics, and correlation: structured stage timing and result, correlated with existing run/operation identifiers; do not record environment values. -- Pending external dependency versus workflow failure: setup timeout/exit is a workflow verification failure; trusted source acquisition is reported separately. -- Rate-limit/retry behavior: no setup retry; existing provider retry policies remain unchanged. -- Noise/comment budget: update existing operation status; do not add per-command comments. +Logs name the fixed action and pinned source SHA, preparation success, and the existing verification result. Native Actions logs expose setup commands normally; maintainers MUST avoid printing secrets. No new secret-bearing artifacts, telemetry fields or result comments are added. The operation worker drains ordinary Action logs in order so masking commands retain their framing. ## 13. Compatibility, migration, rollout, and rollback -- Existing in-flight operations retain their captured policy and do not acquire a helper mid-operation. -- No schema migration is required. Existing repositories without a helper retain compatible behavior and prerequisite checks. -- The helper requires the explicit boolean opt-in and its fixed path; invalid enabled configuration fails closed. Generated workflow updates must preserve the consumer-owned helper. -- Roll out through setup CLI/template wiring and documentation, then verify controlled local same-managed-branch synchronization. Do not launch a release as part of this work. -- Rollback removes hook invocation while preserving existing verification policy. Never undo a completed external commit/push as part of rollback; this design blocks those effects on failure. +Existing installations remain disabled by default. The unreleased `.copilot/verification-setup.mjs` prototype is removed rather than maintained as a second execution mode. Consumers opting in MUST update generated callbacks before activation and review the composite directory onto trusted main. Setup/install/update MUST preserve that directory and all its assets. Rollback disables the Variable and restores ordinary same-job provisioning; no remote migration, release or tag is required. ## 14. Testing strategy and numeric budget -Minimum distinct cases, risk-derived from trust, freshness, route coverage, and irreversible commit/push gating: +Minimum **150 distinct automated cases**, allocated without double counting: -| Area | Minimum distinct cases | Behaviors/risks covered | +| Area | Minimum | Risk | |---|---:|---| -| Domain/configuration/pure planning | 6 | Enabled/absent/invalid helper; verify eligibility; no-op exclusions; fixed limits | -| State/application/idempotency/races | 6 | Snapshot timing; candidate changes; per-command refresh; cancellation; duplicate route; no readiness reuse | -| Adapters/provider contracts | 4 | Regular-file snapshot/SHA; acquisition failure; process arguments; bounded result mapping | -| Workflows/setup | 8 | Issue, PR, general/review comments, commit, autofix and sync boundaries; source/template parity; update idempotency | -| UI/UX/localization/sanitization | 4 | Blocked and success states; links/action; localization fallback; output sanitization | -| Integration/security/migration | 8 | Python pytest prepared/unprepared; Node/pnpm; changed manifests; credential isolation; unsafe path; timeout/failure; no-helper compatibility; controlled sync | -| **Total** | **36** | Distinct cases; no double counting | +| Policy/configuration | 30 | Defaults, invalid values, closed runtime schema | +| Trusted snapshot/assets | 22 | Pinning, hashes, file modes, malformed/native metadata | +| Native protocol/controller | 36 | Lifecycle, output framing, replay, timeout, cancellation | +| Candidate/runner orchestration | 24 | Ordering, fresh profiles, candidate identity, failure gates | +| Workflows/setup contracts | 18 | Native step structure, env, limits, source/distribution, preservation | +| Real Git/composition integration | 8 | Exact managed ref, merge manifests, abort/push guards | +| Runtime result filesystem security | 12 | Bounds, links, containment, races, atomic overlays | +| **Total** | **150** | Native rewrite adds at least 70 cases beyond retained applicable baseline | -Keep repository-wide coverage thresholds in force and meet them in changed modules. New pure eligibility/limits policy requires at least 90% branch coverage. Use table-driven distinct behavior rows, deterministic clocks/IDs/process fakes, no real waits or live services, structural workflow contract checks, and semantic UI assertions alongside golden fixtures. Manual UX evidence must show readable blocked/success results on a narrow view and inspectable run navigation. Controlled acceptance must demonstrate the original branch-sync verification with the same managed branch and no manual generated-workflow edits; no release. +Repository-wide thresholds remain in force. New pure policy targets 100% branches; changed setup adapters target aggregate 95% lines/statements and 90% branches/functions. Deterministic unit fakes cover runner responses without interpreting YAML. Separate native CI acceptance MUST execute real composite `uses`/`run` steps with Python/pytest and Node/pnpm on hosted Linux, Windows and macOS, asserting candidate changes, trusted-source selection, fresh profiles and restoration. Controlled local Git acceptance covers the exact same managed branch and no release. Native CI evidence is additional to the distinct Jest budget. ## 15. Documentation and discoverability -| Audience | Artifact/page | Required content | Validation/navigation | -|---|---|---|---| -| User | `docs/security-operations/operations/verification.mdx` | Normal journey, optional helper, Python/Node examples, defaults and no-helper behavior | Link and example contract check | -| Setup owner | Verification guide section | Fixed path/interface, trusted branch, runtimes/lockfiles, pnpm provisioning, approved outputs | Fixture-backed contract test | -| Operator | `docs/security-operations/operations/troubleshooting.mdx` | Failure stages, sanitized diagnostics, recovery, timeout and cleanup | Navigation/link validation | -| Contributor | `docs/development/architecture.mdx` and companion SDD | Ports, snapshot trust, route boundary and workflow checks | Architecture/workflow contract checks | - -Document credential boundaries, update preservation, migration/rollback, and the fact that a separate CI job does not provision the verifier. Keep examples synchronized with helper contract fixtures. - -## 16. Acceptance - -1. Given an authorized verification operation and a valid trusted helper, when a candidate is ready, then the pinned helper runs in the isolated verifier environment before preflight and the configured command. -2. Given a temporary sync merge or changed manifests, when each verification command starts, then setup refreshes against that exact candidate and does not reuse prior readiness. -3. Given no helper and the hook is disabled, when verification runs, then existing prerequisite behavior remains compatible; given an enabled missing/invalid helper, verification fails closed. -4. Given setup exits unsuccessfully, exceeds 300 seconds, or emits invalid/oversized output, when the operation reaches verification, then verification and commit/push are withheld and the diagnostic is sanitized and actionable. -5. Given setup or verification failure during synchronization, when recovery runs, then the temporary merge follows existing abort/recovery behavior and no push occurs. -6. Given a fork or untrusted event, when admission is evaluated, then helper execution is rejected and no credentials or agent auth-store state is exposed. -7. Given a read-only query, dry run, aligned/no-op branch, or empty command list, when the route completes, then setup is not invoked. -8. Given a consumer updates Copilot templates, when setup CLI/templates are refreshed, then the consumer helper contents remain intact and all verification routes reach the shared boundary. -9. Given the `homeassistant-gateway` Python command, when the trusted helper installs declared locked test dependencies, then pytest preflight and configured verification can run; without preparation the missing dependency blocks commit/push. -10. Given controlled same-managed-branch synchronization, when the candidate is merged and verified, then the run succeeds without manual edits to generated Copilot workflows. No release is part of acceptance. +| Audience | Deliverable | Validation | +|---|---|---| +| User/operator | Verification page: normal flow, Python/Node YAML, native logs, limits, failures and CLI constraint | Documentation links and workflow fixtures | +| Setup owner | Setup configuration reference and same-job adoption order | Setup parser/planning and update-preservation tests | +| Contributor | Generated agent guide, source contract, SDD/catalog evidence | Guidance/specification validation | + +## 16. Acceptance scenarios + +1. Disabled repositories run existing verification unchanged. +2. Enabled authorized sync prepares dependencies from the temporary merged manifests, verifies and pushes only its existing managed branch. +3. A candidate/model change to the action does not execute; trusted YAML/assets run, then original candidate bytes return. +4. Setup success precedes runtime probes; every command gets a fresh HOME. Read-only, dry-run, no-op and empty-command paths execute no setup. +5. Failed/canceled/timed-out setup, changed candidate, malformed output or replayed/cross-job continuation blocks verification and publication and restores the overlay. +6. Native Python/pytest and Node/pnpm acceptance passes on all three hosted platforms. No PAT/model credentials are forwarded to the consumer action or verifier. +7. Updating Copilot twice preserves the consumer directory. Old/missing native callback support and CLI opt-in fail with actionable help. +8. Existing post-verification remote-head/workspace guards and truthful partial-success reporting remain authoritative. ## 17. Requirements traceability -| Requirement | Policy/use case/adapter/presentation | Test or evidence | Documentation | -|---|---|---|---| -| Optional hook and no-op exclusions | Eligibility policy and shared verification orchestration | Pure planning and route tests | Verification guide | -| Trusted immutable snapshot | Composition and snapshot port | SHA/source mutation and fork tests | Setup/security guidance | -| Per-candidate preparation | Verification orchestration | Changed-manifest and sync merge cases | Helper contract | -| Credential/output isolation | Isolated process adapter | Credential, path and sanitization tests | Security and troubleshooting | -| Fixed failure limits and commit gate | Application result policy | Timeout/failure/no-push cases | Troubleshooting | -| All routes and template preservation | Workflow/setup variable planning | Structural route parity/idempotency checks | Contributor guide | -| Understandable status | Sanitized presentation model | Semantic UI/localization fixtures and manual UX evidence | Operator guide | +| Requirements | Implementation/evidence | Documentation | +|---|---|---| +| §§4–7 source/defaults/routes | Policy, snapshot, config/composition and sync tests | Verification/setup reference | +| §6 native handoff/candidate | Runner, protocol, overlay, controller tests | Architecture/recovery | +| §§8–11 workflows/security/cleanup | Structural mutation tests, result filesystem tests, platform fixture | Native steps and trust boundary | +| §§12–16 compatibility/acceptance | Setup preservation, real Git integration, hosted native acceptance | Migration/operator guide | ## 18. Implementation sequence -1. Define helper interface, fixed path, snapshot/result contracts and architecture checks. -2. Implement pure eligibility, limit and output-validation policies with focused tests. -3. Add composition snapshot lifecycle and the existing semantic verification port. -4. Add trusted-file and isolated-process adapters with credential cleanup and bounded execution. -5. Wire sync and shared commit-verification boundaries across source and distributed templates and setup CLI planning. -6. Add sanitized status, fixtures, documentation, troubleshooting, and update-preservation behavior. -7. Complete route/security/integration coverage and controlled local synchronization acceptance; validate specs/catalog artifacts. No release. +1. Resolve native contract and update specification/issue/PR. +2. Capture composite assets, implement bounded same-job native handoff and cleanup. +3. Generate native callbacks and enforce their structure. +4. Replace prototype tests; add native platform acceptance and real Git integration. +5. Update docs/guidance/catalog/bundles and validate all repository gates. +6. Resolve findings, wait for every required check on the final head, then merge to develop as explicitly authorized. ## 19. Definition of Done -- [x] Every normative requirement maps to acceptance and traceability. -- [x] Helper path, invocation, limits, trusted SHA and output contract are implemented and validated. -- [x] All actual verification routes use the shared hook; read-only/no-op/dry-run paths do not. -- [x] Snapshot, candidate freshness, credentials, unsafe paths, timeout, output bounds and fail-closed commit gate pass. -- [x] Numeric test budget of at least 36 distinct cases and coverage requirements pass. -- [x] UI states, sanitized diagnostics, navigation, accessibility and localization are reviewed. -- [x] User, setup-owner, operator and contributor documentation is discoverable and examples match fixtures. -- [x] Setup/update preserves consumer helper contents; generated workflow contract checks pass. -- [x] Controlled same-managed-branch sync acceptance passes without manual workflow edits; no release is launched. -- [x] `specs/catalog.json` evidence and `specs/CATALOG.md` are current and `pnpm run validate:specifications` passes. -- [x] No readiness-blocking decision remains unresolved. +- [x] Normative acceptance and traceability are covered. +- [ ] Native runner executes the composite at the actual candidate verification boundary. +- [x] Architecture, numeric budget, coverage and workflow mutation checks pass. +- [x] Defaults, limits, CLI behavior, failures, replay and cancellation match documentation. +- [x] User/setup/operator/contributor docs, native UI labels and safe diagnostics are reviewed. +- [x] Generated guidance/catalog/bundles and required validations pass. +- [ ] All three hosted native acceptance jobs pass; controlled Git sync preserves the same managed ref. +- [ ] Final-head checks pass and no unresolved review findings remain before merge. ## 20. References and decisions -- Primary sources: [Issue #647](https://github.com/vypdev/copilot/issues/647); [repository verification prerequisites](https://github.com/vypdev/copilot/blob/master/docs/security-operations/operations/verification.mdx#repository-verification-prerequisites); [consumer issue #19](https://github.com/vypdev/homeassistant-gateway/issues/19); [failed run](https://github.com/vypdev/homeassistant-gateway/actions/runs/39570398824). -- Related specs/ADRs/issues/PRs: owning specifications are `specs/branch-synchronization-and-conflict-recovery.md` and `specs/repository-verification-runtime-prerequisites.md`; this companion is `specs/repository-verification-setup-hooks.md` and does not replace either owner. -- Decisions and rejected alternatives: fixed optional repository helper and shared managed invocation boundary; rejected editing generated workflows per consumer, global dependency installation, separate preparation jobs, weakening verification, automatic retries, and contributor-created helper activation. -- Follow-up work explicitly outside this spec: implementation admission, Action-managed branch, PR review, release, and any release operation. -## Implementation evidence — issue #647 - -The two executable verification boundaries are `SyncBranchUseCase.verifyPreparedMerge` and the shared automated commit preflight. General issue/PR comments, natural-language modification, `/copilot implement`, Bugbot autofix, remember-branch changes and review-comment fixes reach those boundaries through the two comment composition roots. Issue lifecycle, PR review, commit analysis and review-state events do not currently run repository verification themselves. The lightweight push sync observer only reads status. All six source/distributed event workflows carry the disabled-default input, so forwarding does not silently activate setup. Existing actor, native-head, workspace and fork admission precedes any verification request; the snapshot itself is read-only and can be captured before admission without executing it. - -New files: pure `verification_setup_policy`, semantic `verification_setup_ports`, infrastructure snapshot/process/result/runner adapters. `GitCommitAdapter` runs setup, existing prerequisites and the command in one fresh environment whenever the existing `untrusted + verifyRuntimePrerequisites` intent is present. Trusted commits and Git reads never invoke setup. Agent runtime cleanup still completes before agent results return to verification. Setup never owns or rewrites the helper. - -Executable evidence is in `verification_setup_policy.test.ts`, `verification_setup_snapshot.test.ts`, `verification_setup_runner.test.ts`, `verification_setup_configuration.test.ts`, `verification_setup_sync.test.ts`, composition tests and the structural workflow validator. The real local Git fixture first fails unprepared, aborts without changing its remote managed ref, then prepares the merged lockfile twice and pushes the verified merge to exactly that same ref. Its candidate helper deliberately throws; only the captured main helper runs. Other rows cover dry run, aligned state, empty commands, setup failure, byte provenance, credential exclusion, limits, cancellation, paths, update preservation and legacy configuration. No public consumer run or release is claimed. - -### Controlled local acceptance (2026-10-21) - -Both cases ran through the real `SyncBranchUseCase`, Git adapter, workspace adapter and trusted snapshot against temporary local bare remotes and `feature/42-controlled-managed`. Neither case changed generated workflows or used an agent/network during setup. The first attempt, without setup, failed verification, aborted and retained the remote head. The second attempt prepared the temporary merge, passed the configured command and pushed only the same managed ref with a clean tracked workspace. - -| Fixture | Actual toolchain/command | Dependency preparation | Unprepared | Prepared | -|---|---|---|---|---| -| Python | Python 3.14.6; `python3 -m pytest -q -p no:cacheprovider` | Fresh private venv; offline wheelhouse, all five requirements version/hash locked, pytest 8.4.2 | Blocked, no push | `merged-cleanly`, one verification | -| Node/pnpm | Node 24.20.0; explicitly provisioned pnpm 10.12.4; `pnpm test` | Offline frozen install and candidate-manifest preparation | Blocked, no push | `merged-cleanly`, one verification | - -The Python wheelhouse was downloaded before the credential-free test from public packages: pytest 8.4.2, pluggy 1.7.0, iniconfig 2.3.1, packaging 26.3 and pygments 2.21.0. Every retained wheel was SHA-256 hashed into the fixture lockfile and installed with `--no-index --require-hashes`. No global dependencies, real consumer branches, public remote refs, tags or releases were modified. This is local controlled acceptance, not a claim that the consumer incident run has been retried. - -### Incremental executable inventory - -The implementation adds 102 distinct rows in five focused files (27 pure policy, 21 snapshot, 34 runner/result, 14 configuration/workflow, six real-Git sync), two composition admission-order rows and one structural workflow mutation row: **105 new automated cases**, exceeding the 36-case risk minimum above. Existing generated-link assertions are updated without counting them again. The two real Python/pnpm controlled acceptance runs are separate manual evidence, not additional automated budget claims. The new executable coverage budget retains 95% lines/statements and 90% branches/functions for the pure policy and aggregate adapters; the pure policy reached 100% of all metrics in focused coverage. +The maintainer selected the composite approach in the task on 2026-10-21. [Issue decision record](https://github.com/vypdev/copilot/issues/647#issuecomment-6318551564). Related contracts: `branch-synchronization`, repository verification prerequisites and the shared automated commit pipeline in the specification catalog. A reusable workflow and a Node ESM helper were rejected for this delivery; no second execution mode is retained. diff --git a/src/actions/github_action.ts b/src/actions/github_action.ts index e94c26acb..83967c200 100644 --- a/src/actions/github_action.ts +++ b/src/actions/github_action.ts @@ -1,3 +1,4 @@ +import { runNativeVerificationEntry } from '../infrastructure/native_verification_controller'; import { createNativeStatusReadPort } from '../infrastructure/composition/issue_completion_composition_root'; import * as core from '@actions/core'; import * as github from '@actions/github'; @@ -255,5 +256,5 @@ function earlyGithubActionErrorMessage(): ApplicationErrorMessageReader { // Only auto-run when executed as the action entry (not when imported by tests) /* istanbul ignore next -- the bundled production entry is covered by the Action smoke path. */ if (typeof process.env.JEST_WORKER_ID === 'undefined') { - void runGitHubActionEntry(); + void runGitHubActionEntry(() => runNativeVerificationEntry(runGitHubActionEntry)); } diff --git a/src/application/policies/__tests__/verification_setup_policy.test.ts b/src/application/policies/__tests__/verification_setup_policy.test.ts index 1e0c15df1..45f773334 100644 --- a/src/application/policies/__tests__/verification_setup_policy.test.ts +++ b/src/application/policies/__tests__/verification_setup_policy.test.ts @@ -2,8 +2,8 @@ import { parseVerificationSetupResult, VerificationSetupError, VERIFICATION_SETU describe('repository verification setup policy', () => { it('fixes the helper and limits without caller overrides', () => { - expect(VERIFICATION_SETUP_PATH).toBe('.copilot/verification-setup.mjs'); - expect(VERIFICATION_SETUP_LIMITS).toEqual({ timeoutMs: 300_000, outputBytes: 65_536, resultBytes: 16_384, scriptBytes: 65_536 }); + expect(VERIFICATION_SETUP_PATH).toBe('.github/actions/copilot-verification-setup/action.yml'); + expect(VERIFICATION_SETUP_LIMITS).toEqual({ timeoutMs: 300_000, resultBytes: 16_384, scriptBytes: 65_536, snapshotBytes: 262_144, files: 32, requests: 20 }); expect(Object.isFrozen(VERIFICATION_SETUP_LIMITS)).toBe(true); }); it.each([ diff --git a/src/application/policies/repository_agent_guidance_policy.ts b/src/application/policies/repository_agent_guidance_policy.ts index 961ca1ed1..9e3e3bdac 100644 --- a/src/application/policies/repository_agent_guidance_policy.ts +++ b/src/application/policies/repository_agent_guidance_policy.ts @@ -200,7 +200,7 @@ The setup runner profile selects placement; it does not install the project's la Declare reviewed version prefixes in \`.copilot/verification-prerequisites.json\` (schemaVersion 1, runtimes such as java 17, node 24, pnpm 10). Copilot checks declared versions in the credential-free verification environment before each configured verification command; a missing or incompatible runtime blocks publication. Without this optional declaration, existing verification behavior remains and no language is inferred. Java provisioning may use \`actions/setup-java\`; \`JAVA_HOME\` and \`PATH\` survive isolation, while agent credentials and tool-option injection variables do not. Do not add an unreviewed bootstrap or weaken verification to work around a missing runtime. Read [verification prerequisites and same-job setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-verification-prerequisites). -The optional repository-owned setup helper prepares each candidate before verification without editing generated workflows. Review \`.copilot/verification-setup.mjs\` onto trusted main before enabling \`VERIFICATION_SETUP=true\` (Action input \`verification-setup\`, disabled by default). Copilot executes captured trusted bytes before each command in a fresh credential-free profile; candidate changes cannot replace them. Setup failure blocks verification and commit/push. Read [repository-owned setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup). +The optional repository-owned composite action prepares each candidate before verification. Review \`.github/actions/copilot-verification-setup/action.yml\` and its assets onto trusted main, update generated workflows, then enable \`VERIFICATION_SETUP=true\` (Action input \`verification-setup\`, disabled by default). GitHub Actions executes the trusted snapshot before each command with a fresh private HOME; candidate changes cannot replace it. Native post steps finish before profile cleanup. Setup failure blocks verification and commit/push. The local CLI cannot execute composite actions and fails closed when opted in. Read [repository-owned setup](https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup). For the full contributor flow, read [repository collaboration](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-collaboration.mdx). For drift and recovery, read [guidance troubleshooting](https://github.com/vypdev/copilot/blob/master/docs/agents/repository-guidance-troubleshooting.mdx). `; diff --git a/src/application/policies/verification_setup_policy.ts b/src/application/policies/verification_setup_policy.ts index 7058ef4aa..07784a412 100644 --- a/src/application/policies/verification_setup_policy.ts +++ b/src/application/policies/verification_setup_policy.ts @@ -1,15 +1,19 @@ -export const VERIFICATION_SETUP_PATH = '.copilot/verification-setup.mjs'; -export const VERIFICATION_SETUP_LIMITS = Object.freeze({ timeoutMs: 300_000, outputBytes: 65_536, resultBytes: 16_384, scriptBytes: 65_536 }); +export const VERIFICATION_SETUP_DIRECTORY = '.github/actions/copilot-verification-setup'; +export const VERIFICATION_SETUP_PATH = `${VERIFICATION_SETUP_DIRECTORY}/action.yml`; +export const VERIFICATION_SETUP_LIMITS = Object.freeze({ timeoutMs: 300_000, resultBytes: 16_384, scriptBytes: 65_536, snapshotBytes: 262_144, files: 32, requests: 20 }); export const VERIFICATION_SETUP_HELP = 'https://docs.page/vypdev/copilot/security-operations/operations/verification#repository-owned-setup'; -export type VerificationSetupFailure = 'source-unavailable' | 'helper-invalid' | 'workspace-changed' | 'process-failed' | 'timeout' | 'cancelled' | 'output-limit' | 'result-invalid'; +export type VerificationSetupFailure = 'source-unavailable' | 'helper-invalid' | 'workspace-changed' | 'process-failed' | 'timeout' | 'cancelled' | 'output-limit' | 'result-invalid' | 'native-workflow-required' | 'candidate-changed' | 'handoff-invalid'; /** Closed facts only. Provider/helper output and raw exception causes never cross this boundary. */ export class VerificationSetupError extends Error { constructor(readonly reason: VerificationSetupFailure, sourceSha?: string, readonly exitCode?: number) { const sha = sourceSha && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(sourceSha) ? sourceSha : 'unavailable'; const exit = Number.isInteger(exitCode) && exitCode! >= 0 && exitCode! <= 255 ? ` Exit status: ${exitCode}.` : ''; - super(`Repository setup blocked verification (${reason}). Review ${VERIFICATION_SETUP_PATH} on the trusted main branch and rerun. Trusted source SHA: ${sha}.${exit} Verification, commit and push are withheld. Help: ${VERIFICATION_SETUP_HELP}`); + const action = reason === 'native-workflow-required' + ? 'Update the generated Copilot workflows and run this operation in GitHub Actions; the local CLI cannot execute composite actions.' + : `Review ${VERIFICATION_SETUP_PATH} on the trusted main branch and rerun.`; + super(`Repository setup blocked verification (${reason}). ${action} Trusted source SHA: ${sha}.${exit} Verification, commit and push are withheld. Help: ${VERIFICATION_SETUP_HELP}`); this.name = 'VerificationSetupError'; } } diff --git a/src/application/ports/verification_setup_ports.ts b/src/application/ports/verification_setup_ports.ts index d643f6453..6b1a292c8 100644 --- a/src/application/ports/verification_setup_ports.ts +++ b/src/application/ports/verification_setup_ports.ts @@ -1,9 +1,11 @@ import type { VerificationSetupFailure } from '../policies/verification_setup_policy'; +export interface VerificationSetupFile { readonly path: string; readonly base64: string; readonly executable: boolean } + /** An operation-private immutable snapshot, captured before candidate mutation. No credentials. */ export type PreparedVerificationSetup = | { readonly kind: 'disabled' } | { readonly kind: 'blocked'; readonly reason: VerificationSetupFailure; readonly sourceSha?: string } - | { readonly kind: 'ready'; readonly script: string; readonly sourceSha: string; readonly workspace: string; readonly node: string; readonly toolRoot?: string }; + | { readonly kind: 'ready'; readonly files: readonly VerificationSetupFile[]; readonly sourceSha: string; readonly workspace: string; readonly toolRoot?: string }; export const DISABLED_VERIFICATION_SETUP: PreparedVerificationSetup = Object.freeze({ kind: 'disabled' }); diff --git a/src/infrastructure/__tests__/native_verification_controller.test.ts b/src/infrastructure/__tests__/native_verification_controller.test.ts new file mode 100644 index 000000000..252d495d7 --- /dev/null +++ b/src/infrastructure/__tests__/native_verification_controller.test.ts @@ -0,0 +1,164 @@ +import fs from 'node:fs'; +import cp from 'node:child_process'; +import { EventEmitter } from 'node:events'; +import { join } from 'node:path'; +import * as core from '@actions/core'; +import * as protocol from '../native_verification_protocol'; +import { awaitNativeBoundary, cleanupNativeVerification, continueNativeVerification, drainNativeWorkerLog, runNativeVerificationEntry } from '../native_verification_controller'; +import { stageNativeVerificationAction } from '../native_verification_overlay'; +import { nativeVerificationFixture } from './native_verification_fixture'; + +jest.mock('@actions/core', () => ({ setOutput: jest.fn(), saveState: jest.fn(), getState: jest.fn(() => ''), getInput: jest.fn(() => 'true'), setFailed: jest.fn(), setSecret: jest.fn() })); + +describe('native Actions phase controller', () => { + let f: ReturnType; let before: NodeJS.ProcessEnv; + const roots: string[] = []; + const done = (exitCode = 0) => protocol.writeNativeRecord(f.session, 'done.json', { exitCode }); + const request = (edit: Record = {}) => protocol.writeNativeRecord(f.session, 'request.json', { + id: f.id, sourceSha: 'a'.repeat(40), environment: f.runtime.environment, ...edit, + }); + beforeEach(() => { before = { ...process.env }; f = nativeVerificationFixture(); jest.clearAllMocks(); jest.mocked(core.getState).mockReturnValue(''); jest.mocked(core.getInput).mockReturnValue('true'); }); + afterEach(async () => { + jest.restoreAllMocks(); + for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); + f.cleanup(); process.env = before; + }); + it('emits a pending request with only the closed runtime outputs', async () => { + request(); await awaitNativeBoundary(f.session); + expect(core.setOutput).toHaveBeenCalledWith('verification-setup-pending', 'true'); + expect(core.setOutput).toHaveBeenCalledWith('verification-request', f.id); + expect(core.setOutput).toHaveBeenCalledWith('verification-home', f.runtime.environment.HOME); + }); + it.each([{id: 'wrong'}, {sourceSha: 'wrong'}, {environment: {}}, {environment: {HOME: '\nsecret'}}])('rejects malformed pending request %j', async edit => { + request(edit); await expect(awaitNativeBoundary(f.session)).rejects.toThrow('handoff-invalid'); + }); + it('reports completion and copies native action outputs into the current step', async () => { + process.env.GITHUB_OUTPUT = join(f.session, 'parent-output'); fs.writeFileSync(join(f.session, 'worker-output'), 'telemetry=value\n'); done(); + await awaitNativeBoundary(f.session); + expect(fs.readFileSync(process.env.GITHUB_OUTPUT, 'utf8')).toBe('telemetry=value\n'); + expect(core.setOutput).toHaveBeenCalledWith('verification-setup-pending', 'false'); expect(core.setFailed).not.toHaveBeenCalled(); + }); + it('preserves worker failure instead of treating a resolved entrypoint as success', async () => { + done(1); await awaitNativeBoundary(f.session); expect(core.setFailed).toHaveBeenCalled(); + }); + it('publishes the existing Job Summary once at completion', async () => { + process.env.GITHUB_STEP_SUMMARY = join(f.session, 'parent-summary'); fs.writeFileSync(join(f.session, 'worker-summary'), 'Verified candidate\n'); done(); + await awaitNativeBoundary(f.session); await awaitNativeBoundary(f.session); + expect(fs.readFileSync(process.env.GITHUB_STEP_SUMMARY, 'utf8')).toBe('Verified candidate\n'); + }); + it('rejects oversized worker outputs without forwarding them', async () => { + process.env.GITHUB_OUTPUT = join(f.session, 'parent-output'); fs.writeFileSync(join(f.session, 'worker-output'), 'x'.repeat(262_145)); done(); + await expect(awaitNativeBoundary(f.session)).rejects.toThrow('output-limit'); + }); + it('bounds the operation wait without restarting the agent', async () => { + await expect(awaitNativeBoundary(f.session, 0)).rejects.toThrow('timeout'); + }); + it('detects a worker that exited before a result or handoff', async () => { + jest.spyOn(process, 'kill').mockImplementation(() => { throw new Error('not alive'); }); + await expect(awaitNativeBoundary(f.session)).rejects.toThrow('process-failed'); + }); + it('frames log lines once and flushes the remaining final line', () => { + fs.writeFileSync(join(f.session, 'worker-log'), '::add-mask::secret\nlast'); + const out = jest.spyOn(process.stdout, 'write').mockReturnValue(true); + drainNativeWorkerLog(f.session); drainNativeWorkerLog(f.session); drainNativeWorkerLog(f.session, true); + expect(out.mock.calls.map(call => Buffer.from(call[0] as Buffer).toString()).join('')).toBe('::add-mask::secret\nlast'); + }); + it('streams logs larger than a single read buffer', () => { + const data = 'line\n'.repeat(20_000); fs.writeFileSync(join(f.session, 'worker-log'), data); + const out = jest.spyOn(process.stdout, 'write').mockReturnValue(true); drainNativeWorkerLog(f.session, true); + expect(out.mock.calls.map(call => Buffer.from(call[0] as Buffer).toString()).join('')).toBe(data); + }); + it('bounds a worker log line rather than accumulating unbounded memory', () => { + fs.writeFileSync(join(f.session, 'worker-log'), 'x'.repeat(1_100_000)); + expect(() => drainNativeWorkerLog(f.session)).toThrow('output-limit'); + }); + it.each(['success', 'failure', 'cancelled', 'skipped'])('delivers native %s and waits for the existing operation', async outcome => { + request(); const write = protocol.writeNativeRecord; + jest.spyOn(protocol, 'writeNativeRecord').mockImplementation((root, name, value) => { write(root, name, value); if (name.startsWith('response-')) write(root, 'done.json', { exitCode: 0 }); }); + await continueNativeVerification(f.session, f.id, outcome, { PATH: 'approved', JAVA_HOME: 'java' }); + expect(protocol.readNativeRecord(f.session, `response-${f.id}.json`)).toEqual({ id: f.id, success: outcome === 'success', path: 'approved', javaHome: 'java' }); + expect(fs.existsSync(join(f.session, 'request.json'))).toBe(false); + }); + it('rejects a replayed continuation', async () => { + request(); protocol.writeNativeRecord(f.session, `response-${f.id}.json`, {}); + await expect(continueNativeVerification(f.session, f.id, 'success')).rejects.toThrow('handoff-invalid'); + }); + it('rejects a stale request before signaling the worker', async () => { + request(); await expect(continueNativeVerification(f.session, 'wrong', 'success')).rejects.toThrow('handoff-invalid'); + }); + it('cleans a completed operation and makes post-job cleanup idempotent', async () => { + done(); await cleanupNativeVerification(f.session); await cleanupNativeVerification(f.session); expect(fs.existsSync(f.session)).toBe(false); + }); + it('cancels and kills a stuck worker before restoring its candidate action', async () => { + fs.mkdirSync(f.action, { recursive: true }); fs.writeFileSync(join(f.action, 'action.yml'), 'candidate'); stageNativeVerificationAction(f.setup as never, f.session); + const kill = jest.spyOn(process, 'kill').mockReturnValue(true); jest.spyOn(cp, 'spawnSync').mockReturnValue({status: 0} as never); + await cleanupNativeVerification(f.session, 0); expect(fs.readFileSync(join(f.action, 'action.yml'), 'utf8')).toBe('candidate'); + if (process.platform !== 'win32') expect(kill).toHaveBeenCalledWith(-process.pid, 'SIGKILL'); + }); + it('allows an already-exited worker during cancellation', async () => { + jest.spyOn(process, 'kill').mockImplementation(() => { throw new Error('exited'); }); jest.spyOn(cp, 'spawnSync').mockReturnValue({status: 0} as never); + await cleanupNativeVerification(f.session, 0); expect(fs.existsSync(f.session)).toBe(false); + }); + it('runs an admitted operation directly when there is no native phase', async () => { + delete process.env.COPILOT_NATIVE_WORKER_SESSION; delete process.env.COPILOT_NATIVE_PHASE; const run = jest.fn(); await runNativeVerificationEntry(run); expect(run).toHaveBeenCalledTimes(1); + }); + it('keeps opt-out workflows on their ordinary entrypoint', async () => { + delete process.env.COPILOT_NATIVE_WORKER_SESSION; process.env.COPILOT_NATIVE_PHASE = 'prepare'; jest.mocked(core.getInput).mockReturnValue('false'); + const run = jest.fn(); await runNativeVerificationEntry(run); expect(run).toHaveBeenCalledTimes(1); + }); + it('executes the worker callback once and retains its final status', async () => { + const run = jest.fn().mockResolvedValue(undefined); await runNativeVerificationEntry(run); + expect(run).toHaveBeenCalledTimes(1); expect(protocol.readNativeRecord(f.session, 'done.json')).toEqual({ exitCode: 0 }); + }); + it('rejects native phases outside GitHub Actions', async () => { + delete process.env.COPILOT_NATIVE_WORKER_SESSION; process.env.COPILOT_NATIVE_PHASE = 'prepare'; delete process.env.GITHUB_ACTIONS; + await expect(runNativeVerificationEntry(jest.fn())).rejects.toThrow('native-workflow-required'); + }); + it('rejects a worker session owned by a different process', async () => { + const session = protocol.nativeSession(f.session); protocol.writeNativeRecord(f.session, 'session.json', { ...session, pid: process.pid + 1 }); + const run = jest.fn(); await expect(runNativeVerificationEntry(run)).rejects.toThrow('handoff-invalid'); expect(run).not.toHaveBeenCalled(); + }); + it('waits for publication of its initial worker session with a fixed deadline', async () => { + fs.rmSync(join(f.session, 'session.json')); let now = 0; jest.spyOn(Date, 'now').mockImplementation(() => now++ ? 30_001 : 0); + await expect(runNativeVerificationEntry(jest.fn())).rejects.toThrow('timeout'); + }); + it('rejects a native continuation without a ticket', async () => { + delete process.env.COPILOT_NATIVE_WORKER_SESSION; process.env.GITHUB_ACTIONS = 'true'; process.env.COPILOT_NATIVE_PHASE = 'continue'; delete process.env.COPILOT_NATIVE_TICKET; + await expect(runNativeVerificationEntry(jest.fn())).rejects.toThrow('handoff-invalid'); + }); + it('rejects unknown phases without running the operation', async () => { + delete process.env.COPILOT_NATIVE_WORKER_SESSION; process.env.GITHUB_ACTIONS = 'true'; process.env.COPILOT_NATIVE_PHASE = 'unknown'; process.env.COPILOT_NATIVE_TICKET = f.session; + await expect(runNativeVerificationEntry(jest.fn())).rejects.toThrow('handoff-invalid'); + }); + it('finishes a successful operation and removes its private state', async () => { + delete process.env.COPILOT_NATIVE_WORKER_SESSION; process.env.GITHUB_ACTIONS = 'true'; process.env.COPILOT_NATIVE_PHASE = 'finish'; process.env.COPILOT_NATIVE_TICKET = f.session; done(); + await runNativeVerificationEntry(jest.fn()); expect(core.setFailed).not.toHaveBeenCalled(); expect(fs.existsSync(f.session)).toBe(true); + }); + it('finishes failed operations without overriding their status', async () => { + delete process.env.COPILOT_NATIVE_WORKER_SESSION; process.env.GITHUB_ACTIONS = 'true'; process.env.COPILOT_NATIVE_PHASE = 'finish'; process.env.COPILOT_NATIVE_TICKET = f.session; done(1); + await runNativeVerificationEntry(jest.fn()); expect(core.setFailed).toHaveBeenCalled(); + }); + it('uses the post phase only for cleanup', async () => { + jest.mocked(core.getState).mockImplementation(key => key === 'copilot-native-post' ? 'true' : f.session); done(); const run = jest.fn(); + await runNativeVerificationEntry(run); expect(run).not.toHaveBeenCalled(); expect(fs.existsSync(f.session)).toBe(false); + }); + it('allows post phases without an active verification session', async () => { + jest.mocked(core.getState).mockImplementation(key => key === 'copilot-native-post' ? 'true' : ''); const run = jest.fn(); await runNativeVerificationEntry(run); expect(run).not.toHaveBeenCalled(); + }); + it('starts one detached worker with private output channels and no credential file', async () => { + delete process.env.COPILOT_NATIVE_WORKER_SESSION; process.env.GITHUB_ACTIONS = 'true'; process.env.COPILOT_NATIVE_PHASE = 'prepare'; + jest.spyOn(cp, 'spawn').mockImplementation((_program, _args, options) => { + const root = options!.env!.COPILOT_NATIVE_WORKER_SESSION!; roots.push(root); const child = new EventEmitter() as EventEmitter & {pid: number; unref(): void}; + child.pid = 12345; child.unref = jest.fn(); queueMicrotask(() => { protocol.writeNativeRecord(root, 'done.json', { exitCode: 0 }); child.emit('spawn'); }); return child as never; + }); + const run = jest.fn(); await runNativeVerificationEntry(run); expect(run).not.toHaveBeenCalled(); expect(cp.spawn).toHaveBeenCalledTimes(1); + expect(core.saveState).toHaveBeenCalledWith('native-verification-ticket', roots[0]); expect(fs.readdirSync(roots[0])).not.toContain('credentials'); + }); + it('cleans a failed worker launch', async () => { + delete process.env.COPILOT_NATIVE_WORKER_SESSION; process.env.GITHUB_ACTIONS = 'true'; process.env.COPILOT_NATIVE_PHASE = 'prepare'; + jest.spyOn(cp, 'spawn').mockImplementation((_program, _args, options) => { + roots.push(options!.env!.COPILOT_NATIVE_WORKER_SESSION!); const child = new EventEmitter(); queueMicrotask(() => child.emit('error', new Error('spawn'))); return child as never; + }); + await expect(runNativeVerificationEntry(jest.fn())).rejects.toThrow('spawn'); expect(fs.existsSync(roots[0])).toBe(false); + }); +}); diff --git a/src/infrastructure/__tests__/native_verification_fixture.ts b/src/infrastructure/__tests__/native_verification_fixture.ts new file mode 100644 index 000000000..9c7c6f8a2 --- /dev/null +++ b/src/infrastructure/__tests__/native_verification_fixture.ts @@ -0,0 +1,41 @@ +import { createPrivateVerificationRuntimeRoot } from '../agents/agent_private_install_root'; +import { removeNativeVerificationProfiles } from '../native_verification_profiles'; +import fs from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { randomUUID } from 'node:crypto'; +import { prepareUntrustedCommandEnvironment } from '../untrusted_command_environment'; +import { writeNativeRecord, readNativeRecord, type NativeVerificationRequest } from '../native_verification_protocol'; +import { VERIFICATION_SETUP_DIRECTORY } from '../../application/policies/verification_setup_policy'; +import type { PreparedVerificationSetup } from '../../application/ports/verification_setup_ports'; + +export const COMPOSITE = 'name: Fixture\nruns:\n using: composite\n steps:\n - run: echo reviewed\n shell: bash\n'; +export function nativeVerificationFixture() { + const root = fs.realpathSync(fs.mkdtempSync(join(tmpdir(), 'copilot-native-fixture-'))); + const runtime = prepareUntrustedCommandEnvironment(); + const session = fs.realpathSync(createPrivateVerificationRuntimeRoot()); + const previous = process.env.COPILOT_NATIVE_WORKER_SESSION; + const git = (...args: string[]) => execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); + git('init'); git('config', 'user.name', 'Fixture'); git('config', 'user.email', 'fixture@example.test'); + fs.writeFileSync(join(root, 'manifest'), 'initial'); git('add', '.'); git('commit', '-m', 'fixture'); + const cwd = jest.spyOn(process, 'cwd').mockReturnValue(root); + process.env.COPILOT_NATIVE_WORKER_SESSION = session; + writeNativeRecord(session, 'session.json', { schemaVersion: 1, workspace: root, run: process.env.GITHUB_RUN_ID ?? '', + attempt: process.env.GITHUB_RUN_ATTEMPT ?? '', job: process.env.GITHUB_JOB ?? '', pid: process.pid }); + const setup: PreparedVerificationSetup = Object.freeze({ kind: 'ready', workspace: root, sourceSha: 'a'.repeat(40), files: Object.freeze([ + Object.freeze({ path: 'action.yml', base64: Buffer.from(COMPOSITE).toString('base64'), executable: false }), + Object.freeze({ path: 'scripts/prepare.sh', base64: Buffer.from('echo reviewed').toString('base64'), executable: true }), + ]) }); + const respond = (edit?: (request: NativeVerificationRequest) => Record) => { + const request = readNativeRecord(session, 'request.json', 32_768)!; + if (!request) throw new Error('No pending native setup'); + writeNativeRecord(session, `response-${request.id}.json`, { id: request.id, success: true, path: request.environment.PATH ?? '', ...edit?.(request) }); + }; + return { root, runtime, session, git, setup, respond, action: join(root, VERIFICATION_SETUP_DIRECTORY), + id: randomUUID(), cleanup: () => { + cwd.mockRestore(); + if (previous === undefined) delete process.env.COPILOT_NATIVE_WORKER_SESSION; else process.env.COPILOT_NATIVE_WORKER_SESSION = previous; + if (fs.existsSync(session)) removeNativeVerificationProfiles(session); fs.rmSync(session, { recursive: true, force: true }); runtime.cleanup(); fs.rmSync(root, { recursive: true, force: true }); + } }; +} diff --git a/src/infrastructure/__tests__/native_verification_profiles.test.ts b/src/infrastructure/__tests__/native_verification_profiles.test.ts new file mode 100644 index 000000000..3f4c63beb --- /dev/null +++ b/src/infrastructure/__tests__/native_verification_profiles.test.ts @@ -0,0 +1,38 @@ +import fs from 'node:fs'; +import { join } from 'node:path'; +import { nativeVerificationFixture } from './native_verification_fixture'; +import { nativeVerificationProfilePaths, nativeVerificationRetainsProfile, registerNativeVerificationProfile, removeNativeVerificationProfiles } from '../native_verification_profiles'; +import { writeNativeRecord } from '../native_verification_protocol'; + +describe('dependency-only native profile lifecycle', () => { + let f: ReturnType; + beforeEach(() => { f = nativeVerificationFixture(); }); + afterEach(() => { fs.rmSync(join(f.session, 'profiles.json'), { force: true }); f.cleanup(); }); + it('retains an operation-owned profile for native post steps, then removes it', () => { + expect(nativeVerificationRetainsProfile(f.runtime.environment.HOME)).toBe(false); + registerNativeVerificationProfile(f.session, f.runtime.environment.HOME); expect(nativeVerificationRetainsProfile(f.runtime.environment.HOME)).toBe(true); + removeNativeVerificationProfiles(f.session); expect(fs.existsSync(f.runtime.environment.HOME)).toBe(false); + removeNativeVerificationProfiles(f.session); + }); + it('rejects duplicate registration without corrupting the registry', () => { + registerNativeVerificationProfile(f.session, f.runtime.environment.HOME); + expect(() => registerNativeVerificationProfile(f.session, f.runtime.environment.HOME)).toThrow('handoff-invalid'); + expect(nativeVerificationProfilePaths(f.session)).toEqual([f.runtime.environment.HOME]); + }); + it.each([null, {}, ['/outside'], Array(21).fill('/outside')])('rejects malformed profile metadata %j', value => { + writeNativeRecord(f.session, 'profiles.json', value); expect(() => nativeVerificationProfilePaths(f.session)).toThrow('handoff-invalid'); + }); + it('never registers the controller root or an unrelated directory', () => { + expect(() => registerNativeVerificationProfile(f.session, f.session)).toThrow('handoff-invalid'); + expect(() => registerNativeVerificationProfile(f.session, f.root)).toThrow('handoff-invalid'); + expect(nativeVerificationProfilePaths(f.session)).toEqual([]); + }); + it('refuses cleanup of a profile whose owner marker changed', () => { + registerNativeVerificationProfile(f.session, f.runtime.environment.HOME); + fs.writeFileSync(join(f.runtime.environment.HOME, 'native-session-owner'), 'other session'); + expect(() => removeNativeVerificationProfiles(f.session)).toThrow('handoff-invalid'); expect(fs.existsSync(f.runtime.environment.HOME)).toBe(true); + }); + it('does not retain a profile outside a native worker', () => { + delete process.env.COPILOT_NATIVE_WORKER_SESSION; expect(nativeVerificationRetainsProfile(f.runtime.environment.HOME)).toBe(false); + }); +}); diff --git a/src/infrastructure/__tests__/native_verification_protocol.test.ts b/src/infrastructure/__tests__/native_verification_protocol.test.ts new file mode 100644 index 000000000..552ce021a --- /dev/null +++ b/src/infrastructure/__tests__/native_verification_protocol.test.ts @@ -0,0 +1,72 @@ +import fs from 'node:fs'; +import { join } from 'node:path'; +import { nativeSession, readNativeRecord, writeNativeRecord } from '../native_verification_protocol'; +import { verificationCandidateIdentity } from '../native_verification_candidate'; +import { stageNativeVerificationAction, restoreNativeVerificationAction } from '../native_verification_overlay'; +import { nativeVerificationFixture } from './native_verification_fixture'; + +describe('native session provenance and filesystem contracts', () => { + let f: ReturnType; + beforeEach(() => { f = nativeVerificationFixture(); }); + afterEach(() => { jest.restoreAllMocks(); f.cleanup(); }); + it('reads atomically published operation records and absent records', () => { + writeNativeRecord(f.session, 'record.json', { value: 42 }); expect(readNativeRecord(f.session, 'record.json')).toEqual({ value: 42 }); + expect(readNativeRecord(f.session, 'absent.json')).toBeUndefined(); expect(nativeSession(f.session).pid).toBe(process.pid); + }); + it.each(['run', 'attempt', 'job', 'workspace', 'pid', 'schemaVersion'])('rejects a session with mismatched %s', field => { + const session = nativeSession(f.session); writeNativeRecord(f.session, 'session.json', { ...session, [field]: 'other' }); + expect(() => nativeSession(f.session)).toThrow('handoff-invalid'); + }); + it('rejects missing sessions', () => { + fs.rmSync(join(f.session, 'session.json')); expect(() => nativeSession(f.session)).toThrow('handoff-invalid'); + }); + it('rejects arbitrary ticket roots before reading or cleaning them', () => { + expect(() => nativeSession(f.root)).toThrow('handoff-invalid'); + }); + it('rejects writable operation directories on POSIX', () => { + if (process.platform === 'win32') return; + fs.chmodSync(f.session, 0o777); expect(() => nativeSession(f.session)).toThrow('handoff-invalid'); fs.chmodSync(f.session, 0o700); + }); + it.each(['oversize', 'directory', 'hardlink', 'invalid-json'])('rejects %s records', kind => { + const file = join(f.session, 'record.json'); + if (kind === 'directory') fs.mkdirSync(file); + else if (kind === 'hardlink') { fs.writeFileSync(join(f.session, 'original'), '{}'); fs.linkSync(join(f.session, 'original'), file); } + else fs.writeFileSync(file, kind === 'oversize' ? 'x'.repeat(16_385) : 'not json'); + expect(() => readNativeRecord(f.session, 'record.json')).toThrow('handoff-invalid'); + }); + it('rejects replaced records before consuming their content', () => { + writeNativeRecord(f.session, 'record.json', {}); const original = fs.fstatSync; + jest.spyOn(fs, 'fstatSync').mockImplementation(((fd: number, options: {bigint: true}) => ({ ...original(fd, options), ino: 0n })) as typeof fs.fstatSync); + expect(() => readNativeRecord(f.session, 'record.json')).toThrow('handoff-invalid'); + }); + it('restores originals on explicit cancellation and makes cleanup idempotent', () => { + fs.mkdirSync(f.action, { recursive: true }); fs.writeFileSync(join(f.action, 'action.yml'), 'original'); + stageNativeVerificationAction(f.setup as never, f.session); restoreNativeVerificationAction(f.session, f.root); restoreNativeVerificationAction(f.session, f.root); + expect(fs.readFileSync(join(f.action, 'action.yml'), 'utf8')).toBe('original'); + }); + it('rejects unsafe action ancestors', () => { + fs.writeFileSync(join(f.root, '.github'), 'file'); + expect(() => stageNativeVerificationAction(f.setup as never, f.session)).toThrow('helper-invalid'); + }); + it('rejects a non-directory action without overwriting it', () => { + fs.mkdirSync(join(f.root, '.github/actions'), { recursive: true }); fs.writeFileSync(f.action, 'original'); + expect(() => stageNativeVerificationAction(f.setup as never, f.session)).toThrow('helper-invalid'); expect(fs.readFileSync(f.action, 'utf8')).toBe('original'); + }); + it('rejects malformed overlay recovery records without deleting candidate content', () => { + writeNativeRecord(f.session, 'overlay.json', { target: '/other', backup: '/other', created: [] }); + expect(() => restoreNativeVerificationAction(f.session, f.root)).toThrow('handoff-invalid'); + }); + it('binds tracked contents, branch and staged content, not filesystem timestamps', () => { + const before = verificationCandidateIdentity(f.root, f.runtime.environment); + fs.utimesSync(join(f.root, 'manifest'), new Date(), new Date()); expect(verificationCandidateIdentity(f.root, f.runtime.environment)).toBe(before); + f.git('checkout', '-b', 'other'); expect(verificationCandidateIdentity(f.root, f.runtime.environment)).not.toBe(before); + }); + it('rejects excessive publishable untracked files', () => { + for (let i = 0; i < 257; i++) fs.writeFileSync(join(f.root, `untracked-${i}`), 'x'); + expect(() => verificationCandidateIdentity(f.root, f.runtime.environment)).toThrow('candidate-changed'); + }); + it('binds untracked file content and links without following the link', () => { + fs.writeFileSync(join(f.root, 'untracked'), 'one'); const before = verificationCandidateIdentity(f.root, f.runtime.environment); + fs.writeFileSync(join(f.root, 'untracked'), 'two'); expect(verificationCandidateIdentity(f.root, f.runtime.environment)).not.toBe(before); + }); +}); diff --git a/src/infrastructure/__tests__/native_verification_redaction.test.ts b/src/infrastructure/__tests__/native_verification_redaction.test.ts new file mode 100644 index 000000000..ffa8c1ffa --- /dev/null +++ b/src/infrastructure/__tests__/native_verification_redaction.test.ts @@ -0,0 +1,40 @@ +import { nativeWorkerSecrets, redactNativeWorkerLogs } from '../native_verification_redaction'; + +describe('native worker private-log redaction', () => { + afterEach(() => jest.restoreAllMocks()); + it('collects credentials from Action env and JSON input without treating metadata as secrets', () => { + expect(nativeWorkerSecrets({ INPUT_TOKEN: 'pat-secret', OPENAI_API_KEY: 'model-secret', INPUT_VARS_JSON: '{"token":"json-secret","main-branch":"master"}', GITHUB_RUN_ID: '12345' })) + .toEqual(expect.arrayContaining(['pat-secret', 'model-secret', 'json-secret'])); + expect(nativeWorkerSecrets({ GITHUB_RUN_ID: '12345', INPUT_VARS_JSON: 'invalid' })).toEqual([]); + }); + it('redacts secrets spanning chunks before stdout reaches disk', () => { + const writes: string[] = []; jest.spyOn(process.stdout, 'write').mockImplementation((data) => { writes.push(String(data)); return true; }); + const restore = redactNativeWorkerLogs({ INPUT_TOKEN: 'private-token' }); + process.stdout.write('prefix private-'); process.stdout.write('token suffix\n'); restore(); + expect(writes.join('')).toBe('prefix [REDACTED] suffix\n'); + }); + it('redacts stderr and flushes a final unterminated line', () => { + const writes: string[] = []; jest.spyOn(process.stderr, 'write').mockImplementation(data => { writes.push(String(data)); return true; }); + const restore = redactNativeWorkerLogs({ MODEL_API_KEY: 'private-key' }); process.stderr.write(Buffer.from('private-key')); restore(); expect(writes.join('')).toBe('[REDACTED]'); + }); + it('consumes dynamic mask commands and never stores their payload', () => { + const writes: string[] = []; jest.spyOn(process.stdout, 'write').mockImplementation(data => { writes.push(String(data)); return true; }); + const restore = redactNativeWorkerLogs({}); process.stdout.write('::add-mask::dynamic%25secret\n'); process.stdout.write('dynamic%secret\n'); restore(); + expect(writes.join('')).toBe('[REDACTED]\n'); + }); + it('supports multiline credentials and callbacks', () => { + const writes: string[] = []; jest.spyOn(process.stdout, 'write').mockImplementation(data => { writes.push(String(data)); return true; }); + const restore = redactNativeWorkerLogs({ TOKEN: 'first-line\nsecond-line' }); const callback = jest.fn(); + process.stdout.write('first-line\nsecond-line\n', callback); process.stdout.write('ok\n', 'utf8', callback); restore(); + expect(writes.join('')).toBe('[REDACTED]\n[REDACTED]\nok\n'); expect(callback).toHaveBeenCalledTimes(2); + }); + it('preserves split UTF-8 and ordinary operational logs', () => { + const writes: string[] = []; jest.spyOn(process.stdout, 'write').mockImplementation(data => { writes.push(String(data)); return true; }); + const restore = redactNativeWorkerLogs({}); const bytes = Buffer.from('verificación\n'); + process.stdout.write(bytes.subarray(0, 12)); process.stdout.write(bytes.subarray(12)); restore(); expect(writes.join('')).toBe('verificación\n'); + }); + it('bounds an unterminated line', () => { + jest.spyOn(process.stdout, 'write').mockReturnValue(true); const restore = redactNativeWorkerLogs({}); + expect(() => process.stdout.write('x'.repeat(1_048_577))).toThrow('fixed limit'); restore(); + }); +}); diff --git a/src/infrastructure/__tests__/native_verification_runner.test.ts b/src/infrastructure/__tests__/native_verification_runner.test.ts new file mode 100644 index 000000000..ed5fd5620 --- /dev/null +++ b/src/infrastructure/__tests__/native_verification_runner.test.ts @@ -0,0 +1,110 @@ +import fs from 'node:fs'; +import { join, delimiter } from 'node:path'; +import { prepareRepositoryVerification } from '../verification_setup_runner'; +import { GitCommitAdapter } from '../git_commit_adapter'; +import { runVerifyCommands } from '../../application/usecases/steps/commit/bugbot/verify_command_runner'; +import { writeNativeRecord } from '../native_verification_protocol'; +import { nativeVerificationFixture, COMPOSITE } from './native_verification_fixture'; + +// The test driver supplies native runner results; production never interprets YAML. +describe('native verification boundary', () => { + let f: ReturnType; + beforeEach(() => { f = nativeVerificationFixture(); }); + afterEach(() => { jest.restoreAllMocks(); f.cleanup(); }); + it('does no work for disabled setup', async () => { + await prepareRepositoryVerification({ kind: 'disabled' }, f.runtime.environment); + expect(fs.existsSync(join(f.session, 'request.json'))).toBe(false); + }); + it('defers blocked acquisition until an actual verifier runs', async () => { + const execute = jest.fn().mockResolvedValue(0); + const adapter = new GitCommitAdapter(execute, jest.fn(), { kind: 'blocked', reason: 'source-unavailable' }); + await adapter.execute('git', ['status'], { untrusted: true }); + await expect(adapter.execute('node', ['test'], { untrusted: true, verifyRuntimePrerequisites: true })).rejects.toMatchObject({ reason: 'source-unavailable' }); + expect(execute).toHaveBeenCalledTimes(1); + }); + it('executes no setup for empty verification lists', async () => { + const execute = jest.fn(); expect(await runVerifyCommands([], execute)).toEqual({ success: true }); expect(execute).not.toHaveBeenCalled(); + }); + it('fails closed in the local CLI or an old workflow without native continuation', async () => { + delete process.env.COPILOT_NATIVE_WORKER_SESSION; + await expect(prepareRepositoryVerification(f.setup, f.runtime.environment)).rejects.toMatchObject({ reason: 'native-workflow-required' }); + }); + it('stages only pinned YAML and assets, then restores the candidate byte for byte', async () => { + fs.mkdirSync(f.action, { recursive: true }); fs.writeFileSync(join(f.action, 'action.yml'), 'candidate'); + const running = prepareRepositoryVerification(f.setup, f.runtime.environment); + expect(fs.readFileSync(join(f.action, 'action.yml'), 'utf8')).toBe(COMPOSITE); + expect(fs.readFileSync(join(f.action, 'scripts/prepare.sh'), 'utf8')).toBe('echo reviewed'); + f.respond(); await running; + expect(fs.readFileSync(join(f.action, 'action.yml'), 'utf8')).toBe('candidate'); + expect(fs.existsSync(join(f.action, 'scripts'))).toBe(false); + }); + it('removes a temporary action absent from the candidate', async () => { + const running = prepareRepositoryVerification(f.setup, f.runtime.environment); f.respond(); await running; + expect(fs.existsSync(f.action)).toBe(false); expect(f.git('status', '--porcelain')).toBe(''); + }); + it('prepares each changed candidate with a fresh HOME before preflight and verification', async () => { + const homes: string[] = []; const order: string[] = []; + const adapter = new GitCommitAdapter(async (_p, _a, options) => { order.push('verify'); homes.push(options!.env!.HOME); return 0; }, () => { order.push('preflight'); }, f.setup); + for (const version of ['first', 'changed']) { + fs.writeFileSync(join(f.root, 'manifest'), version); + const running = adapter.execute('node', ['test'], { untrusted: true, verifyRuntimePrerequisites: true }); + f.respond(request => { + expect(fs.readFileSync(join(f.root, 'manifest'), 'utf8')).toBe(version); + for (const key of ['PAT', 'GITHUB_TOKEN', 'OPENAI_API_KEY', 'CODEX_HOME', 'NODE_OPTIONS']) expect(request.environment).not.toHaveProperty(key); + return {}; + }); + await running; + } + expect(order).toEqual(['preflight', 'verify', 'preflight', 'verify']); expect(new Set(homes).size).toBe(2); + expect(homes.every(home => fs.existsSync(home))).toBe(true); + }); + it('accepts only validated runtime directory additions and JAVA_HOME', async () => { + const bin = join(f.root, 'tools'); fs.mkdirSync(bin); const running = prepareRepositoryVerification(f.setup, f.runtime.environment); + f.respond(request => ({ path: `${bin}${delimiter}${request.environment.PATH}`, javaHome: bin })); await running; + expect(f.runtime.environment.PATH.split(delimiter)[0]).toBe(bin); expect(f.runtime.environment.JAVA_HOME).toBe(bin); + }); + it('drops earlier profiles from the runner PATH instead of reusing their readiness', async () => { + const adapter = new GitCommitAdapter(async (_p, _a, options) => { expect(options!.env!.PATH).not.toContain('stale-tools'); return 0; }, () => {}, f.setup); + let firstHome = ''; + const first = adapter.execute('node', ['test'], { untrusted: true, verifyRuntimePrerequisites: true }); + f.respond(request => { firstHome = request.environment.HOME; fs.mkdirSync(join(firstHome, 'stale-tools')); return {}; }); await first; + const second = adapter.execute('node', ['test'], { untrusted: true, verifyRuntimePrerequisites: true }); + f.respond(request => ({ path: `${join(firstHome, 'stale-tools')}${delimiter}${request.environment.PATH}` })); await second; + }); + it.each(['tracked', 'untracked', 'index', 'head'])('withholds verification when setup changes the %s candidate', async kind => { + if (kind === 'untracked') fs.writeFileSync(join(f.root, 'new-file'), 'candidate'); + const running = prepareRepositoryVerification(f.setup, f.runtime.environment); + if (kind === 'tracked') fs.writeFileSync(join(f.root, 'manifest'), 'modified'); + if (kind === 'untracked') fs.writeFileSync(join(f.root, 'new-file'), 'modified'); + if (kind === 'index') { fs.writeFileSync(join(f.root, 'manifest'), 'modified'); f.git('add', '.'); } + if (kind === 'head') { f.git('commit', '--allow-empty', '-m', 'unexpected'); } + f.respond(); await expect(running).rejects.toMatchObject({ reason: 'candidate-changed' }); + expect(fs.existsSync(f.action)).toBe(false); + }); + it.each([{success: false}, {id: 'wrong'}, {path: 1}, {javaHome: 1}, {path: '/outside/invalid'}])('rejects failed or malformed native outcomes %j', async response => { + const running = prepareRepositoryVerification(f.setup, f.runtime.environment); f.respond(() => response); + await expect(running).rejects.toThrow('Verification, commit and push are withheld'); + expect(fs.existsSync(f.action)).toBe(false); + }); + it('withholds the command and probes after a setup failure', async () => { + const execute = jest.fn(); const preflight = jest.fn(); const adapter = new GitCommitAdapter(execute, preflight, f.setup); + const running = adapter.execute('node', ['test'], { untrusted: true, verifyRuntimePrerequisites: true }); f.respond(() => ({ success: false })); + await expect(running).rejects.toMatchObject({ reason: 'process-failed' }); expect(execute).not.toHaveBeenCalled(); expect(preflight).not.toHaveBeenCalled(); + }); + it('cancels a pending setup and restores the candidate', async () => { + const running = prepareRepositoryVerification(f.setup, f.runtime.environment); writeNativeRecord(f.session, 'cancel.json', { cancelled: true }); + await expect(running).rejects.toMatchObject({ reason: 'cancelled' }); expect(fs.existsSync(f.action)).toBe(false); + }); + it('times out without retrying or publishing', async () => { + let now = 0; jest.spyOn(Date, 'now').mockImplementation(() => now); + const running = prepareRepositoryVerification(f.setup, f.runtime.environment); now = 300_001; + await expect(running).rejects.toMatchObject({ reason: 'timeout' }); expect(fs.existsSync(f.action)).toBe(false); + }); + it('enforces the fixed native invocation ceiling', async () => { + writeNativeRecord(f.session, 'request-count.json', { count: 20 }); + await expect(prepareRepositoryVerification(f.setup, f.runtime.environment)).rejects.toMatchObject({ reason: 'handoff-invalid' }); + }); + it('rejects workspace switches before setup', async () => { + await expect(prepareRepositoryVerification({ ...f.setup, workspace: f.session } as typeof f.setup, f.runtime.environment)).rejects.toMatchObject({ reason: 'workspace-changed' }); + }); +}); diff --git a/src/infrastructure/__tests__/native_verification_workflow.test.ts b/src/infrastructure/__tests__/native_verification_workflow.test.ts new file mode 100644 index 000000000..bfb2d3cee --- /dev/null +++ b/src/infrastructure/__tests__/native_verification_workflow.test.ts @@ -0,0 +1,33 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { load } from 'js-yaml'; +const { assertNativeVerificationWorkflow } = require('../../../scripts/validate-workflow-contract.cjs') as { assertNativeVerificationWorkflow(file: string, workflow: any): void }; +const { MAX_REQUESTS, phaseSteps } = require('../../../scripts/generate-native-verification-workflows.cjs') as {MAX_REQUESTS: number; phaseSteps(action: string): any[]}; + +describe('native verification workflow contract', () => { + it.each(['.github/workflows', 'setup/workflows'])('parses canonical native YAML and anchors from %s', directory => { + for (const name of ['copilot_issue_comment.yml', 'copilot_pull_request_comment.yml']) { + const file = path.join(process.cwd(), directory, name); const workflow = load(fs.readFileSync(file, 'utf8')); + expect(() => assertNativeVerificationWorkflow(file, workflow)).not.toThrow(); + } + }); + it('has exactly twenty native setup slots and one completion, with five-minute native timeouts', () => { + const steps = phaseSteps('./'); expect(MAX_REQUESTS).toBe(20); expect(steps).toHaveLength(41); + expect(steps.filter(step => step.uses === './.github/actions/copilot-verification-setup').every(step => step['timeout-minutes'] === 5)).toBe(true); + }); + it.each(['timeout', 'env', 'condition', 'reference', 'request', 'outcome', 'finish', 'extra', 'remove', 'prepare'])('rejects a weakened %s contract', kind => { + const file = path.join(process.cwd(), 'setup/workflows/copilot_issue_comment.yml'); const workflow = load(fs.readFileSync(file, 'utf8')) as any; + const job = Object.values(workflow.jobs)[0] as any; const start = job.steps.findIndex((step: any) => step.id === 'copilot'); const steps = job.steps; + if (kind === 'timeout') steps[start + 1]['timeout-minutes'] = 60; + if (kind === 'env') steps[start + 1].env.OPENAI_API_KEY = '${{ secrets.OPENAI_API_KEY }}'; + if (kind === 'condition') steps[start + 1].if = '${{ always() }}'; + if (kind === 'reference') steps[start + 2].uses = 'vypdev/copilot@other'; + if (kind === 'request') steps[start + 2].env.COPILOT_NATIVE_REQUEST = 'stale'; + if (kind === 'outcome') steps[start + 2].env.COPILOT_NATIVE_OUTCOME = 'success'; + if (kind === 'finish') steps[steps.length - 1].if = '${{ success() }}'; + if (kind === 'extra') steps.push({run: 'echo bypass'}); + if (kind === 'remove') steps.splice(start + 1, 2); + if (kind === 'prepare') delete steps[start].env.COPILOT_NATIVE_PHASE; + expect(() => assertNativeVerificationWorkflow(file, workflow)).toThrow(/native/u); + }); +}); diff --git a/src/infrastructure/__tests__/verification_setup_configuration.test.ts b/src/infrastructure/__tests__/verification_setup_configuration.test.ts index 305362496..431bbf85c 100644 --- a/src/infrastructure/__tests__/verification_setup_configuration.test.ts +++ b/src/infrastructure/__tests__/verification_setup_configuration.test.ts @@ -48,7 +48,7 @@ describe('verification setup installation contract', () => { }, ); it('preserves the consumer helper through install and repeated workflow updates', () => { - const helper = path.join(root, '.copilot/verification-setup.mjs'); fs.mkdirSync(path.dirname(helper), { recursive: true }); + const helper = path.join(root, '.github/actions/copilot-verification-setup/action.yml'); fs.mkdirSync(path.dirname(helper), { recursive: true }); const bytes = 'export const repositoryOwned = true;\n'; fs.writeFileSync(helper, bytes); const config = createDefaultSetupConfiguration(); config.repository.verificationSetup = true; const source = path.resolve(__dirname, '../../../setup'); @@ -59,6 +59,6 @@ describe('verification setup installation contract', () => { expect(fs.readFileSync(helper, 'utf8')).toBe(bytes); } const manifest = fs.readFileSync(path.join(root, '.copilot/setup-manifest.json'), 'utf8'); - expect(manifest).not.toContain('verification-setup.mjs'); + expect(manifest).not.toContain('copilot-verification-setup/action.yml'); }); }); diff --git a/src/infrastructure/__tests__/verification_setup_runner.test.ts b/src/infrastructure/__tests__/verification_setup_runner.test.ts index 75fefd720..0b8e231b5 100644 --- a/src/infrastructure/__tests__/verification_setup_runner.test.ts +++ b/src/infrastructure/__tests__/verification_setup_runner.test.ts @@ -1,117 +1,8 @@ import fs from 'node:fs'; import { tmpdir } from 'node:os'; import { delimiter, join } from 'node:path'; -import { GitCommitAdapter } from '../git_commit_adapter'; -import { prepareUntrustedCommandEnvironment } from '../untrusted_command_environment'; -import { prepareRepositoryVerification } from '../verification_setup_runner'; import { applyVerificationSetupResult } from '../verification_setup_result'; -import { runVerificationSetupProcess } from '../verification_setup_process'; -import type { PreparedVerificationSetup } from '../../application/ports/verification_setup_ports'; import { VerificationSetupError } from '../../application/policies/verification_setup_policy'; -import { runVerifyCommands } from '../../application/usecases/steps/commit/bugbot/verify_command_runner'; - -const sourceSha = 'a'.repeat(40); -describe('repository verification setup execution', () => { - let root: string; - let runtime: ReturnType; - const setup = (script = 'export {};'): PreparedVerificationSetup => Object.freeze({ kind: 'ready', script, sourceSha, workspace: root, node: process.execPath }); - beforeEach(() => { - root = fs.realpathSync(fs.mkdtempSync(join(tmpdir(), 'copilot-setup-runner-'))); - runtime = prepareUntrustedCommandEnvironment({ PATH: process.env.PATH, SystemRoot: process.env.SystemRoot }); - jest.spyOn(process, 'cwd').mockReturnValue(root); - }); - afterEach(() => { jest.restoreAllMocks(); runtime.cleanup(); fs.rmSync(root, { recursive: true, force: true }); }); - - it('skips disabled setup and defers blocked snapshots until actual verification', async () => { - await prepareRepositoryVerification({ kind: 'disabled' }, runtime.environment); - const execute = jest.fn().mockResolvedValue(0); - const adapter = new GitCommitAdapter(execute, jest.fn(), { kind: 'blocked', reason: 'source-unavailable' }); - await adapter.execute('git', ['status'], { untrusted: true }); - await expect(adapter.execute('node', ['test'], { untrusted: true, verifyRuntimePrerequisites: true })).rejects.toMatchObject({ reason: 'source-unavailable' }); - expect(execute).toHaveBeenCalledTimes(1); - }); - it('does not prepare for an empty command list', async () => { - const execute = jest.fn(); - expect(await runVerifyCommands([], execute)).toEqual({ success: true }); - expect(execute).not.toHaveBeenCalled(); - }); - it('executes trusted standalone ESM in candidate cwd, without process/agent credentials', async () => { - const script = `import fs from 'node:fs'; - for (const key of ['PAT','GITHUB_TOKEN','OPENAI_API_KEY','AWS_SECRET_ACCESS_KEY','CODEX_HOME','NODE_OPTIONS','GIT_CONFIG_VALUE_0']) - if (process.env[key]) throw new Error('credential leaked'); - if (process.env.COPILOT_VERIFICATION_SETUP_PROTOCOL !== '1') throw new Error('protocol'); - fs.writeFileSync('observed.json', JSON.stringify({cwd:process.cwd(),home:process.env.HOME,sha:process.env.COPILOT_VERIFICATION_SETUP_SOURCE_SHA}));`; - await prepareRepositoryVerification(setup(script), runtime.environment); - expect(JSON.parse(fs.readFileSync(join(root, 'observed.json'), 'utf8'))).toEqual({ cwd: root, home: runtime.environment.HOME, sha: sourceSha }); - expect(runtime.environment).not.toHaveProperty('COPILOT_VERIFICATION_SETUP_RESULT'); - }); - it('refreshes setup from changed candidate manifests for each command, before probes in the same environment', async () => { - const script = `import fs from 'node:fs'; import path from 'node:path'; - const bin=path.join(process.env.HOME,'bin'); fs.mkdirSync(bin); - fs.writeFileSync(path.join(bin,'candidate'),fs.readFileSync('manifest.json')); - fs.appendFileSync('preparations','1'); - fs.writeFileSync(process.env.COPILOT_VERIFICATION_SETUP_RESULT,JSON.stringify({schemaVersion:1,path:[bin],environment:{JAVA_HOME:bin}}));`; - const order: string[] = []; const homes: string[] = []; - let observed: Record | undefined; - const adapter = new GitCommitAdapter(async (_program, _args, options) => { - order.push('verify'); expect(options?.env).toBe(observed); homes.push(options!.env!.HOME); - expect(fs.readFileSync(join(options!.env!.JAVA_HOME, 'candidate'), 'utf8')).toBe(fs.readFileSync(join(root, 'manifest.json'), 'utf8')); - return 0; - }, environment => { - order.push('preflight'); observed = environment; - expect(environment.PATH.split(delimiter)[0]).toBe(environment.JAVA_HOME); - }, setup(script)); - fs.writeFileSync(join(root, 'manifest.json'), 'first'); - await adapter.execute('node', ['test'], { untrusted: true, verifyRuntimePrerequisites: true }); - fs.writeFileSync(join(root, 'manifest.json'), 'changed'); - await adapter.execute('node', ['test'], { untrusted: true, verifyRuntimePrerequisites: true }); - expect(order).toEqual(['preflight','verify','preflight','verify']); - expect(new Set(homes).size).toBe(2); expect(homes.every(home => !fs.existsSync(home))).toBe(true); - expect(fs.readFileSync(join(root, 'preparations'), 'utf8')).toBe('11'); - }); - it('rejects a workspace switch before running any helper code', async () => { - jest.mocked(process.cwd).mockReturnValue(runtime.environment.HOME); - await expect(prepareRepositoryVerification(setup(), runtime.environment)).rejects.toMatchObject({ reason: 'workspace-changed' }); - }); - it('retains safe exit metadata and never emits helper output or raw exception text', async () => { - await expect(prepareRepositoryVerification(setup('console.error("secret @everyone"); process.exit(7);'), runtime.environment)) - .rejects.toMatchObject({ reason: 'process-failed', exitCode: 7, message: expect.stringContaining('Exit status: 7') }); - }); - it('withholds verification and cleans the private home when setup fails', async () => { - const execute = jest.fn(); const preflight = jest.fn(); - const adapter = new GitCommitAdapter(execute, preflight, setup('throw new Error("secret");')); - const result = await runVerifyCommands(['node test'], (program, args) => adapter.execute(program, args, { untrusted: true, verifyRuntimePrerequisites: true })); - expect(result).toMatchObject({ success: false, error: expect.stringContaining('Verification, commit and push are withheld') }); - expect(result.error).not.toContain('secret'); expect(execute).not.toHaveBeenCalled(); expect(preflight).not.toHaveBeenCalled(); - }); - it('fails closed on private script write errors without exposing filesystem details', async () => { - const error = jest.spyOn(fs, 'writeFileSync').mockImplementationOnce(() => { throw new Error('secret-path'); }); - await expect(prepareRepositoryVerification(setup(), runtime.environment)).rejects.toMatchObject({ reason: 'process-failed' }); - error.mockRestore(); - }); - it.each([ - ['process.exit(4);', 'process-failed', { timeoutMs: 5_000, outputBytes: 65_536 }], - ['setInterval(()=>{},1000);', 'timeout', { timeoutMs: 100, outputBytes: 65_536 }], - ['process.stdout.write("x".repeat(100000));', 'output-limit', { timeoutMs: 5_000, outputBytes: 100 }], - ['process.stderr.write("x".repeat(100000));', 'output-limit', { timeoutMs: 5_000, outputBytes: 100 }], - ])('bounds process execution %#', async (script, reason, limits) => { - const file = join(runtime.environment.HOME, 'bounded.mjs'); fs.writeFileSync(file, script); - await expect(runVerificationSetupProcess(process.execPath, file, root, runtime.environment, sourceSha, limits)).rejects.toMatchObject({ reason }); - }); - it('maps spawn failure to a sanitized closed diagnostic', async () => { - await expect(runVerificationSetupProcess(join(root, 'missing-node'), 'script', root, runtime.environment, sourceSha)).rejects.toBeInstanceOf(VerificationSetupError); - }); - it('terminates setup on cancellation and removes its signal listeners', async () => { - const file = join(runtime.environment.HOME, 'cancel.mjs'); fs.writeFileSync(file, 'setInterval(()=>{},1000);'); - const before = process.listenerCount('SIGTERM'); - const once = jest.spyOn(process, 'once'); - const running = runVerificationSetupProcess(process.execPath, file, root, runtime.environment, sourceSha); - const cancel = once.mock.calls.find(([signal]) => signal === 'SIGTERM')![1]; - cancel(); - await expect(running).rejects.toMatchObject({ reason: 'cancelled' }); - expect(process.listenerCount('SIGTERM')).toBe(before); - }); -}); describe('bounded setup result filesystem contract', () => { let root: string; let environment: Record; let result: string; diff --git a/src/infrastructure/__tests__/verification_setup_snapshot.test.ts b/src/infrastructure/__tests__/verification_setup_snapshot.test.ts index 115eeda25..533ebd53f 100644 --- a/src/infrastructure/__tests__/verification_setup_snapshot.test.ts +++ b/src/infrastructure/__tests__/verification_setup_snapshot.test.ts @@ -10,7 +10,7 @@ describe('trusted repository setup snapshot', () => { let root: string; const git = (...args: string[]) => cp.execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); const commit = (script: string | Buffer): string => { - mkdirSync(join(root, '.copilot'), { recursive: true }); + mkdirSync(join(root, '.github/actions/copilot-verification-setup'), { recursive: true }); writeFileSync(join(root, VERIFICATION_SETUP_PATH), script); git('add', '.'); git('commit', '-m', 'fixture'); const sha = git('rev-parse', 'HEAD'); @@ -27,10 +27,10 @@ describe('trusted repository setup snapshot', () => { expect(captureVerificationSetup(false, 'invalid', '/missing')).toEqual({ kind: 'disabled' }); }); it('pins trusted bytes and SHA before candidate changes or remote-ref mutation', () => { - const sha = commit('console.log("trusted");'); + const sha = commit('runs:\n using: composite\n steps:\n - run: echo trusted\n shell: bash\n'); const snapshot = captureVerificationSetup(true, 'main', root); commit('throw new Error("candidate");'); - expect(snapshot).toMatchObject({ kind: 'ready', sourceSha: sha, script: 'console.log("trusted");', workspace: root, node: process.execPath }); + expect(snapshot).toMatchObject({ kind: 'ready', sourceSha: sha, files: [expect.objectContaining({ path: 'action.yml' })], workspace: root }); expect(Object.isFrozen(snapshot)).toBe(true); }); it('defers acquisition failure on query/no-op routes', () => { @@ -46,7 +46,7 @@ describe('trusted repository setup snapshot', () => { expect(captureVerificationSetup(true, 'main', root)).toEqual({ kind: 'blocked', reason: 'source-unavailable' }); }); it('captures the canonical trusted tool root independently of later environment changes', () => { - commit('export {};'); const previous = process.env.RUNNER_TOOL_CACHE; + commit('runs:\n using: composite\n steps:\n - run: echo ready\n shell: bash\n'); const previous = process.env.RUNNER_TOOL_CACHE; try { process.env.RUNNER_TOOL_CACHE = root; expect(captureVerificationSetup(true, 'main', root)).toMatchObject({ kind: 'ready', toolRoot: root }); @@ -55,7 +55,7 @@ describe('trusted repository setup snapshot', () => { it('verifies SHA-256 Git object stores without truncating their identity', () => { rmSync(join(root, '.git'), { recursive: true }); git('init', '--object-format=sha256'); git('config', 'user.name', 'Test'); git('config', 'user.email', 'test@example.test'); - const sha = commit('export {};'); expect(sha).toHaveLength(64); + const sha = commit('runs:\n using: composite\n steps:\n - run: echo ready\n shell: bash\n'); expect(sha).toHaveLength(64); expect(captureVerificationSetup(true, 'main', root)).toMatchObject({ kind: 'ready', sourceSha: sha }); }); it('rejects enabled missing helpers at the captured trusted SHA', () => { @@ -77,7 +77,7 @@ describe('trusted repository setup snapshot', () => { expect(captureVerificationSetup(true, 'main', root)).toMatchObject({ reason: 'helper-invalid' }); }); it('never forwards runner credentials or inherited auth profiles to Git reads', () => { - commit('export {};'); + commit('runs:\n using: composite\n steps:\n - run: echo ready\n shell: bash\n'); const original = cp.spawnSync; const reads = jest.spyOn(cp, 'spawnSync').mockImplementation(((program: string, args: string[], options: cp.SpawnSyncOptions) => { expect(options.env).not.toHaveProperty('PAT'); @@ -90,7 +90,7 @@ describe('trusted repository setup snapshot', () => { expect(reads).toHaveBeenCalledTimes(3); }); it('detects corrupted blob responses instead of executing mismatched bytes', () => { - commit('export {};'); const original = cp.spawnSync; + commit('runs:\n using: composite\n steps:\n - run: echo ready\n shell: bash\n'); const original = cp.spawnSync; jest.spyOn(cp, 'spawnSync').mockImplementation(((program: string, args: string[], options: cp.SpawnSyncOptions) => { const value = original(program, args, options); return args.includes('cat-file') ? { ...value, stdout: Buffer.from('different') } : value; @@ -98,3 +98,28 @@ describe('trusted repository setup snapshot', () => { expect(captureVerificationSetup(true, 'main', root)).toMatchObject({ reason: 'helper-invalid' }); }); }); + +// These metadata failures are independent native contract cases, not shell execution tests. +describe('native composite metadata validation', () => { + let root: string; + const git = (...args: string[]) => cp.execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); + beforeEach(() => { + root = realpathSync(mkdtempSync(join(tmpdir(), 'copilot-native-metadata-'))); git('init'); git('config', 'user.name', 'Test'); git('config', 'user.email', 'test@example.test'); + mkdirSync(join(root, '.github/actions/copilot-verification-setup'), { recursive: true }); + }); + afterEach(() => rmSync(root, { recursive: true, force: true })); + it.each([ + 'runs: {using: node24, main: index.js}', 'runs: {using: composite, steps: []}', 'runs: [', + 'runs: {using: composite, steps: [null]}', 'runs: {using: composite, steps: [{uses: "./candidate-action"}]}', + 'runs:\n using: composite\n steps:\n' + Array(33).fill(' - run: echo x\n shell: bash\n').join(''), + ])('rejects invalid native metadata %#', metadata => { + writeFileSync(join(root, VERIFICATION_SETUP_PATH), metadata); git('add', '.'); git('commit', '-m', 'fixture'); git('update-ref', 'refs/remotes/origin/main', git('rev-parse', 'HEAD')); + expect(captureVerificationSetup(true, 'main', root)).toMatchObject({ kind: 'blocked', reason: 'helper-invalid' }); + }); + it('captures reviewed auxiliary script bytes independently of candidate edits', () => { + writeFileSync(join(root, VERIFICATION_SETUP_PATH), 'runs: {using: composite, steps: [{run: "echo ready", shell: bash}]}'); + writeFileSync(join(root, '.github/actions/copilot-verification-setup/prepare.sh'), 'reviewed'); git('add', '.'); git('commit', '-m', 'fixture'); git('update-ref', 'refs/remotes/origin/main', git('rev-parse', 'HEAD')); + const snapshot = captureVerificationSetup(true, 'main', root); writeFileSync(join(root, '.github/actions/copilot-verification-setup/prepare.sh'), 'candidate'); + expect(snapshot).toMatchObject({kind: 'ready', files: expect.arrayContaining([expect.objectContaining({path: 'prepare.sh', base64: Buffer.from('reviewed').toString('base64')})])}); + }); +}); diff --git a/src/infrastructure/__tests__/verification_setup_sync.test.ts b/src/infrastructure/__tests__/verification_setup_sync.test.ts index 0519fe5e6..9e60d4d06 100644 --- a/src/infrastructure/__tests__/verification_setup_sync.test.ts +++ b/src/infrastructure/__tests__/verification_setup_sync.test.ts @@ -1,3 +1,4 @@ +import { removeNativeVerificationProfiles } from '../native_verification_profiles'; import fs from 'node:fs'; import { execFileSync } from 'node:child_process'; import { tmpdir } from 'node:os'; @@ -7,13 +8,13 @@ import { GitCommitAdapter } from '../git_commit_adapter'; import { BranchSyncWorkspaceAdapter } from '../branch_sync_workspace_adapter'; import { SyncBranchUseCase } from '../../application/usecases/branch_sync/sync_branch_use_case'; import { projectBranchSyncContext } from '../../application/usecases/push_single_action_contexts'; +import { prepareUntrustedCommandEnvironment } from '../untrusted_command_environment'; +import { readNativeRecord, writeNativeRecord, type NativeVerificationRequest } from '../native_verification_protocol'; +import { VERIFICATION_SETUP_PATH } from '../../application/policies/verification_setup_policy'; import type { Execution } from '../../data/model/execution'; const branch = 'feature/42-managed-fixture'; -const helper = `import fs from 'node:fs'; import path from 'node:path'; - const bin=path.join(process.env.HOME,'tools'); fs.mkdirSync(bin); - fs.writeFileSync(path.join(bin,'prepared'),fs.readFileSync('dependencies.lock')); - fs.writeFileSync(process.env.COPILOT_VERIFICATION_SETUP_RESULT,JSON.stringify({schemaVersion:1,path:[bin]}));`; +const helper = 'runs:\n using: composite\n steps:\n - run: echo reviewed\n shell: bash\n'; const verifier = `import fs from 'node:fs'; import path from 'node:path'; import assert from 'node:assert/strict'; assert.equal(fs.readFileSync(path.join(process.env.HOME,'tools','prepared'),'utf8'),fs.readFileSync('dependencies.lock','utf8'));`; @@ -29,13 +30,13 @@ describe('repository setup during same-managed-branch synchronization', () => { const emptyHooks = path.join(directory, 'empty-hooks'); fs.mkdirSync(emptyHooks); git('config', 'core.hooksPath', emptyHooks); git('--git-dir', remote, 'config', 'core.hooksPath', emptyHooks); git('config', 'user.name', 'Test'); git('config', 'user.email', 'test@example.test'); - fs.mkdirSync(path.join(root, '.copilot')); - fs.writeFileSync(path.join(root, '.copilot/verification-setup.mjs'), helper); + fs.mkdirSync(path.dirname(path.join(root, VERIFICATION_SETUP_PATH)), { recursive: true }); + fs.writeFileSync(path.join(root, VERIFICATION_SETUP_PATH), helper); fs.writeFileSync(path.join(root, 'verify.mjs'), verifier); fs.writeFileSync(path.join(root, 'dependencies.lock'), 'version-one'); git('add', '.'); git('commit', '-m', 'trusted main'); git('remote', 'add', 'origin', remote); git('push', 'origin', 'main'); git('checkout', '-b', 'develop'); git('push', 'origin', 'develop'); git('checkout', '-b', branch); - fs.writeFileSync(path.join(root, '.copilot/verification-setup.mjs'), 'throw new Error("candidate helper must never execute");'); + fs.writeFileSync(path.join(root, VERIFICATION_SETUP_PATH), 'throw new Error("candidate helper must never execute");'); git('add', '.'); git('commit', '-m', 'candidate helper change'); git('push', 'origin', branch); childSha = git('rev-parse', 'HEAD'); git('checkout', 'develop'); fs.writeFileSync(path.join(root, 'dependencies.lock'), 'version-two'); git('add', '.'); git('commit', '-m', 'parent dependency update'); git('push', 'origin', 'develop'); git('checkout', branch); @@ -63,7 +64,28 @@ describe('repository setup during same-managed-branch synchronization', () => { pullRequest: { number: -1 }, issueNumber: 42, ai: { getAgentConfiguration: () => ({ provider: 'codex', model: '' }), getBugbotFixVerifyCommands: () => commands }, } as unknown as Execution); - const [result] = await useCase.invoke({ context, options: { dryRun, useAgent: false } }); + const runtime = prepareUntrustedCommandEnvironment(); + const session = runtime.environment.HOME; + const previous = process.env.COPILOT_NATIVE_WORKER_SESSION; + process.env.COPILOT_NATIVE_WORKER_SESSION = session; + writeNativeRecord(session, 'session.json', { schemaVersion: 1, workspace: root, run: process.env.GITHUB_RUN_ID ?? '', + attempt: process.env.GITHUB_RUN_ATTEMPT ?? '', job: process.env.GITHUB_JOB ?? '', pid: process.pid }); + const answered = new Set(); + // Test double for native Actions; it reads merged manifests, never parses or executes YAML. + const driver = setInterval(() => { + const request = readNativeRecord(session, 'request.json', 32_768); + if (!request || answered.has(request.id)) return; + answered.add(request.id); + const tools = path.join(request.environment.HOME, 'tools'); fs.mkdirSync(tools); + fs.writeFileSync(path.join(tools, 'prepared'), fs.readFileSync(path.join(root, 'dependencies.lock'))); + writeNativeRecord(session, `response-${request.id}.json`, { id: request.id, success: true, path: request.environment.PATH }); + }, 5); + let result; + try { [result] = await useCase.invoke({ context, options: { dryRun, useAgent: false } }); } + finally { + clearInterval(driver); removeNativeVerificationProfiles(session); runtime.cleanup(); + if (previous === undefined) delete process.env.COPILOT_NATIVE_WORKER_SESSION; else process.env.COPILOT_NATIVE_WORKER_SESSION = previous; + } expect(fixer.fix).not.toHaveBeenCalled(); return result; } @@ -77,7 +99,7 @@ describe('repository setup during same-managed-branch synchronization', () => { expect(result).toMatchObject({ success: true, payload: { outcome: 'merged-cleanly', verificationCount: 2 } }); expect(remoteHead()).toBe(git('rev-parse', 'HEAD')); expect(remoteHead()).not.toBe(childSha); expect(git('show', `${remoteHead()}:dependencies.lock`)).toBe('version-two'); - expect(git('show', `${remoteHead()}:.copilot/verification-setup.mjs`)).toContain('candidate helper must never execute'); + expect(git('show', `${remoteHead()}:${VERIFICATION_SETUP_PATH}`)).toContain('candidate helper must never execute'); expect(git('--git-dir', remote, 'for-each-ref', '--format=%(refname)', 'refs/heads').split('\n').sort()) .toEqual(['refs/heads/develop', `refs/heads/${branch}`, 'refs/heads/main']); }); diff --git a/src/infrastructure/git_commit_adapter.ts b/src/infrastructure/git_commit_adapter.ts index 1e9ed8f7b..349c51a38 100644 --- a/src/infrastructure/git_commit_adapter.ts +++ b/src/infrastructure/git_commit_adapter.ts @@ -1,3 +1,4 @@ +import { nativeVerificationRetainsProfile } from './native_verification_profiles'; import * as exec from '@actions/exec'; import type { GitCommitPort } from '../application/ports/git_ports'; import { buildGitAuthenticationEnvironment } from './git_authentication_environment'; @@ -40,7 +41,7 @@ export class GitCommitAdapter implements GitCommitPort { env: runtime.environment, }); } finally { - runtime.cleanup(); + if (!nativeVerificationRetainsProfile(runtime.environment.HOME)) runtime.cleanup(); } } diff --git a/src/infrastructure/native_verification_candidate.ts b/src/infrastructure/native_verification_candidate.ts new file mode 100644 index 000000000..1c60e851e --- /dev/null +++ b/src/infrastructure/native_verification_candidate.ts @@ -0,0 +1,36 @@ +import { spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { lstatSync, readFileSync, readlinkSync } from 'node:fs'; +import { join } from 'node:path'; +import { VerificationSetupError } from '../application/policies/verification_setup_policy'; + +/** Bind setup to the exact HEAD, branch, index, tracked edits and publishable untracked files. */ +export function verificationCandidateIdentity(workspace: string, environment: Record): string { + const hash = createHash('sha256'); + const git = (...args: string[]): Buffer => { + const result = spawnSync('git', ['--no-replace-objects', ...args], { cwd: workspace, env: environment, shell: false, + windowsHide: true, timeout: 10_000, maxBuffer: 16 * 1024 * 1024 }); + if (result.error || result.status !== 0) throw new VerificationSetupError('candidate-changed'); + hash.update(result.stdout).update('\0'); + return result.stdout; + }; + git('rev-parse', '--verify', 'HEAD'); + git('rev-parse', '--symbolic-full-name', 'HEAD'); + git('ls-files', '--stage', '-z'); + git('diff', '--no-ext-diff', '--no-textconv', '--binary', 'HEAD', '--'); + const paths = git('ls-files', '--others', '--exclude-standard', '-z').toString('utf8').split('\0').filter(Boolean); + let total = 0; + if (paths.length > 256) throw new VerificationSetupError('candidate-changed'); + for (const path of paths) { + const file = join(workspace, path); + const stat = lstatSync(file); + total += stat.size; + if (total > 16 * 1024 * 1024) throw new VerificationSetupError('candidate-changed'); + hash.update(path).update('\0'); + if (stat.isSymbolicLink()) hash.update(readlinkSync(file)); + else if (stat.isFile()) hash.update(readFileSync(file)); + else throw new VerificationSetupError('candidate-changed'); + hash.update('\0'); + } + return hash.digest('hex'); +} diff --git a/src/infrastructure/native_verification_controller.ts b/src/infrastructure/native_verification_controller.ts new file mode 100644 index 000000000..bf028f6ae --- /dev/null +++ b/src/infrastructure/native_verification_controller.ts @@ -0,0 +1,185 @@ +import { nativeWorkerSecrets, redactNativeWorkerLogs } from './native_verification_redaction'; +import { removeNativeVerificationProfiles } from './native_verification_profiles'; +import * as core from '@actions/core'; +import { spawn, spawnSync } from 'node:child_process'; +import { appendFileSync, closeSync, existsSync, openSync, readSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { createPrivateVerificationRuntimeRoot } from './agents/agent_private_install_root'; +import { trustedWindowsSystemTool } from './agents/agent_trusted_system_tools'; +import { getGithubActionInput } from './github_action_input'; +import { VerificationSetupError, VERIFICATION_SETUP_LIMITS } from '../application/policies/verification_setup_policy'; +import { nativePause, nativeSession, readNativeRecord, writeNativeRecord, type NativeVerificationRequest, type NativeVerificationResponse } from './native_verification_protocol'; +import { restoreNativeVerificationAction } from './native_verification_overlay'; + +function emitWorkerOutput(root: string): void { + const file = join(root, 'worker-output'); + if (process.env.GITHUB_OUTPUT && existsSync(file)) appendFileSync(process.env.GITHUB_OUTPUT, requireRead(file)); + const summary = join(root, 'worker-summary'); + if (process.env.GITHUB_STEP_SUMMARY && existsSync(summary) && !readNativeRecord(root, 'summary-emitted.json')) { + appendFileSync(process.env.GITHUB_STEP_SUMMARY, requireRead(summary, 1_048_576)); + writeNativeRecord(root, 'summary-emitted.json', { emitted: true }); + } +} + +function requireRead(file: string, limit = 262_144): Buffer { + const fd = openSync(file, 'r'); + try { const size = statSync(file).size; if (size > limit) throw new VerificationSetupError('output-limit'); const data = Buffer.alloc(size); readSync(fd, data, 0, data.length, 0); return data; } + finally { closeSync(fd); } +} + +/** Drain complete lines so runner masking and workflow commands keep their native framing. */ +export function drainNativeWorkerLog(root: string, flush = false): void { + const file = join(root, 'worker-log'); + if (!existsSync(file)) return; + const cursor = readNativeRecord<{offset: number}>(root, 'log-cursor.json')?.offset ?? 0; + const fd = openSync(file, 'r'); + let offset = cursor; + try { + const buffer = Buffer.alloc(65_536); + let pending = Buffer.alloc(0); + let size: number; + while ((size = readSync(fd, buffer, 0, buffer.length, offset + pending.length))) { + pending = Buffer.concat([pending, buffer.subarray(0, size)]); + const lastLine = pending.lastIndexOf(10); + if (lastLine >= 0) { + const complete = pending.subarray(0, lastLine + 1); + process.stdout.write(complete); + offset += complete.length; + pending = pending.subarray(lastLine + 1); + } + // Existing action logs may contain long lines; retain framing without unbounded buffering. + if (pending.length > 1_048_576) throw new VerificationSetupError('output-limit'); + } + if (flush && pending.length) { process.stdout.write(pending); offset += pending.length; } + writeNativeRecord(root, 'log-cursor.json', { offset }); + } finally { closeSync(fd); } +} + +export async function awaitNativeBoundary(root: string, timeoutMs = 7_200_000): Promise { + const deadline = Date.now() + timeoutMs; + while (true) { + drainNativeWorkerLog(root); + const done = readNativeRecord<{exitCode: number}>(root, 'done.json'); + if (done) { + drainNativeWorkerLog(root, true); + emitWorkerOutput(root); + core.setOutput('verification-setup-pending', 'false'); + if (done.exitCode !== 0) core.setFailed('Copilot execution failed; inspect the preceding operation result.'); + return; + } + const request = readNativeRecord(root, 'request.json', 32_768); + if (request) { + if (!/^[a-f0-9-]{36}$/u.test(request.id) || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(request.sourceSha) + || !request.environment || typeof request.environment.HOME !== 'string') throw new VerificationSetupError('handoff-invalid'); + core.setOutput('verification-request', request.id); + for (const key of ['HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'PATH', 'JAVA_HOME', 'TMP', 'TEMP', 'TMPDIR']) { + const value = request.environment[key] ?? ''; + if (typeof value !== 'string' || /[\r\n\0]/u.test(value)) throw new VerificationSetupError('handoff-invalid'); + core.setOutput(`verification-${key.toLowerCase()}`, value); + } + core.setOutput('verification-setup-pending', 'true'); + return; + } + if (Date.now() >= deadline) throw new VerificationSetupError('timeout'); + const session = nativeSession(root); + try { process.kill(session.pid, 0); } catch { throw new VerificationSetupError('process-failed'); } + await nativePause(100); + } +} + +export async function continueNativeVerification(root: string, id: string, outcome: string, environment = process.env): Promise { + nativeSession(root); + const request = readNativeRecord(root, 'request.json', 32_768); + if (!request || request.id !== id || !/^[a-f0-9-]{36}$/u.test(id) + || existsSync(join(root, `response-${id}.json`))) throw new VerificationSetupError('handoff-invalid'); + const response: NativeVerificationResponse = { id, success: outcome === 'success', path: environment.PATH ?? '', + ...(environment.JAVA_HOME ? { javaHome: environment.JAVA_HOME } : {}) }; + rmSync(join(root, 'request.json')); + writeNativeRecord(root, `response-${id}.json`, response); + await awaitNativeBoundary(root); +} + +export async function cleanupNativeVerification(root: string, waitMs = 10_000, deferProfileCleanup = false): Promise { + if (!existsSync(root)) return; + const session = nativeSession(root); + if (!readNativeRecord(root, 'done.json')) { + writeNativeRecord(root, 'cancel.json', { cancelled: true }); + const deadline = Date.now() + waitMs; + while (!readNativeRecord(root, 'done.json') && Date.now() < deadline) await nativePause(100); + if (!readNativeRecord(root, 'done.json')) { + try { + if (process.platform === 'win32') spawnSync(trustedWindowsSystemTool('taskkill.exe'), ['/PID', String(session.pid), '/T', '/F'], { windowsHide: true, shell: false, timeout: 5_000, stdio: 'ignore' }); + else process.kill(-session.pid, 'SIGKILL'); + } catch { /* Worker may have exited while cancellation completed. */ } + } + } + restoreNativeVerificationAction(root, session.workspace); + drainNativeWorkerLog(root, true); + if (!deferProfileCleanup) { removeNativeVerificationProfiles(root); rmSync(root, { recursive: true, force: true }); } +} + +async function startNativeVerification(): Promise { + for (const secret of nativeWorkerSecrets(process.env)) core.setSecret(secret); + const root = realpathSync(createPrivateVerificationRuntimeRoot()); + for (const name of ['worker-output', 'worker-env', 'worker-path', 'worker-log', 'worker-summary']) writeFileSync(join(root, name), '', { flag: 'wx', mode: 0o600 }); + const log = openSync(join(root, 'worker-log'), 'a'); + try { + const worker = spawn(process.execPath, [process.argv[1]], { cwd: process.cwd(), detached: true, windowsHide: true, + stdio: ['ignore', log, log], env: { ...process.env, COPILOT_NATIVE_PHASE: '', COPILOT_NATIVE_WORKER_SESSION: root, + GITHUB_STEP_SUMMARY: join(root, 'worker-summary'), GITHUB_OUTPUT: join(root, 'worker-output'), GITHUB_ENV: join(root, 'worker-env'), GITHUB_PATH: join(root, 'worker-path') } }); + await new Promise((resolve, reject) => { worker.once('spawn', resolve); worker.once('error', reject); }); + worker.unref(); + writeNativeRecord(root, 'session.json', { schemaVersion: 1, workspace: realpathSync(process.cwd()), run: process.env.GITHUB_RUN_ID ?? '', + attempt: process.env.GITHUB_RUN_ATTEMPT ?? '', job: process.env.GITHUB_JOB ?? '', pid: worker.pid }); + core.saveState('native-verification-ticket', root); + core.setOutput('verification-ticket', root); + await awaitNativeBoundary(root); + } catch (error) { + if (existsSync(join(root, 'session.json'))) await cleanupNativeVerification(root); + else rmSync(root, { recursive: true, force: true }); + throw error; + } finally { closeSync(log); } +} + +/** Native phases are runner-only control messages; no credentials or agent operations are replayed. */ +export async function runNativeVerificationEntry(run: () => Promise): Promise { + const post = core.getState('copilot-native-post'); + if (post === 'true') { + const root = core.getState('native-verification-ticket'); + if (root) await cleanupNativeVerification(root); + return; + } + core.saveState('copilot-native-post', 'true'); + const worker = process.env.COPILOT_NATIVE_WORKER_SESSION; + if (worker) { + const deadline = Date.now() + 30_000; + while (!readNativeRecord(worker, 'session.json')) { + if (Date.now() >= deadline) throw new VerificationSetupError('timeout'); + await nativePause(10); + } + if (nativeSession(worker).pid !== process.pid) throw new VerificationSetupError('handoff-invalid'); + const restoreLog = redactNativeWorkerLogs(); + try { await run(); } + finally { restoreLog(); writeNativeRecord(worker, 'done.json', { exitCode: Number(process.exitCode ?? 0) }); } + return; + } + const phase = process.env.COPILOT_NATIVE_PHASE; + if (!phase || phase === 'prepare' && getGithubActionInput('verification-setup') !== 'true') { await run(); return; } + if (process.env.GITHUB_ACTIONS !== 'true') throw new VerificationSetupError('native-workflow-required'); + if (phase === 'prepare') { await startNativeVerification(); return; } + const root = process.env.COPILOT_NATIVE_TICKET; + if (!root) throw new VerificationSetupError('handoff-invalid'); + if (phase === 'continue') { + await continueNativeVerification(root, process.env.COPILOT_NATIVE_REQUEST ?? '', process.env.COPILOT_NATIVE_OUTCOME ?? ''); + return; + } + if (phase === 'finish') { + nativeSession(root); + const done = readNativeRecord<{exitCode: number}>(root, 'done.json'); + if (done) { emitWorkerOutput(root); if (done.exitCode !== 0) core.setFailed('Copilot execution failed.'); } + else core.setFailed(`Repository setup did not complete within ${VERIFICATION_SETUP_LIMITS.requests} native verification boundaries. Pending verification and subsequent publication are blocked; inspect earlier operation results for completed effects.`); + await cleanupNativeVerification(root, 10_000, true); + return; + } + throw new VerificationSetupError('handoff-invalid'); +} diff --git a/src/infrastructure/native_verification_overlay.ts b/src/infrastructure/native_verification_overlay.ts new file mode 100644 index 000000000..d9921d5a1 --- /dev/null +++ b/src/infrastructure/native_verification_overlay.ts @@ -0,0 +1,51 @@ +import { existsSync, lstatSync, mkdirSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from 'node:fs'; +import { dirname, join, relative, sep } from 'node:path'; +import type { PreparedVerificationSetup } from '../application/ports/verification_setup_ports'; +import { VERIFICATION_SETUP_DIRECTORY, VerificationSetupError } from '../application/policies/verification_setup_policy'; +import { readNativeRecord, writeNativeRecord } from './native_verification_protocol'; + +interface OverlayRecord { readonly target: string; readonly backup: string; readonly existed: boolean; readonly created: string[] } + +/** A temporary reviewed action overlays the candidate path only while the operation is paused. */ +export function stageNativeVerificationAction(setup: Extract, session: string): void { + const target = join(setup.workspace, VERIFICATION_SETUP_DIRECTORY); + const backup = join(session, 'candidate-action'); + const created: string[] = []; + let parent = setup.workspace; + for (const part of VERIFICATION_SETUP_DIRECTORY.split('/').slice(0, -1)) { + parent = join(parent, part); + if (existsSync(parent)) { + if (lstatSync(parent).isSymbolicLink() || !lstatSync(parent).isDirectory() || realpathSync(parent) !== parent) throw new VerificationSetupError('helper-invalid'); + } else { mkdirSync(parent, { mode: 0o700 }); created.push(parent); } + } + const targetStat = lstatSync(target, { throwIfNoEntry: false }); + const existed = !!targetStat; + if (targetStat && (targetStat.isSymbolicLink() || !targetStat.isDirectory())) throw new VerificationSetupError('helper-invalid'); + const record: OverlayRecord = { target, backup, existed, created }; + writeNativeRecord(session, 'overlay.json', record); + if (existed) renameSync(target, backup); + mkdirSync(target, { mode: 0o700 }); + for (const file of setup.files) { + const destination = join(target, file.path); + const remainder = relative(target, destination); + if (!remainder || remainder.startsWith(`..${sep}`) || remainder === '..') throw new VerificationSetupError('helper-invalid'); + mkdirSync(dirname(destination), { recursive: true, mode: 0o700 }); + writeFileSync(destination, Buffer.from(file.base64, 'base64'), { mode: file.executable ? 0o700 : 0o600, flag: 'wx' }); + } +} + +export function restoreNativeVerificationAction(session: string, workspace: string): void { + const record = readNativeRecord(session, 'overlay.json'); + if (!record) return; + if (record.target !== join(workspace, VERIFICATION_SETUP_DIRECTORY) || record.backup !== join(session, 'candidate-action') + || !Array.isArray(record.created) || record.created.some(path => ![join(workspace, '.github'), join(workspace, '.github/actions')].includes(path))) throw new VerificationSetupError('handoff-invalid'); + for (const parent of [join(workspace, '.github'), join(workspace, '.github/actions')]) { + if (!existsSync(parent) || lstatSync(parent).isSymbolicLink() || !lstatSync(parent).isDirectory() || realpathSync(parent) !== parent) throw new VerificationSetupError('handoff-invalid'); + } + // A backup is the authoritative original. A second cleanup must never remove it. + if (record.existed && !existsSync(record.backup)) return; + rmSync(record.target, { recursive: true, force: true }); + if (record.existed) renameSync(record.backup, record.target); + for (const directory of [...record.created].reverse()) { try { rmdirSync(directory); } catch { /* Other candidate content remains. */ } } + rmSync(join(session, 'overlay.json'), { force: true }); +} diff --git a/src/infrastructure/native_verification_profiles.ts b/src/infrastructure/native_verification_profiles.ts new file mode 100644 index 000000000..9f2f616b8 --- /dev/null +++ b/src/infrastructure/native_verification_profiles.ts @@ -0,0 +1,36 @@ +import { lstatSync, realpathSync, rmSync, writeFileSync, readFileSync } from 'node:fs'; +import { basename, dirname, join } from 'node:path'; +import { VerificationSetupError } from '../application/policies/verification_setup_policy'; +import { readNativeRecord, writeNativeRecord } from './native_verification_protocol'; + +export function nativeVerificationProfilePaths(root: string): string[] { + const stored = readNativeRecord(root, 'profiles.json', 65_536); + const paths = stored === undefined ? [] : stored; + if (!Array.isArray(paths) || paths.length > 20 || new Set(paths).size !== paths.length || paths.some(path => typeof path !== 'string' + || dirname(path) !== dirname(root) || !/^copilot-verify-runtime-[A-Za-z0-9]+$/u.test(basename(path)) || path === root)) throw new VerificationSetupError('handoff-invalid'); + return paths; +} + +/** Native actions may have post steps. Preserve dependency-only profiles until those steps finish. */ +export function registerNativeVerificationProfile(root: string, profile: string): void { + const previous = nativeVerificationProfilePaths(root); + if (previous.length >= 20 || previous.includes(profile) || dirname(profile) !== dirname(root) + || profile === root || !/^copilot-verify-runtime-[A-Za-z0-9]+$/u.test(basename(profile)) + || realpathSync(profile) !== profile || !lstatSync(profile).isDirectory() || lstatSync(profile).isSymbolicLink()) throw new VerificationSetupError('handoff-invalid'); + writeFileSync(join(profile, 'native-session-owner'), root, { mode: 0o600, flag: 'wx' }); + writeNativeRecord(root, 'profiles.json', [...previous, profile]); +} + +export function nativeVerificationRetainsProfile(profile: string): boolean { + const root = process.env.COPILOT_NATIVE_WORKER_SESSION; + return !!root && nativeVerificationProfilePaths(root).includes(profile); +} + +export function removeNativeVerificationProfiles(root: string): void { + for (const path of nativeVerificationProfilePaths(root)) { + let stat; + try { stat = lstatSync(path); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue; throw error; } + if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync(path) !== path || readFileSync(join(path, 'native-session-owner'), 'utf8') !== root) throw new VerificationSetupError('handoff-invalid'); + rmSync(path, { recursive: true, force: true }); + } +} diff --git a/src/infrastructure/native_verification_protocol.ts b/src/infrastructure/native_verification_protocol.ts new file mode 100644 index 000000000..45718b84c --- /dev/null +++ b/src/infrastructure/native_verification_protocol.ts @@ -0,0 +1,62 @@ +import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs'; +import { basename, join } from 'node:path'; +import { randomUUID } from 'node:crypto'; +import { VerificationSetupError } from '../application/policies/verification_setup_policy'; + +export interface NativeVerificationSession { + readonly schemaVersion: 1; + readonly workspace: string; + readonly run: string; + readonly attempt: string; + readonly job: string; + readonly pid: number; +} +export interface NativeVerificationRequest { + readonly id: string; + readonly sourceSha: string; + readonly environment: Record; +} +export interface NativeVerificationResponse { + readonly id: string; + readonly success: boolean; + readonly path: string; + readonly javaHome?: string; +} + +export function writeNativeRecord(root: string, name: string, value: unknown): void { + const temporary = join(root, `${randomUUID()}.tmp`); + writeFileSync(temporary, JSON.stringify(value), { mode: 0o600, flag: 'wx' }); + renameSync(temporary, join(root, name)); +} + +export function readNativeRecord(root: string, name: string, limit = 16_384): T | undefined { + const file = join(root, name); + let descriptor: number | undefined; + try { + const before = lstatSync(file, { bigint: true }); + if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.size > BigInt(limit)) throw new Error(); + descriptor = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)); + const after = fstatSync(descriptor, { bigint: true }); + if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.mtimeNs !== before.mtimeNs) throw new Error(); + const data = readFileSync(descriptor); + const final = fstatSync(descriptor, { bigint: true }); + if (data.length > limit || final.size !== after.size || final.mtimeNs !== after.mtimeNs) throw new Error(); + return JSON.parse(data.toString('utf8')) as T; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined; + throw new VerificationSetupError('handoff-invalid'); + } finally { if (descriptor !== undefined) closeSync(descriptor); } +} + +export function nativeSession(root: string, environment = process.env): NativeVerificationSession { + if (!/^copilot-verify-runtime-[A-Za-z0-9]+$/u.test(basename(root)) || realpathSync(root) !== root + || !lstatSync(root).isDirectory() || lstatSync(root).isSymbolicLink()) throw new VerificationSetupError('handoff-invalid'); + if (process.platform !== 'win32' && (lstatSync(root).mode & 0o077) !== 0) throw new VerificationSetupError('handoff-invalid'); + const session = readNativeRecord(root, 'session.json'); + if (session?.schemaVersion !== 1 || session.workspace !== realpathSync(process.cwd()) + || session.run !== (environment.GITHUB_RUN_ID ?? '') || session.attempt !== (environment.GITHUB_RUN_ATTEMPT ?? '') + || session.job !== (environment.GITHUB_JOB ?? '') || !Number.isSafeInteger(session.pid) || session.pid <= 0) throw new VerificationSetupError('handoff-invalid'); + return session; +} + +export const nativePause = (milliseconds: number): Promise => new Promise(resolve => setTimeout(resolve, milliseconds)); diff --git a/src/infrastructure/native_verification_redaction.ts b/src/infrastructure/native_verification_redaction.ts new file mode 100644 index 000000000..b1446d352 --- /dev/null +++ b/src/infrastructure/native_verification_redaction.ts @@ -0,0 +1,45 @@ +import { StringDecoder } from 'node:string_decoder'; + +const SECRET_KEY = /(?:TOKEN|SECRET|API_KEY|PASSWORD|AUTH_JSON)/iu; + +export function nativeWorkerSecrets(environment: NodeJS.ProcessEnv): string[] { + const values = Object.entries(environment).filter(([key, value]) => SECRET_KEY.test(key) && value).map(([,value]) => value!); + if (environment.INPUT_VARS_JSON) { + try { + const inputs = JSON.parse(environment.INPUT_VARS_JSON) as Record; + for (const [key, value] of Object.entries(inputs)) if (SECRET_KEY.test(key) && typeof value === 'string') values.push(value); + } catch { /* The existing input parser reports invalid configuration without raw values. */ } + } + return [...new Set(values.flatMap(value => [value, ...value.split(/[\r\n]/u)]).filter(value => value.length >= 3))].sort((a,b) => b.length - a.length); +} + +/** Secrets are removed before bytes reach a private log file, not merely masked in the runner UI. */ +export function redactNativeWorkerLogs(environment: NodeJS.ProcessEnv = process.env): () => void { + const secrets = nativeWorkerSecrets(environment); + const restores: (() => void)[] = []; + for (const stream of [process.stdout, process.stderr]) { + const original = stream.write; + const decoder = new StringDecoder('utf8'); + let pending = ''; + const emit = (line: string): void => { + const command = /^::add-mask::(.*?)(?:\r?\n)?$/u.exec(line); + if (command) { + const value = command[1].replace(/%0D/gu, '\r').replace(/%0A/gu, '\n').replace(/%25/gu, '%'); + secrets.push(...value.split(/[\r\n]/u).filter(part => part.length >= 3)); + return; + } + for (const secret of secrets) line = line.split(secret).join('[REDACTED]'); + original.call(stream, line); + }; + stream.write = ((chunk: string | Uint8Array, encoding?: BufferEncoding | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean => { + pending += typeof chunk === 'string' ? chunk : decoder.write(Buffer.from(chunk)); + let end: number; + while ((end = pending.indexOf('\n')) >= 0) { const line = pending.slice(0, end + 1); pending = pending.slice(end + 1); emit(line); } + if (pending.length > 1_048_576) throw new Error('Copilot worker log line exceeded its fixed limit.'); + if (typeof encoding === 'function') encoding(); else callback?.(); + return true; + }) as typeof stream.write; + restores.push(() => { pending += decoder.end(); if (pending) emit(pending); stream.write = original; }); + } + return () => { for (const restore of restores) restore(); }; +} diff --git a/src/infrastructure/verification_setup_process.ts b/src/infrastructure/verification_setup_process.ts deleted file mode 100644 index a1590a25b..000000000 --- a/src/infrastructure/verification_setup_process.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { spawn, spawnSync } from 'node:child_process'; -import { trustedWindowsSystemTool } from './agents/agent_trusted_system_tools'; -import { VerificationSetupError, VERIFICATION_SETUP_LIMITS } from '../application/policies/verification_setup_policy'; - -/** All output is counted and discarded. Fixed production limits; short injected limits are for deterministic tests. */ -export function runVerificationSetupProcess(node: string, script: string, cwd: string, env: Record, sourceSha: string, - limits: { timeoutMs: number; outputBytes: number } = VERIFICATION_SETUP_LIMITS): Promise { - return new Promise((resolve, reject) => { - const child = spawn(node, [script], { cwd, env, shell: false, windowsHide: true, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] }); - let failure: 'timeout' | 'cancelled' | 'output-limit' | 'process-failed' | undefined; - let outputBytes = 0; - const terminate = (): void => { - if (!child.pid) return; - try { - if (process.platform === 'win32') spawnSync(trustedWindowsSystemTool('taskkill.exe'), ['/PID', String(child.pid), '/T', '/F'], { env, windowsHide: true, timeout: 5_000, stdio: 'ignore', shell: false }); - else process.kill(-child.pid, 'SIGKILL'); - } catch { child.kill('SIGKILL'); } - }; - const timer = setTimeout(() => { failure ??= 'timeout'; terminate(); }, limits.timeoutMs); - const cancel = (): void => { failure ??= 'cancelled'; terminate(); }; - process.once('SIGINT', cancel); - process.once('SIGTERM', cancel); - const capture = (data: Buffer): void => { - outputBytes += data.length; - if (outputBytes > limits.outputBytes) { failure ??= 'output-limit'; terminate(); } - }; - child.stdout.on('data', capture); - child.stderr.on('data', capture); - child.on('error', () => { failure ??= 'process-failed'; }); - child.on('close', code => { - clearTimeout(timer); - process.removeListener('SIGINT', cancel); - process.removeListener('SIGTERM', cancel); - terminate(); - if (failure || code !== 0) reject(new VerificationSetupError(failure ?? 'process-failed', sourceSha, code ?? undefined)); - else resolve(); - }); - }); -} diff --git a/src/infrastructure/verification_setup_runner.ts b/src/infrastructure/verification_setup_runner.ts index ae9d918f6..cf7112832 100644 --- a/src/infrastructure/verification_setup_runner.ts +++ b/src/infrastructure/verification_setup_runner.ts @@ -1,32 +1,61 @@ -import { realpathSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; +import { registerNativeVerificationProfile, nativeVerificationProfilePaths } from './native_verification_profiles'; +import { realpathSync, rmSync, writeFileSync } from 'node:fs'; +import { delimiter, join, relative, isAbsolute, sep } from 'node:path'; +import { randomUUID } from 'node:crypto'; import type { PreparedVerificationSetup } from '../application/ports/verification_setup_ports'; -import { VerificationSetupError } from '../application/policies/verification_setup_policy'; +import { VerificationSetupError, VERIFICATION_SETUP_LIMITS, VERIFICATION_SETUP_PATH } from '../application/policies/verification_setup_policy'; import { applyVerificationSetupResult } from './verification_setup_result'; -import { runVerificationSetupProcess } from './verification_setup_process'; import { logInfo } from '../application/ports/logging_ports'; -import { VERIFICATION_SETUP_PATH } from '../application/policies/verification_setup_policy'; +import { nativePause, nativeSession, readNativeRecord, writeNativeRecord, type NativeVerificationResponse } from './native_verification_protocol'; +import { stageNativeVerificationAction, restoreNativeVerificationAction } from './native_verification_overlay'; +import { verificationCandidateIdentity } from './native_verification_candidate'; -/** Called once per verification command, on its fresh private profile, before runtime probes. */ + +/** The runner executes YAML. This adapter pauses the admitted call stack at its verification boundary. */ export async function prepareRepositoryVerification(setup: PreparedVerificationSetup, environment: Record): Promise { if (setup.kind === 'disabled') return; if (setup.kind === 'blocked') throw new VerificationSetupError(setup.reason, setup.sourceSha); + const session = process.env.COPILOT_NATIVE_WORKER_SESSION; + if (!session) throw new VerificationSetupError('native-workflow-required', setup.sourceSha); try { - if (realpathSync(process.cwd()) !== setup.workspace) throw new VerificationSetupError('workspace-changed', setup.sourceSha); - const profile = realpathSync(environment.HOME); - const script = join(profile, 'verification-setup.mjs'); - const result = join(profile, 'verification-setup-result.json'); - writeFileSync(script, setup.script, { mode: 0o600, flag: 'wx' }); - await runVerificationSetupProcess(setup.node, script, setup.workspace, { - ...environment, - COPILOT_VERIFICATION_SETUP_PROTOCOL: '1', - COPILOT_VERIFICATION_SETUP_RESULT: result, - COPILOT_VERIFICATION_SETUP_SOURCE_SHA: setup.sourceSha, - }, setup.sourceSha); - applyVerificationSetupResult(result, environment, [setup.workspace, profile, ...(setup.toolRoot ? [setup.toolRoot] : [])]); - logInfo(`Repository setup prepared (${VERIFICATION_SETUP_PATH}; trusted source SHA: ${setup.sourceSha}).`); + nativeSession(session); + const requests = readNativeRecord<{count: number}>(session, 'request-count.json')?.count ?? 0; + if (requests >= VERIFICATION_SETUP_LIMITS.requests) throw new VerificationSetupError('handoff-invalid'); + writeNativeRecord(session, 'request-count.json', { count: requests + 1 }); + if (realpathSync(process.cwd()) !== setup.workspace) throw new VerificationSetupError('workspace-changed'); + registerNativeVerificationProfile(session, environment.HOME); + const identity = verificationCandidateIdentity(setup.workspace, environment); + stageNativeVerificationAction(setup, session); + const id = randomUUID(); + writeNativeRecord(session, 'request.json', { id, sourceSha: setup.sourceSha, environment }); + logInfo(`Preparing repository verification (${VERIFICATION_SETUP_PATH}; trusted source SHA: ${setup.sourceSha}).`); + const deadline = Date.now() + VERIFICATION_SETUP_LIMITS.timeoutMs; + let response: NativeVerificationResponse | undefined; + while (!(response = readNativeRecord(session, `response-${id}.json`))) { + if (readNativeRecord(session, 'cancel.json')) throw new VerificationSetupError('cancelled'); + if (Date.now() >= deadline) throw new VerificationSetupError('timeout'); + await nativePause(100); + } + restoreNativeVerificationAction(session, setup.workspace); + if (response.id !== id || response.success !== true) throw new VerificationSetupError('process-failed'); + if (verificationCandidateIdentity(setup.workspace, environment) !== identity) throw new VerificationSetupError('candidate-changed'); + if (typeof response.path !== 'string' || response.path.length > 16_384 || response.javaHome !== undefined && typeof response.javaHome !== 'string') throw new VerificationSetupError('result-invalid'); + const original = new Set((environment.PATH ?? '').split(delimiter)); + const previousProfiles = nativeVerificationProfilePaths(session).filter(profile => profile !== environment.HOME); + const fromPreviousProfile = (path: string): boolean => previousProfiles.some(profile => { + const part = relative(profile, path); + return part === '' || part !== '..' && !part.startsWith(`..${sep}`) && !isAbsolute(part); + }); + const additions = [...new Set(response.path.split(delimiter).filter(path => path && !original.has(path) && !fromPreviousProfile(path)))]; + const result = join(environment.HOME, 'native-setup-result.json'); + writeFileSync(result, JSON.stringify({ schemaVersion: 1, path: additions, + environment: response.javaHome && response.javaHome !== environment.JAVA_HOME ? { JAVA_HOME: response.javaHome } : {} }), { mode: 0o600, flag: 'wx' }); + applyVerificationSetupResult(result, environment, [setup.workspace, environment.HOME, ...(setup.toolRoot ? [setup.toolRoot] : [])]); + logInfo('Repository setup completed; runtime preflight and verification may proceed.'); } catch (error) { - throw new VerificationSetupError(error instanceof VerificationSetupError ? error.reason : 'process-failed', setup.sourceSha, - error instanceof VerificationSetupError ? error.exitCode : undefined); + throw new VerificationSetupError(error instanceof VerificationSetupError ? error.reason : 'handoff-invalid', setup.sourceSha); + } finally { + restoreNativeVerificationAction(session, setup.workspace); + rmSync(join(session, 'request.json'), { force: true }); } } diff --git a/src/infrastructure/verification_setup_snapshot.ts b/src/infrastructure/verification_setup_snapshot.ts index f28dcfc5a..cc7e85457 100644 --- a/src/infrastructure/verification_setup_snapshot.ts +++ b/src/infrastructure/verification_setup_snapshot.ts @@ -1,12 +1,13 @@ import { spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { realpathSync } from 'node:fs'; -import type { PreparedVerificationSetup } from '../application/ports/verification_setup_ports'; +import { load } from 'js-yaml'; +import type { PreparedVerificationSetup, VerificationSetupFile } from '../application/ports/verification_setup_ports'; import { DISABLED_VERIFICATION_SETUP } from '../application/ports/verification_setup_ports'; -import { VERIFICATION_SETUP_LIMITS, VERIFICATION_SETUP_PATH, VerificationSetupError } from '../application/policies/verification_setup_policy'; +import { VERIFICATION_SETUP_DIRECTORY, VERIFICATION_SETUP_LIMITS, VerificationSetupError } from '../application/policies/verification_setup_policy'; import { prepareUntrustedCommandEnvironment } from './untrusted_command_environment'; -/** No network, agent, worktree-file reads, or credentials. Acquisition errors are deferred until verification. */ +/** Only immutable Git objects are read. Errors remain deferred on non-verifying routes. */ export function captureVerificationSetup(enabled: boolean, mainBranch: string, root = process.cwd()): PreparedVerificationSetup { if (!enabled) return DISABLED_VERIFICATION_SETUP; let sourceSha: string | undefined; @@ -20,23 +21,38 @@ export function captureVerificationSetup(enabled: boolean, mainBranch: string, r const workspace = realpathSync(root); const git = (...args: string[]): Buffer => { const result = spawnSync('git', ['--no-replace-objects', ...args], { cwd: workspace, env: environment, - shell: false, windowsHide: true, timeout: 5_000, maxBuffer: VERIFICATION_SETUP_LIMITS.scriptBytes + 4096 }); + shell: false, windowsHide: true, timeout: 5_000, maxBuffer: VERIFICATION_SETUP_LIMITS.snapshotBytes + 4096 }); if (result.error || result.status !== 0) throw new Error(); return result.stdout; }; - const candidateSha = git('rev-parse', '--verify', `refs/remotes/origin/${mainBranch}^{commit}`).toString('utf8').trim(); - if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(candidateSha)) throw new Error(); - sourceSha = candidateSha; - const entry = git('ls-tree', '-z', sourceSha, '--', VERIFICATION_SETUP_PATH).toString('utf8'); - const match = /^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64})\t\.copilot\/verification-setup\.mjs\0$/u.exec(entry); - if (!match) throw new VerificationSetupError('helper-invalid'); - const bytes = git('cat-file', 'blob', match[2]); - const digest = createHash(match[2].length === 40 ? 'sha1' : 'sha256').update(`blob ${bytes.length}\0`).update(bytes).digest('hex'); - const script = bytes.toString('utf8'); - if (!bytes.length || bytes.length > VERIFICATION_SETUP_LIMITS.scriptBytes || digest !== match[2] - || !Buffer.from(script).equals(bytes) || script.includes('\0')) throw new VerificationSetupError('helper-invalid'); + const sha = git('rev-parse', '--verify', `refs/remotes/origin/${mainBranch}^{commit}`).toString('utf8').trim(); + if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(sha)) throw new Error(); + sourceSha = sha; + const entries = git('ls-tree', '-rz', sourceSha, '--', VERIFICATION_SETUP_DIRECTORY).toString('utf8').split('\0').filter(Boolean); + if (!entries.length || entries.length > VERIFICATION_SETUP_LIMITS.files) throw new VerificationSetupError('helper-invalid'); + let total = 0; + const files: VerificationSetupFile[] = entries.map(entry => { + const match = /^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64})\t(.+)$/u.exec(entry); + if (!match || !match[3].startsWith(`${VERIFICATION_SETUP_DIRECTORY}/`)) throw new VerificationSetupError('helper-invalid'); + const path = match[3].slice(VERIFICATION_SETUP_DIRECTORY.length + 1); + if (!/^[A-Za-z0-9_./-]{1,200}$/u.test(path) || path.split('/').some(part => !part || part === '.' || part === '..' || part.endsWith('.') || /^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])(?:\.|$)/iu.test(part))) throw new VerificationSetupError('helper-invalid'); + const bytes = git('cat-file', 'blob', match[2]); + total += bytes.length; + const digest = createHash(match[2].length === 40 ? 'sha1' : 'sha256').update(`blob ${bytes.length}\0`).update(bytes).digest('hex'); + if (bytes.length > VERIFICATION_SETUP_LIMITS.scriptBytes || total > VERIFICATION_SETUP_LIMITS.snapshotBytes || digest !== match[2]) throw new VerificationSetupError('helper-invalid'); + return Object.freeze({ path, base64: bytes.toString('base64'), executable: match[1] === '100755' }); + }); + const manifest = files.find(file => file.path === 'action.yml'); + if (!manifest) throw new VerificationSetupError('helper-invalid'); + const bytes = Buffer.from(manifest.base64, 'base64'); + const text = bytes.toString('utf8'); + if (!bytes.length || !bytes.equals(Buffer.from(text)) || text.includes('\0')) throw new VerificationSetupError('helper-invalid'); + let action: { runs?: { using?: unknown; steps?: unknown[] } } | null; + try { action = load(text) as typeof action; } catch { throw new VerificationSetupError('helper-invalid'); } + if (action?.runs?.using !== 'composite' || !Array.isArray(action.runs.steps) || !action.runs.steps.length || action.runs.steps.length > 32) throw new VerificationSetupError('helper-invalid'); + if (action.runs.steps.some(step => !step || typeof step !== 'object' || 'uses' in step && typeof step.uses === 'string' && step.uses.startsWith('./'))) throw new VerificationSetupError('helper-invalid'); const toolRoot = environment.RUNNER_TOOL_CACHE ? realpathSync(environment.RUNNER_TOOL_CACHE) : undefined; - snapshot = Object.freeze({ kind: 'ready', script, sourceSha, workspace, node: process.execPath, ...(toolRoot ? { toolRoot } : {}) }); + snapshot = Object.freeze({ kind: 'ready', files: Object.freeze(files), sourceSha, workspace, ...(toolRoot ? { toolRoot } : {}) }); } catch (error) { snapshot = Object.freeze({ kind: 'blocked', reason: error instanceof VerificationSetupError ? error.reason : 'source-unavailable', ...(sourceSha ? { sourceSha } : {}) }); } diff --git a/src/tooling/__tests__/fixtures/verification_native_fixture.ts b/src/tooling/__tests__/fixtures/verification_native_fixture.ts new file mode 100644 index 000000000..f55e4d339 --- /dev/null +++ b/src/tooling/__tests__/fixtures/verification_native_fixture.ts @@ -0,0 +1,77 @@ +/** Native CI acceptance: the real Actions runner executes the reviewed YAML between candidate and verifier. */ +import * as core from '@actions/core'; +import { execFileSync } from 'node:child_process'; +import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { runNativeVerificationEntry } from '../../../infrastructure/native_verification_controller'; +import { captureVerificationSetup } from '../../../infrastructure/verification_setup_snapshot'; +import { GitCommitAdapter } from '../../../infrastructure/git_commit_adapter'; +import { VERIFICATION_SETUP_DIRECTORY, VERIFICATION_SETUP_PATH } from '../../../application/policies/verification_setup_policy'; + +const yaml = `name: Native setup acceptance +runs: + using: composite + steps: + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + - uses: pnpm/action-setup@v5 + with: + version: 10.12.4 + dest: \${{ env.HOME }}/native-pnpm + standalone: false + - name: Install candidate test dependencies privately + run: | + python - <<'PYTHON' + import os, pathlib, subprocess, venv + home = pathlib.Path(os.environ['HOME']) / 'venv' + venv.create(home, with_pip=True) + tools = home / ('Scripts' if os.name == 'nt' else 'bin') + python = tools / ('python.exe' if os.name == 'nt' else 'python') + subprocess.check_call([str(python), '-m', 'pip', 'install', '--disable-pip-version-check', '-r', 'requirements-native-fixture.txt']) + with open(os.environ['GITHUB_PATH'], 'a') as paths: + print(tools, file=paths) + PYTHON + shell: bash + - run: python -c "from pathlib import Path; assert Path('.copilot-native-candidate').read_text() == 'after-agent'" + shell: bash +`; + +async function fixture(): Promise { + const git = (...args: string[]): string => execFileSync('git', args, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); + const inputGit = (input: string, ...args: string[]): string => execFileSync('git', args, { input, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); + const blob = inputGit(yaml, 'hash-object', '-w', '--stdin'); + let tree = inputGit(`100644 blob ${blob}\taction.yml\n`, 'mktree'); + for (const name of ['copilot-verification-setup', 'actions', '.github']) tree = inputGit(`040000 tree ${tree}\t${name}\n`, 'mktree'); + const commit = execFileSync('git', ['commit-tree', tree, '-m', 'Native setup fixture'], { encoding: 'utf8', env: { ...process.env, + GIT_AUTHOR_NAME: 'Fixture', GIT_AUTHOR_EMAIL: 'fixture@example.test', GIT_COMMITTER_NAME: 'Fixture', GIT_COMMITTER_EMAIL: 'fixture@example.test' } }).trim(); + git('update-ref', 'refs/remotes/origin/copilot-native-fixture', commit); + const setup = captureVerificationSetup(true, 'copilot-native-fixture'); + if (setup.kind !== 'ready') throw new Error('Native fixture snapshot was not captured'); + const candidateAction = join(process.cwd(), VERIFICATION_SETUP_DIRECTORY); + const marker = join(process.cwd(), '.copilot-native-candidate'); + const testFile = join(process.cwd(), 'test_copilot_native_candidate.py'); + const requirements = join(process.cwd(), 'requirements-native-fixture.txt'); + const candidateYaml = 'runs:\n using: composite\n steps:\n - run: exit 99\n shell: bash\n'; + mkdirSync(candidateAction, { recursive: true }); + writeFileSync(join(candidateAction, 'action.yml'), candidateYaml); + writeFileSync(marker, 'after-agent'); + writeFileSync(requirements, 'pytest==8.4.2\n'); + writeFileSync(testFile, 'from pathlib import Path\ndef test_candidate():\n assert Path(".copilot-native-candidate").read_text() == "after-agent"\n'); + try { + const adapter = new GitCommitAdapter(undefined, undefined, setup); + await adapter.execute('python', ['-m', 'pytest', '-q', testFile, '-p', 'no:cacheprovider'], { untrusted: true, verifyRuntimePrerequisites: true }); + await adapter.execute('pnpm', ['--version'], { untrusted: true, verifyRuntimePrerequisites: true }); + if (readFileSync(VERIFICATION_SETUP_PATH, 'utf8') !== candidateYaml) throw new Error('Candidate action was not restored'); + core.info('Native composite acceptance passed: candidate manifests, trusted snapshot, Python/pytest and Node/pnpm, two fresh profiles.'); + } finally { + rmSync(candidateAction, { recursive: true, force: true }); rmSync(marker, { force: true }); rmSync(testFile, { force: true }); rmSync(requirements, { force: true }); + rmSync(join(process.cwd(), '__pycache__'), { recursive: true, force: true }); + git('update-ref', '-d', 'refs/remotes/origin/copilot-native-fixture', commit); + } +} + +void runNativeVerificationEntry(async () => { + try { await fixture(); } + catch (error) { core.setFailed(error instanceof Error ? error.message : 'Native acceptance failed'); } +}).catch(error => core.setFailed(error instanceof Error ? error.message : 'Native continuation failed')); From 54c2201ff71afee39c728410456354bed1b706b6 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 21 Oct 2026 18:12:04 +0200 Subject: [PATCH 4/8] fix: preserve native verification runtime through deferred post steps --- .../copilot-native-cleanup-check/action.yml | 6 + .../copilot-native-cleanup-check/main.cjs | 2 + .../copilot-native-cleanup-check/post.cjs | 7 + .../actions/copilot-native-fixture/action.yml | 2 +- .github/workflows/copilot_issue_comment.yml | 153 ++++++--- .../copilot_pull_request_comment.yml | 153 ++++++--- .github/workflows/setup_platform_smoke.yml | 306 +++++++++++++----- action.yml | 2 +- build/cli/index.js | 203 +++++++----- build/github_action/index.js | 279 ++++++++++------ docs/development/architecture.mdx | 19 +- .../operations/troubleshooting.mdx | 2 +- .../operations/verification.mdx | 31 +- scripts/coverage-budgets.json | 6 +- ...generate-native-verification-workflows.cjs | 15 +- scripts/prepare-native-source-action.cjs | 42 +++ scripts/validate-workflow-contract.cjs | 19 +- setup/workflows/copilot_issue_comment.yml | 103 ++++-- .../copilot_pull_request_comment.yml | 103 ++++-- specs/CATALOG.md | 6 +- specs/catalog.json | 12 +- specs/repository-verification-setup-hooks.md | 32 +- .../native_verification_controller.test.ts | 6 +- .../__tests__/native_verification_fixture.ts | 2 +- .../native_verification_post.test.ts | 46 +++ .../native_verification_protocol.test.ts | 28 +- .../native_verification_runner.test.ts | 9 +- ...native_verification_source_runtime.test.ts | 42 +++ .../native_verification_workflow.test.ts | 2 +- .../__tests__/verification_executable.test.ts | 38 +++ .../verification_setup_configuration.test.ts | 2 +- src/infrastructure/git_commit_adapter.ts | 6 +- .../native_verification_controller.ts | 39 ++- .../native_verification_overlay.ts | 51 --- .../native_verification_profiles.ts | 5 +- .../native_verification_protocol.ts | 1 + .../native_verification_source_runtime.ts | 15 + .../native_verification_staging.ts | 41 +++ src/infrastructure/verification_executable.ts | 17 + .../verification_setup_runner.ts | 7 +- .../verification_setup_snapshot.ts | 23 +- .../fixtures/verification_native_fixture.ts | 7 +- .../validate_workflow_contract.test.ts | 2 +- 43 files changed, 1365 insertions(+), 527 deletions(-) create mode 100644 .github/actions/copilot-native-cleanup-check/action.yml create mode 100644 .github/actions/copilot-native-cleanup-check/main.cjs create mode 100644 .github/actions/copilot-native-cleanup-check/post.cjs create mode 100644 scripts/prepare-native-source-action.cjs create mode 100644 src/infrastructure/__tests__/native_verification_post.test.ts create mode 100644 src/infrastructure/__tests__/native_verification_source_runtime.test.ts create mode 100644 src/infrastructure/__tests__/verification_executable.test.ts delete mode 100644 src/infrastructure/native_verification_overlay.ts create mode 100644 src/infrastructure/native_verification_source_runtime.ts create mode 100644 src/infrastructure/native_verification_staging.ts create mode 100644 src/infrastructure/verification_executable.ts diff --git a/.github/actions/copilot-native-cleanup-check/action.yml b/.github/actions/copilot-native-cleanup-check/action.yml new file mode 100644 index 000000000..e23989c47 --- /dev/null +++ b/.github/actions/copilot-native-cleanup-check/action.yml @@ -0,0 +1,6 @@ +name: Verify native setup cleanup +runs: + using: node24 + main: main.cjs + post: post.cjs + post-if: always() diff --git a/.github/actions/copilot-native-cleanup-check/main.cjs b/.github/actions/copilot-native-cleanup-check/main.cjs new file mode 100644 index 000000000..e036784e6 --- /dev/null +++ b/.github/actions/copilot-native-cleanup-check/main.cjs @@ -0,0 +1,2 @@ +const fs = require('node:fs'); +fs.appendFileSync(process.env.GITHUB_STATE, `workspace=${fs.realpathSync(process.cwd())}\n`); diff --git a/.github/actions/copilot-native-cleanup-check/post.cjs b/.github/actions/copilot-native-cleanup-check/post.cjs new file mode 100644 index 000000000..6348ce94d --- /dev/null +++ b/.github/actions/copilot-native-cleanup-check/post.cjs @@ -0,0 +1,7 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const workspace = process.env.STATE_workspace; +for (const file of [path.join(workspace, '.git/copilot-verification-setup'), path.join(path.dirname(workspace), '.copilot-native-fixture-runtime')]) { + if (fs.existsSync(file)) throw new Error('Native acceptance left owned staging or runtime snapshots after post cleanup.'); +} +console.log('Native acceptance cleanup passed after all deferred post steps.'); diff --git a/.github/actions/copilot-native-fixture/action.yml b/.github/actions/copilot-native-fixture/action.yml index 05ddc8acf..165ffd61b 100644 --- a/.github/actions/copilot-native-fixture/action.yml +++ b/.github/actions/copilot-native-fixture/action.yml @@ -7,4 +7,4 @@ runs: using: node24 main: dist/index.js post: dist/index.js - post-if: always() + post-if: always() && env.COPILOT_NATIVE_PHASE != '' diff --git a/.github/workflows/copilot_issue_comment.yml b/.github/workflows/copilot_issue_comment.yml index 28a557a8b..b8a567d2d 100644 --- a/.github/workflows/copilot_issue_comment.yml +++ b/.github/workflows/copilot_issue_comment.yml @@ -42,7 +42,12 @@ jobs: shell: bash run: pnpm install --frozen-lockfile --ignore-scripts - - uses: ./ + - name: Snapshot the Copilot runtime before candidate changes + id: prepare_native_runtime + run: node scripts/prepare-native-source-action.cjs + shell: bash + + - uses: ./../.copilot-native-runtime id: copilot with: &copilot-inputs repository-locale: ${{ vars.REPOSITORY_LOCALE || 'en-US' }} @@ -97,6 +102,7 @@ jobs: bugbot-organization-rules: ${{ vars.BUGBOT_ORGANIZATION_RULES }} env: COPILOT_NATIVE_PHASE: prepare + COPILOT_NATIVE_RUNTIME_CACHE: ${{ steps.prepare_native_runtime.outputs.path }} AGENT_PROVIDER: ${{ vars.AGENT_PROVIDER || 'codex' }} AGENT_MODEL_PROVIDER: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} AGENT_MODEL: ${{ vars.AGENT_MODEL }} @@ -117,7 +123,7 @@ jobs: - name: Prepare repository verification 1 id: verification_setup_1 if: ${{ !cancelled() && steps.copilot.outcome == 'success' && steps.copilot.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.copilot.outputs.verification-home }} @@ -132,17 +138,20 @@ jobs: - name: Continue repository verification 1 id: verification_continue_1 if: ${{ always() && steps.copilot.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.copilot.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_1.outcome }} - name: Prepare repository verification 2 id: verification_setup_2 if: ${{ !cancelled() && steps.verification_continue_1.outcome == 'success' && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_1.outputs.verification-home }} @@ -157,17 +166,20 @@ jobs: - name: Continue repository verification 2 id: verification_continue_2 if: ${{ always() && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_1.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_2.outcome }} - name: Prepare repository verification 3 id: verification_setup_3 if: ${{ !cancelled() && steps.verification_continue_2.outcome == 'success' && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_2.outputs.verification-home }} @@ -182,17 +194,20 @@ jobs: - name: Continue repository verification 3 id: verification_continue_3 if: ${{ always() && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_2.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_3.outcome }} - name: Prepare repository verification 4 id: verification_setup_4 if: ${{ !cancelled() && steps.verification_continue_3.outcome == 'success' && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_3.outputs.verification-home }} @@ -207,17 +222,20 @@ jobs: - name: Continue repository verification 4 id: verification_continue_4 if: ${{ always() && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_3.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_4.outcome }} - name: Prepare repository verification 5 id: verification_setup_5 if: ${{ !cancelled() && steps.verification_continue_4.outcome == 'success' && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_4.outputs.verification-home }} @@ -232,17 +250,20 @@ jobs: - name: Continue repository verification 5 id: verification_continue_5 if: ${{ always() && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_4.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_5.outcome }} - name: Prepare repository verification 6 id: verification_setup_6 if: ${{ !cancelled() && steps.verification_continue_5.outcome == 'success' && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_5.outputs.verification-home }} @@ -257,17 +278,20 @@ jobs: - name: Continue repository verification 6 id: verification_continue_6 if: ${{ always() && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_5.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_6.outcome }} - name: Prepare repository verification 7 id: verification_setup_7 if: ${{ !cancelled() && steps.verification_continue_6.outcome == 'success' && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_6.outputs.verification-home }} @@ -282,17 +306,20 @@ jobs: - name: Continue repository verification 7 id: verification_continue_7 if: ${{ always() && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_6.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_7.outcome }} - name: Prepare repository verification 8 id: verification_setup_8 if: ${{ !cancelled() && steps.verification_continue_7.outcome == 'success' && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_7.outputs.verification-home }} @@ -307,17 +334,20 @@ jobs: - name: Continue repository verification 8 id: verification_continue_8 if: ${{ always() && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_7.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_8.outcome }} - name: Prepare repository verification 9 id: verification_setup_9 if: ${{ !cancelled() && steps.verification_continue_8.outcome == 'success' && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_8.outputs.verification-home }} @@ -332,17 +362,20 @@ jobs: - name: Continue repository verification 9 id: verification_continue_9 if: ${{ always() && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_8.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_9.outcome }} - name: Prepare repository verification 10 id: verification_setup_10 if: ${{ !cancelled() && steps.verification_continue_9.outcome == 'success' && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_9.outputs.verification-home }} @@ -357,17 +390,20 @@ jobs: - name: Continue repository verification 10 id: verification_continue_10 if: ${{ always() && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_9.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_10.outcome }} - name: Prepare repository verification 11 id: verification_setup_11 if: ${{ !cancelled() && steps.verification_continue_10.outcome == 'success' && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_10.outputs.verification-home }} @@ -382,17 +418,20 @@ jobs: - name: Continue repository verification 11 id: verification_continue_11 if: ${{ always() && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_10.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_11.outcome }} - name: Prepare repository verification 12 id: verification_setup_12 if: ${{ !cancelled() && steps.verification_continue_11.outcome == 'success' && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_11.outputs.verification-home }} @@ -407,17 +446,20 @@ jobs: - name: Continue repository verification 12 id: verification_continue_12 if: ${{ always() && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_11.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_12.outcome }} - name: Prepare repository verification 13 id: verification_setup_13 if: ${{ !cancelled() && steps.verification_continue_12.outcome == 'success' && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_12.outputs.verification-home }} @@ -432,17 +474,20 @@ jobs: - name: Continue repository verification 13 id: verification_continue_13 if: ${{ always() && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_12.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_13.outcome }} - name: Prepare repository verification 14 id: verification_setup_14 if: ${{ !cancelled() && steps.verification_continue_13.outcome == 'success' && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_13.outputs.verification-home }} @@ -457,17 +502,20 @@ jobs: - name: Continue repository verification 14 id: verification_continue_14 if: ${{ always() && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_13.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_14.outcome }} - name: Prepare repository verification 15 id: verification_setup_15 if: ${{ !cancelled() && steps.verification_continue_14.outcome == 'success' && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_14.outputs.verification-home }} @@ -482,17 +530,20 @@ jobs: - name: Continue repository verification 15 id: verification_continue_15 if: ${{ always() && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_14.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_15.outcome }} - name: Prepare repository verification 16 id: verification_setup_16 if: ${{ !cancelled() && steps.verification_continue_15.outcome == 'success' && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_15.outputs.verification-home }} @@ -507,17 +558,20 @@ jobs: - name: Continue repository verification 16 id: verification_continue_16 if: ${{ always() && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_15.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_16.outcome }} - name: Prepare repository verification 17 id: verification_setup_17 if: ${{ !cancelled() && steps.verification_continue_16.outcome == 'success' && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_16.outputs.verification-home }} @@ -532,17 +586,20 @@ jobs: - name: Continue repository verification 17 id: verification_continue_17 if: ${{ always() && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_16.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_17.outcome }} - name: Prepare repository verification 18 id: verification_setup_18 if: ${{ !cancelled() && steps.verification_continue_17.outcome == 'success' && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_17.outputs.verification-home }} @@ -557,17 +614,20 @@ jobs: - name: Continue repository verification 18 id: verification_continue_18 if: ${{ always() && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_17.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_18.outcome }} - name: Prepare repository verification 19 id: verification_setup_19 if: ${{ !cancelled() && steps.verification_continue_18.outcome == 'success' && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_18.outputs.verification-home }} @@ -582,17 +642,20 @@ jobs: - name: Continue repository verification 19 id: verification_continue_19 if: ${{ always() && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_18.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_19.outcome }} - name: Prepare repository verification 20 id: verification_setup_20 if: ${{ !cancelled() && steps.verification_continue_19.outcome == 'success' && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_19.outputs.verification-home }} @@ -607,19 +670,25 @@ jobs: - name: Continue repository verification 20 id: verification_continue_20 if: ${{ always() && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_19.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_20.outcome }} - name: Complete repository verification id: verification_complete if: ${{ always() && steps.copilot.outputs.verification-ticket != '' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: finish + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} # copilot:native-verification:end diff --git a/.github/workflows/copilot_pull_request_comment.yml b/.github/workflows/copilot_pull_request_comment.yml index 88f786e3a..b3e5f1f38 100644 --- a/.github/workflows/copilot_pull_request_comment.yml +++ b/.github/workflows/copilot_pull_request_comment.yml @@ -42,7 +42,12 @@ jobs: shell: bash run: pnpm install --frozen-lockfile --ignore-scripts - - uses: ./ + - name: Snapshot the Copilot runtime before candidate changes + id: prepare_native_runtime + run: node scripts/prepare-native-source-action.cjs + shell: bash + + - uses: ./../.copilot-native-runtime id: copilot with: &copilot-inputs repository-locale: ${{ vars.REPOSITORY_LOCALE || 'en-US' }} @@ -97,6 +102,7 @@ jobs: bugbot-organization-rules: ${{ vars.BUGBOT_ORGANIZATION_RULES }} env: COPILOT_NATIVE_PHASE: prepare + COPILOT_NATIVE_RUNTIME_CACHE: ${{ steps.prepare_native_runtime.outputs.path }} AGENT_PROVIDER: ${{ vars.AGENT_PROVIDER || 'codex' }} AGENT_MODEL_PROVIDER: ${{ vars.AGENT_MODEL_PROVIDER || 'openai' }} AGENT_MODEL: ${{ vars.AGENT_MODEL }} @@ -117,7 +123,7 @@ jobs: - name: Prepare repository verification 1 id: verification_setup_1 if: ${{ !cancelled() && steps.copilot.outcome == 'success' && steps.copilot.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.copilot.outputs.verification-home }} @@ -132,17 +138,20 @@ jobs: - name: Continue repository verification 1 id: verification_continue_1 if: ${{ always() && steps.copilot.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.copilot.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_1.outcome }} - name: Prepare repository verification 2 id: verification_setup_2 if: ${{ !cancelled() && steps.verification_continue_1.outcome == 'success' && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_1.outputs.verification-home }} @@ -157,17 +166,20 @@ jobs: - name: Continue repository verification 2 id: verification_continue_2 if: ${{ always() && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_1.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_2.outcome }} - name: Prepare repository verification 3 id: verification_setup_3 if: ${{ !cancelled() && steps.verification_continue_2.outcome == 'success' && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_2.outputs.verification-home }} @@ -182,17 +194,20 @@ jobs: - name: Continue repository verification 3 id: verification_continue_3 if: ${{ always() && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_2.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_3.outcome }} - name: Prepare repository verification 4 id: verification_setup_4 if: ${{ !cancelled() && steps.verification_continue_3.outcome == 'success' && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_3.outputs.verification-home }} @@ -207,17 +222,20 @@ jobs: - name: Continue repository verification 4 id: verification_continue_4 if: ${{ always() && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_3.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_4.outcome }} - name: Prepare repository verification 5 id: verification_setup_5 if: ${{ !cancelled() && steps.verification_continue_4.outcome == 'success' && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_4.outputs.verification-home }} @@ -232,17 +250,20 @@ jobs: - name: Continue repository verification 5 id: verification_continue_5 if: ${{ always() && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_4.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_5.outcome }} - name: Prepare repository verification 6 id: verification_setup_6 if: ${{ !cancelled() && steps.verification_continue_5.outcome == 'success' && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_5.outputs.verification-home }} @@ -257,17 +278,20 @@ jobs: - name: Continue repository verification 6 id: verification_continue_6 if: ${{ always() && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_5.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_6.outcome }} - name: Prepare repository verification 7 id: verification_setup_7 if: ${{ !cancelled() && steps.verification_continue_6.outcome == 'success' && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_6.outputs.verification-home }} @@ -282,17 +306,20 @@ jobs: - name: Continue repository verification 7 id: verification_continue_7 if: ${{ always() && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_6.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_7.outcome }} - name: Prepare repository verification 8 id: verification_setup_8 if: ${{ !cancelled() && steps.verification_continue_7.outcome == 'success' && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_7.outputs.verification-home }} @@ -307,17 +334,20 @@ jobs: - name: Continue repository verification 8 id: verification_continue_8 if: ${{ always() && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_7.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_8.outcome }} - name: Prepare repository verification 9 id: verification_setup_9 if: ${{ !cancelled() && steps.verification_continue_8.outcome == 'success' && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_8.outputs.verification-home }} @@ -332,17 +362,20 @@ jobs: - name: Continue repository verification 9 id: verification_continue_9 if: ${{ always() && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_8.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_9.outcome }} - name: Prepare repository verification 10 id: verification_setup_10 if: ${{ !cancelled() && steps.verification_continue_9.outcome == 'success' && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_9.outputs.verification-home }} @@ -357,17 +390,20 @@ jobs: - name: Continue repository verification 10 id: verification_continue_10 if: ${{ always() && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_9.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_10.outcome }} - name: Prepare repository verification 11 id: verification_setup_11 if: ${{ !cancelled() && steps.verification_continue_10.outcome == 'success' && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_10.outputs.verification-home }} @@ -382,17 +418,20 @@ jobs: - name: Continue repository verification 11 id: verification_continue_11 if: ${{ always() && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_10.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_11.outcome }} - name: Prepare repository verification 12 id: verification_setup_12 if: ${{ !cancelled() && steps.verification_continue_11.outcome == 'success' && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_11.outputs.verification-home }} @@ -407,17 +446,20 @@ jobs: - name: Continue repository verification 12 id: verification_continue_12 if: ${{ always() && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_11.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_12.outcome }} - name: Prepare repository verification 13 id: verification_setup_13 if: ${{ !cancelled() && steps.verification_continue_12.outcome == 'success' && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_12.outputs.verification-home }} @@ -432,17 +474,20 @@ jobs: - name: Continue repository verification 13 id: verification_continue_13 if: ${{ always() && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_12.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_13.outcome }} - name: Prepare repository verification 14 id: verification_setup_14 if: ${{ !cancelled() && steps.verification_continue_13.outcome == 'success' && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_13.outputs.verification-home }} @@ -457,17 +502,20 @@ jobs: - name: Continue repository verification 14 id: verification_continue_14 if: ${{ always() && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_13.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_14.outcome }} - name: Prepare repository verification 15 id: verification_setup_15 if: ${{ !cancelled() && steps.verification_continue_14.outcome == 'success' && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_14.outputs.verification-home }} @@ -482,17 +530,20 @@ jobs: - name: Continue repository verification 15 id: verification_continue_15 if: ${{ always() && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_14.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_15.outcome }} - name: Prepare repository verification 16 id: verification_setup_16 if: ${{ !cancelled() && steps.verification_continue_15.outcome == 'success' && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_15.outputs.verification-home }} @@ -507,17 +558,20 @@ jobs: - name: Continue repository verification 16 id: verification_continue_16 if: ${{ always() && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_15.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_16.outcome }} - name: Prepare repository verification 17 id: verification_setup_17 if: ${{ !cancelled() && steps.verification_continue_16.outcome == 'success' && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_16.outputs.verification-home }} @@ -532,17 +586,20 @@ jobs: - name: Continue repository verification 17 id: verification_continue_17 if: ${{ always() && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_16.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_17.outcome }} - name: Prepare repository verification 18 id: verification_setup_18 if: ${{ !cancelled() && steps.verification_continue_17.outcome == 'success' && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_17.outputs.verification-home }} @@ -557,17 +614,20 @@ jobs: - name: Continue repository verification 18 id: verification_continue_18 if: ${{ always() && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_17.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_18.outcome }} - name: Prepare repository verification 19 id: verification_setup_19 if: ${{ !cancelled() && steps.verification_continue_18.outcome == 'success' && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_18.outputs.verification-home }} @@ -582,17 +642,20 @@ jobs: - name: Continue repository verification 19 id: verification_continue_19 if: ${{ always() && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_18.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_19.outcome }} - name: Prepare repository verification 20 id: verification_setup_20 if: ${{ !cancelled() && steps.verification_continue_19.outcome == 'success' && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_19.outputs.verification-home }} @@ -607,19 +670,25 @@ jobs: - name: Continue repository verification 20 id: verification_continue_20 if: ${{ always() && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_19.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_20.outcome }} - name: Complete repository verification id: verification_complete if: ${{ always() && steps.copilot.outputs.verification-ticket != '' }} - uses: ./ + uses: ./../.copilot-native-runtime with: *copilot-inputs env: COPILOT_NATIVE_PHASE: finish + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} # copilot:native-verification:end diff --git a/.github/workflows/setup_platform_smoke.yml b/.github/workflows/setup_platform_smoke.yml index 93ea203b6..81a8fec6c 100644 --- a/.github/workflows/setup_platform_smoke.yml +++ b/.github/workflows/setup_platform_smoke.yml @@ -63,19 +63,25 @@ jobs: if: runner.os == 'Windows' run: node scripts/verify-windows-job-npm.cjs - run: pnpm install --frozen-lockfile + - name: Register native post cleanup acceptance + uses: ./.github/actions/copilot-native-cleanup-check - name: Build native verification acceptance fixture run: pnpm exec ncc build src/tooling/__tests__/fixtures/verification_native_fixture.ts -o .github/actions/copilot-native-fixture/dist + - name: Snapshot native fixture runtime + id: prepare_native_fixture_runtime + run: node scripts/prepare-native-source-action.cjs --fixture - name: Prepare native verification acceptance id: copilot_native_fixture - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: &native-fixture-inputs verification-setup: 'true' env: COPILOT_NATIVE_PHASE: prepare + COPILOT_NATIVE_RUNTIME_CACHE: ${{ steps.prepare_native_fixture_runtime.outputs.path }} - name: Prepare repository verification 1 id: verification_setup_1 if: ${{ !cancelled() && steps.copilot_native_fixture.outcome == 'success' && steps.copilot_native_fixture.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.copilot_native_fixture.outputs.verification-home }} @@ -90,17 +96,20 @@ jobs: - name: Continue repository verification 1 id: verification_continue_1 if: ${{ always() && steps.copilot_native_fixture.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.copilot_native_fixture.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_1.outcome }} - name: Prepare repository verification 2 id: verification_setup_2 if: ${{ !cancelled() && steps.verification_continue_1.outcome == 'success' && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_1.outputs.verification-home }} @@ -115,17 +124,20 @@ jobs: - name: Continue repository verification 2 id: verification_continue_2 if: ${{ always() && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_1.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_2.outcome }} - name: Prepare repository verification 3 id: verification_setup_3 if: ${{ !cancelled() && steps.verification_continue_2.outcome == 'success' && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_2.outputs.verification-home }} @@ -140,17 +152,20 @@ jobs: - name: Continue repository verification 3 id: verification_continue_3 if: ${{ always() && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_2.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_3.outcome }} - name: Prepare repository verification 4 id: verification_setup_4 if: ${{ !cancelled() && steps.verification_continue_3.outcome == 'success' && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_3.outputs.verification-home }} @@ -165,17 +180,20 @@ jobs: - name: Continue repository verification 4 id: verification_continue_4 if: ${{ always() && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_3.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_4.outcome }} - name: Prepare repository verification 5 id: verification_setup_5 if: ${{ !cancelled() && steps.verification_continue_4.outcome == 'success' && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_4.outputs.verification-home }} @@ -190,17 +208,20 @@ jobs: - name: Continue repository verification 5 id: verification_continue_5 if: ${{ always() && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_4.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_5.outcome }} - name: Prepare repository verification 6 id: verification_setup_6 if: ${{ !cancelled() && steps.verification_continue_5.outcome == 'success' && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_5.outputs.verification-home }} @@ -215,17 +236,20 @@ jobs: - name: Continue repository verification 6 id: verification_continue_6 if: ${{ always() && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_5.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_6.outcome }} - name: Prepare repository verification 7 id: verification_setup_7 if: ${{ !cancelled() && steps.verification_continue_6.outcome == 'success' && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_6.outputs.verification-home }} @@ -240,17 +264,20 @@ jobs: - name: Continue repository verification 7 id: verification_continue_7 if: ${{ always() && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_6.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_7.outcome }} - name: Prepare repository verification 8 id: verification_setup_8 if: ${{ !cancelled() && steps.verification_continue_7.outcome == 'success' && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_7.outputs.verification-home }} @@ -265,17 +292,20 @@ jobs: - name: Continue repository verification 8 id: verification_continue_8 if: ${{ always() && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_7.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_8.outcome }} - name: Prepare repository verification 9 id: verification_setup_9 if: ${{ !cancelled() && steps.verification_continue_8.outcome == 'success' && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_8.outputs.verification-home }} @@ -290,17 +320,20 @@ jobs: - name: Continue repository verification 9 id: verification_continue_9 if: ${{ always() && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_8.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_9.outcome }} - name: Prepare repository verification 10 id: verification_setup_10 if: ${{ !cancelled() && steps.verification_continue_9.outcome == 'success' && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_9.outputs.verification-home }} @@ -315,17 +348,20 @@ jobs: - name: Continue repository verification 10 id: verification_continue_10 if: ${{ always() && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_9.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_10.outcome }} - name: Prepare repository verification 11 id: verification_setup_11 if: ${{ !cancelled() && steps.verification_continue_10.outcome == 'success' && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_10.outputs.verification-home }} @@ -340,17 +376,20 @@ jobs: - name: Continue repository verification 11 id: verification_continue_11 if: ${{ always() && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_10.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_11.outcome }} - name: Prepare repository verification 12 id: verification_setup_12 if: ${{ !cancelled() && steps.verification_continue_11.outcome == 'success' && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_11.outputs.verification-home }} @@ -365,17 +404,20 @@ jobs: - name: Continue repository verification 12 id: verification_continue_12 if: ${{ always() && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_11.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_12.outcome }} - name: Prepare repository verification 13 id: verification_setup_13 if: ${{ !cancelled() && steps.verification_continue_12.outcome == 'success' && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_12.outputs.verification-home }} @@ -390,17 +432,20 @@ jobs: - name: Continue repository verification 13 id: verification_continue_13 if: ${{ always() && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_12.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_13.outcome }} - name: Prepare repository verification 14 id: verification_setup_14 if: ${{ !cancelled() && steps.verification_continue_13.outcome == 'success' && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_13.outputs.verification-home }} @@ -415,17 +460,20 @@ jobs: - name: Continue repository verification 14 id: verification_continue_14 if: ${{ always() && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_13.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_14.outcome }} - name: Prepare repository verification 15 id: verification_setup_15 if: ${{ !cancelled() && steps.verification_continue_14.outcome == 'success' && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_14.outputs.verification-home }} @@ -440,17 +488,20 @@ jobs: - name: Continue repository verification 15 id: verification_continue_15 if: ${{ always() && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_14.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_15.outcome }} - name: Prepare repository verification 16 id: verification_setup_16 if: ${{ !cancelled() && steps.verification_continue_15.outcome == 'success' && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_15.outputs.verification-home }} @@ -465,17 +516,20 @@ jobs: - name: Continue repository verification 16 id: verification_continue_16 if: ${{ always() && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_15.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_16.outcome }} - name: Prepare repository verification 17 id: verification_setup_17 if: ${{ !cancelled() && steps.verification_continue_16.outcome == 'success' && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_16.outputs.verification-home }} @@ -490,17 +544,20 @@ jobs: - name: Continue repository verification 17 id: verification_continue_17 if: ${{ always() && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_16.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_17.outcome }} - name: Prepare repository verification 18 id: verification_setup_18 if: ${{ !cancelled() && steps.verification_continue_17.outcome == 'success' && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_17.outputs.verification-home }} @@ -515,17 +572,20 @@ jobs: - name: Continue repository verification 18 id: verification_continue_18 if: ${{ always() && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_17.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_18.outcome }} - name: Prepare repository verification 19 id: verification_setup_19 if: ${{ !cancelled() && steps.verification_continue_18.outcome == 'success' && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_18.outputs.verification-home }} @@ -540,17 +600,20 @@ jobs: - name: Continue repository verification 19 id: verification_continue_19 if: ${{ always() && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_18.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_19.outcome }} - name: Prepare repository verification 20 id: verification_setup_20 if: ${{ !cancelled() && steps.verification_continue_19.outcome == 'success' && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_19.outputs.verification-home }} @@ -565,20 +628,26 @@ jobs: - name: Continue repository verification 20 id: verification_continue_20 if: ${{ always() && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_19.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_20.outcome }} - name: Complete repository verification id: verification_complete if: ${{ always() && steps.copilot_native_fixture.outputs.verification-ticket != '' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: finish + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} - run: pnpm run build # Checked-in ncc bundles are generated on Unix; Windows output differs. @@ -674,19 +743,25 @@ jobs: run: | git ls-files --eol -- .copilot/repository-profile.json .copilot/AGENT_GUIDE.md .agents/skills/copilot-repository-workflow/SKILL.md pnpm run validate:agent-docs + - name: Register native post cleanup acceptance + uses: ./.github/actions/copilot-native-cleanup-check - name: Build native verification acceptance fixture run: pnpm exec ncc build src/tooling/__tests__/fixtures/verification_native_fixture.ts -o .github/actions/copilot-native-fixture/dist + - name: Snapshot native fixture runtime + id: prepare_native_fixture_runtime + run: node scripts/prepare-native-source-action.cjs --fixture - name: Prepare native verification acceptance id: copilot_native_fixture - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: &native-fixture-inputs verification-setup: 'true' env: COPILOT_NATIVE_PHASE: prepare + COPILOT_NATIVE_RUNTIME_CACHE: ${{ steps.prepare_native_fixture_runtime.outputs.path }} - name: Prepare repository verification 1 id: verification_setup_1 if: ${{ !cancelled() && steps.copilot_native_fixture.outcome == 'success' && steps.copilot_native_fixture.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.copilot_native_fixture.outputs.verification-home }} @@ -701,17 +776,20 @@ jobs: - name: Continue repository verification 1 id: verification_continue_1 if: ${{ always() && steps.copilot_native_fixture.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.copilot_native_fixture.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_1.outcome }} - name: Prepare repository verification 2 id: verification_setup_2 if: ${{ !cancelled() && steps.verification_continue_1.outcome == 'success' && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_1.outputs.verification-home }} @@ -726,17 +804,20 @@ jobs: - name: Continue repository verification 2 id: verification_continue_2 if: ${{ always() && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_1.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_2.outcome }} - name: Prepare repository verification 3 id: verification_setup_3 if: ${{ !cancelled() && steps.verification_continue_2.outcome == 'success' && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_2.outputs.verification-home }} @@ -751,17 +832,20 @@ jobs: - name: Continue repository verification 3 id: verification_continue_3 if: ${{ always() && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_2.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_3.outcome }} - name: Prepare repository verification 4 id: verification_setup_4 if: ${{ !cancelled() && steps.verification_continue_3.outcome == 'success' && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_3.outputs.verification-home }} @@ -776,17 +860,20 @@ jobs: - name: Continue repository verification 4 id: verification_continue_4 if: ${{ always() && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_3.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_4.outcome }} - name: Prepare repository verification 5 id: verification_setup_5 if: ${{ !cancelled() && steps.verification_continue_4.outcome == 'success' && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_4.outputs.verification-home }} @@ -801,17 +888,20 @@ jobs: - name: Continue repository verification 5 id: verification_continue_5 if: ${{ always() && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_4.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_5.outcome }} - name: Prepare repository verification 6 id: verification_setup_6 if: ${{ !cancelled() && steps.verification_continue_5.outcome == 'success' && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_5.outputs.verification-home }} @@ -826,17 +916,20 @@ jobs: - name: Continue repository verification 6 id: verification_continue_6 if: ${{ always() && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_5.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_6.outcome }} - name: Prepare repository verification 7 id: verification_setup_7 if: ${{ !cancelled() && steps.verification_continue_6.outcome == 'success' && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_6.outputs.verification-home }} @@ -851,17 +944,20 @@ jobs: - name: Continue repository verification 7 id: verification_continue_7 if: ${{ always() && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_6.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_7.outcome }} - name: Prepare repository verification 8 id: verification_setup_8 if: ${{ !cancelled() && steps.verification_continue_7.outcome == 'success' && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_7.outputs.verification-home }} @@ -876,17 +972,20 @@ jobs: - name: Continue repository verification 8 id: verification_continue_8 if: ${{ always() && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_7.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_8.outcome }} - name: Prepare repository verification 9 id: verification_setup_9 if: ${{ !cancelled() && steps.verification_continue_8.outcome == 'success' && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_8.outputs.verification-home }} @@ -901,17 +1000,20 @@ jobs: - name: Continue repository verification 9 id: verification_continue_9 if: ${{ always() && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_8.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_9.outcome }} - name: Prepare repository verification 10 id: verification_setup_10 if: ${{ !cancelled() && steps.verification_continue_9.outcome == 'success' && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_9.outputs.verification-home }} @@ -926,17 +1028,20 @@ jobs: - name: Continue repository verification 10 id: verification_continue_10 if: ${{ always() && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_9.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_10.outcome }} - name: Prepare repository verification 11 id: verification_setup_11 if: ${{ !cancelled() && steps.verification_continue_10.outcome == 'success' && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_10.outputs.verification-home }} @@ -951,17 +1056,20 @@ jobs: - name: Continue repository verification 11 id: verification_continue_11 if: ${{ always() && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_10.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_11.outcome }} - name: Prepare repository verification 12 id: verification_setup_12 if: ${{ !cancelled() && steps.verification_continue_11.outcome == 'success' && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_11.outputs.verification-home }} @@ -976,17 +1084,20 @@ jobs: - name: Continue repository verification 12 id: verification_continue_12 if: ${{ always() && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_11.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_12.outcome }} - name: Prepare repository verification 13 id: verification_setup_13 if: ${{ !cancelled() && steps.verification_continue_12.outcome == 'success' && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_12.outputs.verification-home }} @@ -1001,17 +1112,20 @@ jobs: - name: Continue repository verification 13 id: verification_continue_13 if: ${{ always() && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_12.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_13.outcome }} - name: Prepare repository verification 14 id: verification_setup_14 if: ${{ !cancelled() && steps.verification_continue_13.outcome == 'success' && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_13.outputs.verification-home }} @@ -1026,17 +1140,20 @@ jobs: - name: Continue repository verification 14 id: verification_continue_14 if: ${{ always() && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_13.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_14.outcome }} - name: Prepare repository verification 15 id: verification_setup_15 if: ${{ !cancelled() && steps.verification_continue_14.outcome == 'success' && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_14.outputs.verification-home }} @@ -1051,17 +1168,20 @@ jobs: - name: Continue repository verification 15 id: verification_continue_15 if: ${{ always() && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_14.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_15.outcome }} - name: Prepare repository verification 16 id: verification_setup_16 if: ${{ !cancelled() && steps.verification_continue_15.outcome == 'success' && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_15.outputs.verification-home }} @@ -1076,17 +1196,20 @@ jobs: - name: Continue repository verification 16 id: verification_continue_16 if: ${{ always() && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_15.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_16.outcome }} - name: Prepare repository verification 17 id: verification_setup_17 if: ${{ !cancelled() && steps.verification_continue_16.outcome == 'success' && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_16.outputs.verification-home }} @@ -1101,17 +1224,20 @@ jobs: - name: Continue repository verification 17 id: verification_continue_17 if: ${{ always() && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_16.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_17.outcome }} - name: Prepare repository verification 18 id: verification_setup_18 if: ${{ !cancelled() && steps.verification_continue_17.outcome == 'success' && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_17.outputs.verification-home }} @@ -1126,17 +1252,20 @@ jobs: - name: Continue repository verification 18 id: verification_continue_18 if: ${{ always() && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_17.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_18.outcome }} - name: Prepare repository verification 19 id: verification_setup_19 if: ${{ !cancelled() && steps.verification_continue_18.outcome == 'success' && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_18.outputs.verification-home }} @@ -1151,17 +1280,20 @@ jobs: - name: Continue repository verification 19 id: verification_continue_19 if: ${{ always() && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_18.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_19.outcome }} - name: Prepare repository verification 20 id: verification_setup_20 if: ${{ !cancelled() && steps.verification_continue_19.outcome == 'success' && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_19.outputs.verification-home }} @@ -1176,20 +1308,26 @@ jobs: - name: Continue repository verification 20 id: verification_continue_20 if: ${{ always() && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_19.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_20.outcome }} - name: Complete repository verification id: verification_complete if: ${{ always() && steps.copilot_native_fixture.outputs.verification-ticket != '' }} - uses: ./.github/actions/copilot-native-fixture + uses: ./../.copilot-native-fixture-runtime with: *native-fixture-inputs env: COPILOT_NATIVE_PHASE: finish + COPILOT_NATIVE_SOURCE: ${{ steps.copilot_native_fixture.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot_native_fixture.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot_native_fixture.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot_native_fixture.outputs.verification-ticket }} - run: pnpm run build - run: pnpm run validate:build diff --git a/action.yml b/action.yml index 2afacc5af..9772d06bf 100644 --- a/action.yml +++ b/action.yml @@ -562,7 +562,7 @@ runs: using: "node24" main: "build/github_action/index.js" post: "build/github_action/index.js" - post-if: "always()" + post-if: "always() && env.COPILOT_NATIVE_PHASE != ''" branding: icon: "cpu" color: "blue" diff --git a/build/cli/index.js b/build/cli/index.js index 90e58e2dc..3230732a4 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -97211,6 +97211,7 @@ var __importStar = (this && this.__importStar) || (function () { })(); Object.defineProperty(exports, "__esModule", ({ value: true })); exports.GitCommitAdapter = void 0; +const verification_executable_1 = __nccwpck_require__(20878); const native_verification_profiles_1 = __nccwpck_require__(22290); const exec = __importStar(__nccwpck_require__(18538)); const git_authentication_environment_1 = __nccwpck_require__(16535); @@ -97221,7 +97222,7 @@ const verification_setup_ports_1 = __nccwpck_require__(26509); const verification_setup_runner_1 = __nccwpck_require__(11709); class GitCommitAdapter { constructor(executeCommand = (program, args, options) => options - ? exec.exec(program, args, { + ? exec.exec(options.resolveFromEnvironment && options.env ? (0, verification_executable_1.verificationExecutable)(program, options.env) : program, args, { // Captured Git reads can contain NUL-delimited records that // hide subsequent lines in GitHub's Actions log viewer. ...(options.stdout ? { silent: true } : {}), @@ -97249,6 +97250,7 @@ class GitCommitAdapter { return await this.executeCommand(program, args, { ...(options.stdout ? { stdout: options.stdout } : {}), env: runtime.environment, + ...(options.verifyRuntimePrerequisites && this.verificationSetup.kind === 'ready' ? { resolveFromEnvironment: true } : {}), }); } finally { @@ -98293,82 +98295,6 @@ function verificationCandidateIdentity(workspace, environment) { } -/***/ }), - -/***/ 64378: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { - -"use strict"; - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.stageNativeVerificationAction = stageNativeVerificationAction; -exports.restoreNativeVerificationAction = restoreNativeVerificationAction; -const node_fs_1 = __nccwpck_require__(87561); -const node_path_1 = __nccwpck_require__(49411); -const verification_setup_policy_1 = __nccwpck_require__(65326); -const native_verification_protocol_1 = __nccwpck_require__(88709); -/** A temporary reviewed action overlays the candidate path only while the operation is paused. */ -function stageNativeVerificationAction(setup, session) { - const target = (0, node_path_1.join)(setup.workspace, verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY); - const backup = (0, node_path_1.join)(session, 'candidate-action'); - const created = []; - let parent = setup.workspace; - for (const part of verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY.split('/').slice(0, -1)) { - parent = (0, node_path_1.join)(parent, part); - if ((0, node_fs_1.existsSync)(parent)) { - if ((0, node_fs_1.lstatSync)(parent).isSymbolicLink() || !(0, node_fs_1.lstatSync)(parent).isDirectory() || (0, node_fs_1.realpathSync)(parent) !== parent) - throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); - } - else { - (0, node_fs_1.mkdirSync)(parent, { mode: 0o700 }); - created.push(parent); - } - } - const targetStat = (0, node_fs_1.lstatSync)(target, { throwIfNoEntry: false }); - const existed = !!targetStat; - if (targetStat && (targetStat.isSymbolicLink() || !targetStat.isDirectory())) - throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); - const record = { target, backup, existed, created }; - (0, native_verification_protocol_1.writeNativeRecord)(session, 'overlay.json', record); - if (existed) - (0, node_fs_1.renameSync)(target, backup); - (0, node_fs_1.mkdirSync)(target, { mode: 0o700 }); - for (const file of setup.files) { - const destination = (0, node_path_1.join)(target, file.path); - const remainder = (0, node_path_1.relative)(target, destination); - if (!remainder || remainder.startsWith(`..${node_path_1.sep}`) || remainder === '..') - throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); - (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(destination), { recursive: true, mode: 0o700 }); - (0, node_fs_1.writeFileSync)(destination, Buffer.from(file.base64, 'base64'), { mode: file.executable ? 0o700 : 0o600, flag: 'wx' }); - } -} -function restoreNativeVerificationAction(session, workspace) { - const record = (0, native_verification_protocol_1.readNativeRecord)(session, 'overlay.json'); - if (!record) - return; - if (record.target !== (0, node_path_1.join)(workspace, verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY) || record.backup !== (0, node_path_1.join)(session, 'candidate-action') - || !Array.isArray(record.created) || record.created.some(path => ![(0, node_path_1.join)(workspace, '.github'), (0, node_path_1.join)(workspace, '.github/actions')].includes(path))) - throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); - for (const parent of [(0, node_path_1.join)(workspace, '.github'), (0, node_path_1.join)(workspace, '.github/actions')]) { - if (!(0, node_fs_1.existsSync)(parent) || (0, node_fs_1.lstatSync)(parent).isSymbolicLink() || !(0, node_fs_1.lstatSync)(parent).isDirectory() || (0, node_fs_1.realpathSync)(parent) !== parent) - throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); - } - // A backup is the authoritative original. A second cleanup must never remove it. - if (record.existed && !(0, node_fs_1.existsSync)(record.backup)) - return; - (0, node_fs_1.rmSync)(record.target, { recursive: true, force: true }); - if (record.existed) - (0, node_fs_1.renameSync)(record.backup, record.target); - for (const directory of [...record.created].reverse()) { - try { - (0, node_fs_1.rmdirSync)(directory); - } - catch { /* Other candidate content remains. */ } - } - (0, node_fs_1.rmSync)((0, node_path_1.join)(session, 'overlay.json'), { force: true }); -} - - /***/ }), /***/ 22290: @@ -98381,6 +98307,7 @@ exports.nativeVerificationProfilePaths = nativeVerificationProfilePaths; exports.registerNativeVerificationProfile = registerNativeVerificationProfile; exports.nativeVerificationRetainsProfile = nativeVerificationRetainsProfile; exports.removeNativeVerificationProfiles = removeNativeVerificationProfiles; +const node_crypto_1 = __nccwpck_require__(6005); const node_fs_1 = __nccwpck_require__(87561); const node_path_1 = __nccwpck_require__(49411); const verification_setup_policy_1 = __nccwpck_require__(65326); @@ -98400,7 +98327,7 @@ function registerNativeVerificationProfile(root, profile) { || profile === root || !/^copilot-verify-runtime-[A-Za-z0-9]+$/u.test((0, node_path_1.basename)(profile)) || (0, node_fs_1.realpathSync)(profile) !== profile || !(0, node_fs_1.lstatSync)(profile).isDirectory() || (0, node_fs_1.lstatSync)(profile).isSymbolicLink()) throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); - (0, node_fs_1.writeFileSync)((0, node_path_1.join)(profile, 'native-session-owner'), root, { mode: 0o600, flag: 'wx' }); + (0, node_fs_1.writeFileSync)((0, node_path_1.join)(profile, 'native-session-owner'), (0, node_crypto_1.createHash)('sha256').update(root).digest('hex'), { mode: 0o600, flag: 'wx' }); (0, native_verification_protocol_1.writeNativeRecord)(root, 'profiles.json', [...previous, profile]); } function nativeVerificationRetainsProfile(profile) { @@ -98418,7 +98345,7 @@ function removeNativeVerificationProfiles(root) { continue; throw error; } - if (!stat.isDirectory() || stat.isSymbolicLink() || (0, node_fs_1.realpathSync)(path) !== path || (0, node_fs_1.readFileSync)((0, node_path_1.join)(path, 'native-session-owner'), 'utf8') !== root) + if (!stat.isDirectory() || stat.isSymbolicLink() || (0, node_fs_1.realpathSync)(path) !== path || (0, node_fs_1.readFileSync)((0, node_path_1.join)(path, 'native-session-owner'), 'utf8') !== (0, node_crypto_1.createHash)('sha256').update(root).digest('hex')) throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); (0, node_fs_1.rmSync)(path, { recursive: true, force: true }); } @@ -98490,6 +98417,59 @@ const nativePause = (milliseconds) => new Promise(resolve => setTimeout(resolve, exports.nativePause = nativePause; +/***/ }), + +/***/ 63587: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.NATIVE_VERIFICATION_ACTION = void 0; +exports.stageNativeVerificationAction = stageNativeVerificationAction; +exports.cleanupNativeVerificationAction = cleanupNativeVerificationAction; +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const verification_setup_policy_1 = __nccwpck_require__(65326); +const native_verification_protocol_1 = __nccwpck_require__(88709); +exports.NATIVE_VERIFICATION_ACTION = '.git/copilot-verification-setup'; +function targetPath(workspace) { + const metadata = (0, node_path_1.join)(workspace, '.git'); + const stat = (0, node_fs_1.lstatSync)(metadata); + if (!stat.isDirectory() || stat.isSymbolicLink() || (0, node_fs_1.realpathSync)(metadata) !== metadata) + throw new verification_setup_policy_1.VerificationSetupError('native-workflow-required'); + return (0, node_path_1.join)(workspace, exports.NATIVE_VERIFICATION_ACTION); +} +/** Native composite metadata must remain available until its deferred post steps have finished. */ +function stageNativeVerificationAction(setup, session) { + const target = targetPath(setup.workspace); + const record = (0, native_verification_protocol_1.readNativeRecord)(session, 'staging.json'); + if (record && record.target !== target || !record && !!(0, node_fs_1.lstatSync)(target, { throwIfNoEntry: false })) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + (0, native_verification_protocol_1.writeNativeRecord)(session, 'staging.json', { target }); + (0, node_fs_1.rmSync)(target, { recursive: true, force: true }); + (0, node_fs_1.mkdirSync)(target, { mode: 0o700 }); + for (const file of setup.files) { + const destination = (0, node_path_1.join)(target, file.path); + const remainder = (0, node_path_1.relative)(target, destination); + if (!remainder || remainder.startsWith(`..${node_path_1.sep}`) || remainder === '..') + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(destination), { recursive: true, mode: 0o700 }); + (0, node_fs_1.writeFileSync)(destination, Buffer.from(file.base64, 'base64'), { mode: file.executable ? 0o700 : 0o600, flag: 'wx' }); + } +} +function cleanupNativeVerificationAction(session, workspace) { + const record = (0, native_verification_protocol_1.readNativeRecord)(session, 'staging.json'); + if (!record) + return; + const target = targetPath(workspace); + if (record.target !== target) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + (0, node_fs_1.rmSync)(target, { recursive: true, force: true }); + (0, node_fs_1.rmSync)((0, node_path_1.join)(session, 'staging.json'), { force: true }); +} + + /***/ }), /***/ 35849: @@ -103036,6 +103016,39 @@ function prepareUntrustedCommandEnvironment(source = process.env, platform = pro } +/***/ }), + +/***/ 20878: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.verificationExecutable = verificationExecutable; +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +/** @actions/exec resolves basenames against its own process PATH; native tools belong to the verifier PATH. */ +function verificationExecutable(program, environment, platform = process.platform) { + const explicit = (0, node_path_1.isAbsolute)(program) || /[\\/]/u.test(program); + const candidates = explicit ? [(0, node_path_1.resolve)(program)] : (environment.PATH ?? '').split(node_path_1.delimiter).filter(Boolean).map(directory => (0, node_path_1.resolve)((0, node_path_1.join)(directory, program))); + const windowsExtensions = (environment.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean); + const hasWindowsExtension = windowsExtensions.some(extension => program.toLowerCase().endsWith(extension.toLowerCase())); + const extensions = platform === 'win32' && !hasWindowsExtension ? windowsExtensions : ['']; + for (const candidate of candidates) + for (const extension of extensions) { + const file = candidate + extension; + try { + if ((0, node_fs_1.statSync)(file).isFile()) { + (0, node_fs_1.accessSync)(file, node_fs_1.constants.X_OK); + return file; + } + } + catch { /* Try only the next executable in the isolated PATH. */ } + } + throw new Error('Verification executable is unavailable in the isolated PATH.'); +} + + /***/ }), /***/ 91053: @@ -103241,6 +103254,7 @@ function applyVerificationSetupResult(file, environment, roots) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.prepareRepositoryVerification = prepareRepositoryVerification; +const verification_setup_snapshot_1 = __nccwpck_require__(62148); const native_verification_profiles_1 = __nccwpck_require__(22290); const node_fs_1 = __nccwpck_require__(87561); const node_path_1 = __nccwpck_require__(49411); @@ -103249,7 +103263,7 @@ const verification_setup_policy_1 = __nccwpck_require__(65326); const verification_setup_result_1 = __nccwpck_require__(23358); const logging_ports_1 = __nccwpck_require__(6152); const native_verification_protocol_1 = __nccwpck_require__(88709); -const native_verification_overlay_1 = __nccwpck_require__(64378); +const native_verification_staging_1 = __nccwpck_require__(63587); const native_verification_candidate_1 = __nccwpck_require__(18883); /** The runner executes YAML. This adapter pauses the admitted call stack at its verification boundary. */ async function prepareRepositoryVerification(setup, environment) { @@ -103270,9 +103284,9 @@ async function prepareRepositoryVerification(setup, environment) { throw new verification_setup_policy_1.VerificationSetupError('workspace-changed'); (0, native_verification_profiles_1.registerNativeVerificationProfile)(session, environment.HOME); const identity = (0, native_verification_candidate_1.verificationCandidateIdentity)(setup.workspace, environment); - (0, native_verification_overlay_1.stageNativeVerificationAction)(setup, session); + (0, native_verification_staging_1.stageNativeVerificationAction)(setup, session); const id = (0, node_crypto_1.randomUUID)(); - (0, native_verification_protocol_1.writeNativeRecord)(session, 'request.json', { id, sourceSha: setup.sourceSha, environment }); + (0, native_verification_protocol_1.writeNativeRecord)(session, 'request.json', { id, sourceSha: setup.sourceSha, digest: (0, verification_setup_snapshot_1.verificationSetupDigest)(setup), environment }); (0, logging_ports_1.logInfo)(`Preparing repository verification (${verification_setup_policy_1.VERIFICATION_SETUP_PATH}; trusted source SHA: ${setup.sourceSha}).`); const deadline = Date.now() + verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.timeoutMs; let response; @@ -103283,7 +103297,6 @@ async function prepareRepositoryVerification(setup, environment) { throw new verification_setup_policy_1.VerificationSetupError('timeout'); await (0, native_verification_protocol_1.nativePause)(100); } - (0, native_verification_overlay_1.restoreNativeVerificationAction)(session, setup.workspace); if (response.id !== id || response.success !== true) throw new verification_setup_policy_1.VerificationSetupError('process-failed'); if ((0, native_verification_candidate_1.verificationCandidateIdentity)(setup.workspace, environment) !== identity) @@ -103307,7 +103320,6 @@ async function prepareRepositoryVerification(setup, environment) { throw new verification_setup_policy_1.VerificationSetupError(error instanceof verification_setup_policy_1.VerificationSetupError ? error.reason : 'handoff-invalid', setup.sourceSha); } finally { - (0, native_verification_overlay_1.restoreNativeVerificationAction)(session, setup.workspace); (0, node_fs_1.rmSync)((0, node_path_1.join)(session, 'request.json'), { force: true }); } } @@ -103321,7 +103333,10 @@ async function prepareRepositoryVerification(setup, environment) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.verificationSetupDigest = verificationSetupDigest; exports.captureVerificationSetup = captureVerificationSetup; +exports.captureVerificationSetupAtCommit = captureVerificationSetupAtCommit; +const node_path_1 = __nccwpck_require__(49411); const node_child_process_1 = __nccwpck_require__(17718); const node_crypto_1 = __nccwpck_require__(6005); const node_fs_1 = __nccwpck_require__(87561); @@ -103330,7 +103345,17 @@ const verification_setup_ports_1 = __nccwpck_require__(26509); const verification_setup_policy_1 = __nccwpck_require__(65326); const untrusted_command_environment_1 = __nccwpck_require__(2304); /** Only immutable Git objects are read. Errors remain deferred on non-verifying routes. */ +function verificationSetupDigest(setup) { + return (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(setup.files)).digest('hex'); +} function captureVerificationSetup(enabled, mainBranch, root = process.cwd()) { + return capture(enabled, mainBranch, root); +} +/** Post steps use the already pinned commit from runner state, never a mutable branch ref. */ +function captureVerificationSetupAtCommit(commit, root = process.cwd(), gitExecutable) { + return capture(true, '', root, commit, gitExecutable); +} +function capture(enabled, mainBranch, root, pinned, gitExecutable) { if (!enabled) return verification_setup_ports_1.DISABLED_VERIFICATION_SETUP; let sourceSha; @@ -103339,18 +103364,20 @@ function captureVerificationSetup(enabled, mainBranch, root = process.cwd()) { try { runtime = (0, untrusted_command_environment_1.prepareUntrustedCommandEnvironment)(); const environment = runtime.environment; - if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/u.test(mainBranch) || mainBranch.includes('..') - || mainBranch.includes('//') || mainBranch.endsWith('/') || mainBranch.endsWith('.lock')) + if (!pinned && (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/u.test(mainBranch) || mainBranch.includes('..') + || mainBranch.includes('//') || mainBranch.endsWith('/') || mainBranch.endsWith('.lock'))) + throw new Error(); + if (gitExecutable !== undefined && !(0, node_path_1.isAbsolute)(gitExecutable)) throw new Error(); const workspace = (0, node_fs_1.realpathSync)(root); const git = (...args) => { - const result = (0, node_child_process_1.spawnSync)('git', ['--no-replace-objects', ...args], { cwd: workspace, env: environment, + const result = (0, node_child_process_1.spawnSync)(gitExecutable ?? 'git', ['--no-replace-objects', ...args], { cwd: workspace, env: environment, shell: false, windowsHide: true, timeout: 5000, maxBuffer: verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.snapshotBytes + 4096 }); if (result.error || result.status !== 0) throw new Error(); return result.stdout; }; - const sha = git('rev-parse', '--verify', `refs/remotes/origin/${mainBranch}^{commit}`).toString('utf8').trim(); + const sha = pinned ?? git('rev-parse', '--verify', `refs/remotes/origin/${mainBranch}^{commit}`).toString('utf8').trim(); if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(sha)) throw new Error(); sourceSha = sha; diff --git a/build/github_action/index.js b/build/github_action/index.js index 5f78df7ea..9bab40461 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -87600,6 +87600,7 @@ var __importStar = (this && this.__importStar) || (function () { })(); Object.defineProperty(exports, "__esModule", ({ value: true })); exports.GitCommitAdapter = void 0; +const verification_executable_1 = __nccwpck_require__(20878); const native_verification_profiles_1 = __nccwpck_require__(22290); const exec = __importStar(__nccwpck_require__(18538)); const git_authentication_environment_1 = __nccwpck_require__(16535); @@ -87610,7 +87611,7 @@ const verification_setup_ports_1 = __nccwpck_require__(26509); const verification_setup_runner_1 = __nccwpck_require__(11709); class GitCommitAdapter { constructor(executeCommand = (program, args, options) => options - ? exec.exec(program, args, { + ? exec.exec(options.resolveFromEnvironment && options.env ? (0, verification_executable_1.verificationExecutable)(program, options.env) : program, args, { // Captured Git reads can contain NUL-delimited records that // hide subsequent lines in GitHub's Actions log viewer. ...(options.stdout ? { silent: true } : {}), @@ -87638,6 +87639,7 @@ class GitCommitAdapter { return await this.executeCommand(program, args, { ...(options.stdout ? { stdout: options.stdout } : {}), env: runtime.environment, + ...(options.verifyRuntimePrerequisites && this.verificationSetup.kind === 'ready' ? { resolveFromEnvironment: true } : {}), }); } finally { @@ -88436,6 +88438,8 @@ exports.awaitNativeBoundary = awaitNativeBoundary; exports.continueNativeVerification = continueNativeVerification; exports.cleanupNativeVerification = cleanupNativeVerification; exports.runNativeVerificationEntry = runNativeVerificationEntry; +const native_verification_source_runtime_1 = __nccwpck_require__(85290); +const verification_executable_1 = __nccwpck_require__(20878); const native_verification_redaction_1 = __nccwpck_require__(27632); const native_verification_profiles_1 = __nccwpck_require__(22290); const core = __importStar(__nccwpck_require__(75855)); @@ -88447,7 +88451,8 @@ const agent_trusted_system_tools_1 = __nccwpck_require__(76049); const github_action_input_1 = __nccwpck_require__(57581); const verification_setup_policy_1 = __nccwpck_require__(65326); const native_verification_protocol_1 = __nccwpck_require__(88709); -const native_verification_overlay_1 = __nccwpck_require__(64378); +const native_verification_staging_1 = __nccwpck_require__(63587); +const verification_setup_snapshot_1 = __nccwpck_require__(62148); function emitWorkerOutput(root) { const file = (0, node_path_1.join)(root, 'worker-output'); if (process.env.GITHUB_OUTPUT && (0, node_fs_1.existsSync)(file)) @@ -88523,9 +88528,13 @@ async function awaitNativeBoundary(root, timeoutMs = 7200000) { const request = (0, native_verification_protocol_1.readNativeRecord)(root, 'request.json', 32768); if (request) { if (!/^[a-f0-9-]{36}$/u.test(request.id) || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(request.sourceSha) - || !request.environment || typeof request.environment.HOME !== 'string') + || !/^[a-f0-9]{64}$/u.test(request.digest) || !request.environment || typeof request.environment.HOME !== 'string') throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); core.setOutput('verification-request', request.id); + core.setOutput('verification-source', request.sourceSha); + core.setOutput('verification-digest', request.digest); + core.saveState('native-verification-digest', request.digest); + core.saveState('native-verification-source', request.sourceSha); for (const key of ['HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'PATH', 'JAVA_HOME', 'TMP', 'TEMP', 'TMPDIR']) { const value = request.environment[key] ?? ''; if (typeof value !== 'string' || /[\r\n\0]/u.test(value)) @@ -88578,7 +88587,8 @@ async function cleanupNativeVerification(root, waitMs = 10000, deferProfileClean catch { /* Worker may have exited while cancellation completed. */ } } } - (0, native_verification_overlay_1.restoreNativeVerificationAction)(root, session.workspace); + if (!deferProfileCleanup) + (0, native_verification_staging_1.cleanupNativeVerificationAction)(root, session.workspace); drainNativeWorkerLog(root, true); if (!deferProfileCleanup) { (0, native_verification_profiles_1.removeNativeVerificationProfiles)(root); @@ -88588,6 +88598,9 @@ async function cleanupNativeVerification(root, waitMs = 10000, deferProfileClean async function startNativeVerification() { for (const secret of (0, native_verification_redaction_1.nativeWorkerSecrets)(process.env)) core.setSecret(secret); + const git = (0, verification_executable_1.verificationExecutable)('git', { PATH: process.env.PATH ?? '', PATHEXT: process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM' }); + core.saveState('native-verification-git', git); + core.setOutput('verification-git', git); const root = (0, node_fs_1.realpathSync)((0, agent_private_install_root_1.createPrivateVerificationRuntimeRoot)()); for (const name of ['worker-output', 'worker-env', 'worker-path', 'worker-log', 'worker-summary']) (0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, name), '', { flag: 'wx', mode: 0o600 }); @@ -88601,6 +88614,7 @@ async function startNativeVerification() { (0, native_verification_protocol_1.writeNativeRecord)(root, 'session.json', { schemaVersion: 1, workspace: (0, node_fs_1.realpathSync)(process.cwd()), run: process.env.GITHUB_RUN_ID ?? '', attempt: process.env.GITHUB_RUN_ATTEMPT ?? '', job: process.env.GITHUB_JOB ?? '', pid: worker.pid }); core.saveState('native-verification-ticket', root); + core.saveState('native-verification-owner', 'true'); core.setOutput('verification-ticket', root); await awaitNativeBoundary(root); } @@ -88620,11 +88634,30 @@ async function runNativeVerificationEntry(run) { const post = core.getState('copilot-native-post'); if (post === 'true') { const root = core.getState('native-verification-ticket'); - if (root) + if (root && core.getState('native-verification-owner') === 'true') await cleanupNativeVerification(root); + else if (root && (0, node_fs_1.existsSync)(root) && core.getState('native-verification-source')) { + const session = (0, native_verification_protocol_1.nativeSession)(root); + try { + const snapshot = (0, verification_setup_snapshot_1.captureVerificationSetupAtCommit)(core.getState('native-verification-source'), session.workspace, core.getState('native-verification-git')); + if (snapshot.kind !== 'ready' || (0, verification_setup_snapshot_1.verificationSetupDigest)(snapshot) !== core.getState('native-verification-digest')) + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + (0, native_verification_staging_1.stageNativeVerificationAction)(snapshot, root); + } + catch { + // A later native post must fail to load rather than execute candidate-modified metadata. + (0, native_verification_staging_1.cleanupNativeVerificationAction)(root, session.workspace); + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + } + } + const sourceRuntime = core.getState('native-source-runtime'); + if (sourceRuntime) + (0, native_verification_source_runtime_1.cleanupNativeSourceRuntime)(sourceRuntime, (0, node_fs_1.realpathSync)(process.cwd())); return; } core.saveState('copilot-native-post', 'true'); + if (process.env.COPILOT_NATIVE_PHASE === 'prepare' && process.env.COPILOT_NATIVE_RUNTIME_CACHE) + core.saveState('native-source-runtime', process.env.COPILOT_NATIVE_RUNTIME_CACHE); const worker = process.env.COPILOT_NATIVE_WORKER_SESSION; if (worker) { const deadline = Date.now() + 30000; @@ -88659,6 +88692,11 @@ async function runNativeVerificationEntry(run) { const root = process.env.COPILOT_NATIVE_TICKET; if (!root) throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + (0, native_verification_protocol_1.nativeSession)(root); + core.saveState('native-verification-ticket', root); + core.saveState('native-verification-source', process.env.COPILOT_NATIVE_SOURCE ?? ''); + core.saveState('native-verification-digest', process.env.COPILOT_NATIVE_DIGEST ?? ''); + core.saveState('native-verification-git', process.env.COPILOT_NATIVE_GIT ?? ''); if (phase === 'continue') { await continueNativeVerification(root, process.env.COPILOT_NATIVE_REQUEST ?? '', process.env.COPILOT_NATIVE_OUTCOME ?? ''); return; @@ -88680,82 +88718,6 @@ async function runNativeVerificationEntry(run) { } -/***/ }), - -/***/ 64378: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { - -"use strict"; - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.stageNativeVerificationAction = stageNativeVerificationAction; -exports.restoreNativeVerificationAction = restoreNativeVerificationAction; -const node_fs_1 = __nccwpck_require__(87561); -const node_path_1 = __nccwpck_require__(49411); -const verification_setup_policy_1 = __nccwpck_require__(65326); -const native_verification_protocol_1 = __nccwpck_require__(88709); -/** A temporary reviewed action overlays the candidate path only while the operation is paused. */ -function stageNativeVerificationAction(setup, session) { - const target = (0, node_path_1.join)(setup.workspace, verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY); - const backup = (0, node_path_1.join)(session, 'candidate-action'); - const created = []; - let parent = setup.workspace; - for (const part of verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY.split('/').slice(0, -1)) { - parent = (0, node_path_1.join)(parent, part); - if ((0, node_fs_1.existsSync)(parent)) { - if ((0, node_fs_1.lstatSync)(parent).isSymbolicLink() || !(0, node_fs_1.lstatSync)(parent).isDirectory() || (0, node_fs_1.realpathSync)(parent) !== parent) - throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); - } - else { - (0, node_fs_1.mkdirSync)(parent, { mode: 0o700 }); - created.push(parent); - } - } - const targetStat = (0, node_fs_1.lstatSync)(target, { throwIfNoEntry: false }); - const existed = !!targetStat; - if (targetStat && (targetStat.isSymbolicLink() || !targetStat.isDirectory())) - throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); - const record = { target, backup, existed, created }; - (0, native_verification_protocol_1.writeNativeRecord)(session, 'overlay.json', record); - if (existed) - (0, node_fs_1.renameSync)(target, backup); - (0, node_fs_1.mkdirSync)(target, { mode: 0o700 }); - for (const file of setup.files) { - const destination = (0, node_path_1.join)(target, file.path); - const remainder = (0, node_path_1.relative)(target, destination); - if (!remainder || remainder.startsWith(`..${node_path_1.sep}`) || remainder === '..') - throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); - (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(destination), { recursive: true, mode: 0o700 }); - (0, node_fs_1.writeFileSync)(destination, Buffer.from(file.base64, 'base64'), { mode: file.executable ? 0o700 : 0o600, flag: 'wx' }); - } -} -function restoreNativeVerificationAction(session, workspace) { - const record = (0, native_verification_protocol_1.readNativeRecord)(session, 'overlay.json'); - if (!record) - return; - if (record.target !== (0, node_path_1.join)(workspace, verification_setup_policy_1.VERIFICATION_SETUP_DIRECTORY) || record.backup !== (0, node_path_1.join)(session, 'candidate-action') - || !Array.isArray(record.created) || record.created.some(path => ![(0, node_path_1.join)(workspace, '.github'), (0, node_path_1.join)(workspace, '.github/actions')].includes(path))) - throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); - for (const parent of [(0, node_path_1.join)(workspace, '.github'), (0, node_path_1.join)(workspace, '.github/actions')]) { - if (!(0, node_fs_1.existsSync)(parent) || (0, node_fs_1.lstatSync)(parent).isSymbolicLink() || !(0, node_fs_1.lstatSync)(parent).isDirectory() || (0, node_fs_1.realpathSync)(parent) !== parent) - throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); - } - // A backup is the authoritative original. A second cleanup must never remove it. - if (record.existed && !(0, node_fs_1.existsSync)(record.backup)) - return; - (0, node_fs_1.rmSync)(record.target, { recursive: true, force: true }); - if (record.existed) - (0, node_fs_1.renameSync)(record.backup, record.target); - for (const directory of [...record.created].reverse()) { - try { - (0, node_fs_1.rmdirSync)(directory); - } - catch { /* Other candidate content remains. */ } - } - (0, node_fs_1.rmSync)((0, node_path_1.join)(session, 'overlay.json'), { force: true }); -} - - /***/ }), /***/ 22290: @@ -88768,6 +88730,7 @@ exports.nativeVerificationProfilePaths = nativeVerificationProfilePaths; exports.registerNativeVerificationProfile = registerNativeVerificationProfile; exports.nativeVerificationRetainsProfile = nativeVerificationRetainsProfile; exports.removeNativeVerificationProfiles = removeNativeVerificationProfiles; +const node_crypto_1 = __nccwpck_require__(6005); const node_fs_1 = __nccwpck_require__(87561); const node_path_1 = __nccwpck_require__(49411); const verification_setup_policy_1 = __nccwpck_require__(65326); @@ -88787,7 +88750,7 @@ function registerNativeVerificationProfile(root, profile) { || profile === root || !/^copilot-verify-runtime-[A-Za-z0-9]+$/u.test((0, node_path_1.basename)(profile)) || (0, node_fs_1.realpathSync)(profile) !== profile || !(0, node_fs_1.lstatSync)(profile).isDirectory() || (0, node_fs_1.lstatSync)(profile).isSymbolicLink()) throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); - (0, node_fs_1.writeFileSync)((0, node_path_1.join)(profile, 'native-session-owner'), root, { mode: 0o600, flag: 'wx' }); + (0, node_fs_1.writeFileSync)((0, node_path_1.join)(profile, 'native-session-owner'), (0, node_crypto_1.createHash)('sha256').update(root).digest('hex'), { mode: 0o600, flag: 'wx' }); (0, native_verification_protocol_1.writeNativeRecord)(root, 'profiles.json', [...previous, profile]); } function nativeVerificationRetainsProfile(profile) { @@ -88805,7 +88768,7 @@ function removeNativeVerificationProfiles(root) { continue; throw error; } - if (!stat.isDirectory() || stat.isSymbolicLink() || (0, node_fs_1.realpathSync)(path) !== path || (0, node_fs_1.readFileSync)((0, node_path_1.join)(path, 'native-session-owner'), 'utf8') !== root) + if (!stat.isDirectory() || stat.isSymbolicLink() || (0, node_fs_1.realpathSync)(path) !== path || (0, node_fs_1.readFileSync)((0, node_path_1.join)(path, 'native-session-owner'), 'utf8') !== (0, node_crypto_1.createHash)('sha256').update(root).digest('hex')) throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); (0, node_fs_1.rmSync)(path, { recursive: true, force: true }); } @@ -88945,6 +88908,91 @@ function redactNativeWorkerLogs(environment = process.env) { } +/***/ }), + +/***/ 85290: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.cleanupNativeSourceRuntime = cleanupNativeSourceRuntime; +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const verification_setup_policy_1 = __nccwpck_require__(65326); +/** Only source workflows use this snapshot; installed remote Actions already live outside the candidate. */ +function cleanupNativeSourceRuntime(root, workspace) { + if (!root || !(0, node_fs_1.existsSync)(root)) + return; + const allowed = ['.copilot-native-runtime', '.copilot-native-fixture-runtime'].map(name => (0, node_path_1.join)((0, node_path_1.dirname)(workspace), name)); + if (!allowed.includes(root) || (0, node_fs_1.lstatSync)(root).isSymbolicLink() || (0, node_fs_1.realpathSync)(root) !== root) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + let receipt; + try { + receipt = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(root, '.copilot-runtime-snapshot.json'), 'utf8')); + } + catch { + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + } + if (receipt?.kind !== 'copilot-native-runtime' || receipt.workspace !== workspace) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + (0, node_fs_1.rmSync)(root, { recursive: true, force: true }); +} + + +/***/ }), + +/***/ 63587: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.NATIVE_VERIFICATION_ACTION = void 0; +exports.stageNativeVerificationAction = stageNativeVerificationAction; +exports.cleanupNativeVerificationAction = cleanupNativeVerificationAction; +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +const verification_setup_policy_1 = __nccwpck_require__(65326); +const native_verification_protocol_1 = __nccwpck_require__(88709); +exports.NATIVE_VERIFICATION_ACTION = '.git/copilot-verification-setup'; +function targetPath(workspace) { + const metadata = (0, node_path_1.join)(workspace, '.git'); + const stat = (0, node_fs_1.lstatSync)(metadata); + if (!stat.isDirectory() || stat.isSymbolicLink() || (0, node_fs_1.realpathSync)(metadata) !== metadata) + throw new verification_setup_policy_1.VerificationSetupError('native-workflow-required'); + return (0, node_path_1.join)(workspace, exports.NATIVE_VERIFICATION_ACTION); +} +/** Native composite metadata must remain available until its deferred post steps have finished. */ +function stageNativeVerificationAction(setup, session) { + const target = targetPath(setup.workspace); + const record = (0, native_verification_protocol_1.readNativeRecord)(session, 'staging.json'); + if (record && record.target !== target || !record && !!(0, node_fs_1.lstatSync)(target, { throwIfNoEntry: false })) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + (0, native_verification_protocol_1.writeNativeRecord)(session, 'staging.json', { target }); + (0, node_fs_1.rmSync)(target, { recursive: true, force: true }); + (0, node_fs_1.mkdirSync)(target, { mode: 0o700 }); + for (const file of setup.files) { + const destination = (0, node_path_1.join)(target, file.path); + const remainder = (0, node_path_1.relative)(target, destination); + if (!remainder || remainder.startsWith(`..${node_path_1.sep}`) || remainder === '..') + throw new verification_setup_policy_1.VerificationSetupError('helper-invalid'); + (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(destination), { recursive: true, mode: 0o700 }); + (0, node_fs_1.writeFileSync)(destination, Buffer.from(file.base64, 'base64'), { mode: file.executable ? 0o700 : 0o600, flag: 'wx' }); + } +} +function cleanupNativeVerificationAction(session, workspace) { + const record = (0, native_verification_protocol_1.readNativeRecord)(session, 'staging.json'); + if (!record) + return; + const target = targetPath(workspace); + if (record.target !== target) + throw new verification_setup_policy_1.VerificationSetupError('handoff-invalid'); + (0, node_fs_1.rmSync)(target, { recursive: true, force: true }); + (0, node_fs_1.rmSync)((0, node_path_1.join)(session, 'staging.json'), { force: true }); +} + + /***/ }), /***/ 35849: @@ -89617,6 +89665,39 @@ function prepareUntrustedCommandEnvironment(source = process.env, platform = pro } +/***/ }), + +/***/ 20878: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.verificationExecutable = verificationExecutable; +const node_fs_1 = __nccwpck_require__(87561); +const node_path_1 = __nccwpck_require__(49411); +/** @actions/exec resolves basenames against its own process PATH; native tools belong to the verifier PATH. */ +function verificationExecutable(program, environment, platform = process.platform) { + const explicit = (0, node_path_1.isAbsolute)(program) || /[\\/]/u.test(program); + const candidates = explicit ? [(0, node_path_1.resolve)(program)] : (environment.PATH ?? '').split(node_path_1.delimiter).filter(Boolean).map(directory => (0, node_path_1.resolve)((0, node_path_1.join)(directory, program))); + const windowsExtensions = (environment.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean); + const hasWindowsExtension = windowsExtensions.some(extension => program.toLowerCase().endsWith(extension.toLowerCase())); + const extensions = platform === 'win32' && !hasWindowsExtension ? windowsExtensions : ['']; + for (const candidate of candidates) + for (const extension of extensions) { + const file = candidate + extension; + try { + if ((0, node_fs_1.statSync)(file).isFile()) { + (0, node_fs_1.accessSync)(file, node_fs_1.constants.X_OK); + return file; + } + } + catch { /* Try only the next executable in the isolated PATH. */ } + } + throw new Error('Verification executable is unavailable in the isolated PATH.'); +} + + /***/ }), /***/ 91053: @@ -89822,6 +89903,7 @@ function applyVerificationSetupResult(file, environment, roots) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.prepareRepositoryVerification = prepareRepositoryVerification; +const verification_setup_snapshot_1 = __nccwpck_require__(62148); const native_verification_profiles_1 = __nccwpck_require__(22290); const node_fs_1 = __nccwpck_require__(87561); const node_path_1 = __nccwpck_require__(49411); @@ -89830,7 +89912,7 @@ const verification_setup_policy_1 = __nccwpck_require__(65326); const verification_setup_result_1 = __nccwpck_require__(23358); const logging_ports_1 = __nccwpck_require__(6152); const native_verification_protocol_1 = __nccwpck_require__(88709); -const native_verification_overlay_1 = __nccwpck_require__(64378); +const native_verification_staging_1 = __nccwpck_require__(63587); const native_verification_candidate_1 = __nccwpck_require__(18883); /** The runner executes YAML. This adapter pauses the admitted call stack at its verification boundary. */ async function prepareRepositoryVerification(setup, environment) { @@ -89851,9 +89933,9 @@ async function prepareRepositoryVerification(setup, environment) { throw new verification_setup_policy_1.VerificationSetupError('workspace-changed'); (0, native_verification_profiles_1.registerNativeVerificationProfile)(session, environment.HOME); const identity = (0, native_verification_candidate_1.verificationCandidateIdentity)(setup.workspace, environment); - (0, native_verification_overlay_1.stageNativeVerificationAction)(setup, session); + (0, native_verification_staging_1.stageNativeVerificationAction)(setup, session); const id = (0, node_crypto_1.randomUUID)(); - (0, native_verification_protocol_1.writeNativeRecord)(session, 'request.json', { id, sourceSha: setup.sourceSha, environment }); + (0, native_verification_protocol_1.writeNativeRecord)(session, 'request.json', { id, sourceSha: setup.sourceSha, digest: (0, verification_setup_snapshot_1.verificationSetupDigest)(setup), environment }); (0, logging_ports_1.logInfo)(`Preparing repository verification (${verification_setup_policy_1.VERIFICATION_SETUP_PATH}; trusted source SHA: ${setup.sourceSha}).`); const deadline = Date.now() + verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.timeoutMs; let response; @@ -89864,7 +89946,6 @@ async function prepareRepositoryVerification(setup, environment) { throw new verification_setup_policy_1.VerificationSetupError('timeout'); await (0, native_verification_protocol_1.nativePause)(100); } - (0, native_verification_overlay_1.restoreNativeVerificationAction)(session, setup.workspace); if (response.id !== id || response.success !== true) throw new verification_setup_policy_1.VerificationSetupError('process-failed'); if ((0, native_verification_candidate_1.verificationCandidateIdentity)(setup.workspace, environment) !== identity) @@ -89888,7 +89969,6 @@ async function prepareRepositoryVerification(setup, environment) { throw new verification_setup_policy_1.VerificationSetupError(error instanceof verification_setup_policy_1.VerificationSetupError ? error.reason : 'handoff-invalid', setup.sourceSha); } finally { - (0, native_verification_overlay_1.restoreNativeVerificationAction)(session, setup.workspace); (0, node_fs_1.rmSync)((0, node_path_1.join)(session, 'request.json'), { force: true }); } } @@ -89902,7 +89982,10 @@ async function prepareRepositoryVerification(setup, environment) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.verificationSetupDigest = verificationSetupDigest; exports.captureVerificationSetup = captureVerificationSetup; +exports.captureVerificationSetupAtCommit = captureVerificationSetupAtCommit; +const node_path_1 = __nccwpck_require__(49411); const node_child_process_1 = __nccwpck_require__(17718); const node_crypto_1 = __nccwpck_require__(6005); const node_fs_1 = __nccwpck_require__(87561); @@ -89911,7 +89994,17 @@ const verification_setup_ports_1 = __nccwpck_require__(26509); const verification_setup_policy_1 = __nccwpck_require__(65326); const untrusted_command_environment_1 = __nccwpck_require__(2304); /** Only immutable Git objects are read. Errors remain deferred on non-verifying routes. */ +function verificationSetupDigest(setup) { + return (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(setup.files)).digest('hex'); +} function captureVerificationSetup(enabled, mainBranch, root = process.cwd()) { + return capture(enabled, mainBranch, root); +} +/** Post steps use the already pinned commit from runner state, never a mutable branch ref. */ +function captureVerificationSetupAtCommit(commit, root = process.cwd(), gitExecutable) { + return capture(true, '', root, commit, gitExecutable); +} +function capture(enabled, mainBranch, root, pinned, gitExecutable) { if (!enabled) return verification_setup_ports_1.DISABLED_VERIFICATION_SETUP; let sourceSha; @@ -89920,18 +90013,20 @@ function captureVerificationSetup(enabled, mainBranch, root = process.cwd()) { try { runtime = (0, untrusted_command_environment_1.prepareUntrustedCommandEnvironment)(); const environment = runtime.environment; - if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/u.test(mainBranch) || mainBranch.includes('..') - || mainBranch.includes('//') || mainBranch.endsWith('/') || mainBranch.endsWith('.lock')) + if (!pinned && (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/u.test(mainBranch) || mainBranch.includes('..') + || mainBranch.includes('//') || mainBranch.endsWith('/') || mainBranch.endsWith('.lock'))) + throw new Error(); + if (gitExecutable !== undefined && !(0, node_path_1.isAbsolute)(gitExecutable)) throw new Error(); const workspace = (0, node_fs_1.realpathSync)(root); const git = (...args) => { - const result = (0, node_child_process_1.spawnSync)('git', ['--no-replace-objects', ...args], { cwd: workspace, env: environment, + const result = (0, node_child_process_1.spawnSync)(gitExecutable ?? 'git', ['--no-replace-objects', ...args], { cwd: workspace, env: environment, shell: false, windowsHide: true, timeout: 5000, maxBuffer: verification_setup_policy_1.VERIFICATION_SETUP_LIMITS.snapshotBytes + 4096 }); if (result.error || result.status !== 0) throw new Error(); return result.stdout; }; - const sha = git('rev-parse', '--verify', `refs/remotes/origin/${mainBranch}^{commit}`).toString('utf8').trim(); + const sha = pinned ?? git('rev-parse', '--verify', `refs/remotes/origin/${mainBranch}^{commit}`).toString('utf8').trim(); if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(sha)) throw new Error(); sourceSha = sha; diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index dbe8a494e..63611b0c4 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -644,18 +644,25 @@ actor, fork, native-head and workspace gates decide whether verification is reac Branch synchronization and shared automated commit preflight request the same `untrusted + verifyRuntimePrerequisites` port intent. The Git adapter creates a fresh private HOME. Its infrastructure pauses the admitted same-job worker exactly -before preflight, stages the trusted action and lets GitHub Actions execute the -native YAML. Continuation matches the operation/request/run/job, restores candidate -action files, validates candidate identity and bounded runtime directories, then -runs the existing probes and verifier. The agent and publication are not replayed. - -Pure policy owns limits, schemas and sanitized errors. Snapshot, overlay, protocol, +before preflight, stages the trusted action in local Git metadata and lets GitHub Actions execute the +native YAML. Continuation matches the operation/request/run/job, validates the unchanged candidate identity and bounded runtime directories, then +resolves executables against the private runtime PATH and runs the existing probes +and verifier. Staging survives native post steps; trusted Copilot post phases +use the initially pinned absolute Git executable to reload pinned Git objects and verify their original digest before native post +execution, then the initial Action post step cleans staging and profiles. The agent and publication are not replayed. + +Pure policy owns limits, schemas and sanitized errors. Snapshot, staging, protocol, profile and controller adapters own Git/filesystem/process details; application use cases retain mutation policy. Native action post steps can use dependency-only profiles until the original Action's final post cleanup removes them. They are never reused by later commands. Native step failure/cancellation blocks publication and triggers existing recovery. A final cleanup guard stops a leftover worker. +Source callbacks first copy the Action manifest and runtime outside the mutable +workspace, so rebuilding a candidate cannot replace continuation or post code. +Remote installed Actions already use the runner action cache. Only native callback +phases register active Copilot post behavior. + The workflow generator emits twenty conditional setup/continuation slots and a completion step in each actual mutation callback. Structural validation compares the complete native contract, including timeout and runtime-only setup environment. diff --git a/docs/security-operations/operations/troubleshooting.mdx b/docs/security-operations/operations/troubleshooting.mdx index 6b60450d7..b48a1c140 100644 --- a/docs/security-operations/operations/troubleshooting.mdx +++ b/docs/security-operations/operations/troubleshooting.mdx @@ -595,7 +595,7 @@ run, then repair the reviewed action on `main-branch`. | --- | --- | | `source-unavailable` | Make the trusted origin ref available in the full checkout. | | `helper-invalid` | Review the fixed native composite and regular auxiliary files; respect file/step bounds and avoid nested local `uses`. | -| `native-workflow-required` | Update generated callbacks and run in GitHub Actions. The local CLI cannot execute composite actions. | +| `native-workflow-required` | Update generated callbacks and use their normal directory-based Git checkout in GitHub Actions. The local CLI cannot execute composite actions. | | `workspace-changed` / `candidate-changed` | Keep setup dependency-only; do not change publishable source, index, branch or lockfiles. Retry from a fresh candidate. | | `process-failed` | Inspect the native action's logs and fix its reviewed dependency installation. | | `timeout` | Fit each native setup within five minutes; no limit override or automatic retry exists. | diff --git a/docs/security-operations/operations/verification.mdx b/docs/security-operations/operations/verification.mdx index 11bd4c1bb..2b2f080e2 100644 --- a/docs/security-operations/operations/verification.mdx +++ b/docs/security-operations/operations/verification.mdx @@ -150,8 +150,15 @@ before candidate changes. The trusted origin ref must be available locally; generated comment workflows use a full checkout. The runner executes that snapshot **after merge/fix changes and before each configured verifier**, reading the actual candidate manifests. The admitted operation continues once; the agent is not rerun. -Copilot temporarily overlays the action directory with the reviewed snapshot and -restores the candidate's original files before verification or publication. +Copilot stages the reviewed snapshot in reserved local Git metadata at +`.git/copilot-verification-setup`; the consumer's candidate files stay unchanged. +The normal directory-based `.git` checkout from generated workflows is required. +Staging remains available for native post steps. Trusted Copilot post steps reload +the pinned Git objects and verify their original content digest before those +phases with the initially pinned Git executable, so later candidate edits cannot +supply post code. Source workflows also snapshot Copilot itself outside the +mutable workspace; normal remote Action installations already use a separate +runner cache. For Python, the consumer file can contain: @@ -161,11 +168,13 @@ runs: using: composite steps: - uses: actions/setup-python@v6 + id: python with: + update-environment: false python-version: '3.12' - name: Install the candidate's test dependencies run: | - python - <<'PYTHON' + "$PYTHON_EXECUTABLE" - <<'PYTHON' import os, pathlib, subprocess, venv home = pathlib.Path(os.environ['HOME']) / 'venv' venv.create(home, with_pip=True) @@ -177,6 +186,8 @@ runs: print(tools, file=paths) PYTHON shell: bash + env: + PYTHON_EXECUTABLE: ${{ steps.python.outputs.python-path }} ``` Keep pytest and all transitive dependencies pinned with hashes in the candidate's @@ -206,15 +217,15 @@ runs: The action may include reviewed auxiliary scripts, referenced through `${{ github.action_path }}`. Use native external `uses` actions and `run` steps; nested local `uses: ./...` actions are rejected because their deferred post steps -could otherwise load restored candidate code. Review external action versions as +could otherwise load candidate code outside trusted staging. Review external action versions as part of the executable setup. The composite inherits the job's operating system; its metadata cannot declare jobs, runners or a matrix. Each command gets a **fresh private HOME**. These dependency-only profiles stay private until native post-job actions finish, then Copilot's final post cleanup removes them. They are never reused by another command. Keep dependencies that -must survive restoration under the private HOME, an ignored candidate directory -or the trusted runner tool cache, rather than inside the temporary action folder. +must survive native post steps under the private HOME, an ignored candidate directory +or the trusted runner tool cache, rather than inside the reserved action staging folder. Native setup exports tools through normal Actions mechanisms such as `GITHUB_PATH` and `JAVA_HOME`; no custom JSON helper protocol is required. Copilot accepts up to @@ -245,7 +256,8 @@ pinned trusted SHA, never copied raw setup output. Setup must install dependencies without changing publishable code or lockfiles. Copilot compares HEAD, branch, index, tracked edits and publishable untracked file -contents before allowing the verifier to continue. Observations are bounded to +contents before allowing the verifier to continue. Executable lookup uses that verifier's +validated PATH, so its private Python/pnpm takes precedence over the worker tools. Observations are bounded to 16 MiB, 256 untracked files and 16 MiB aggregate untracked contents. Ignore dependency caches and keep source changes within these bounds. Failure, changed candidate, invalid runtime output, timeout or cancellation withholds verification and push; @@ -255,6 +267,11 @@ Read-only questions/status, dry runs, already-aligned branches and empty command lists execute no setup. Snapshot failures remain deferred until verification. Setup/install/update preserves this consumer-owned directory and all its assets. +If a runner crash leaves an unknown reserved staging directory, preparation fails +closed rather than overwriting it. Confirm the prior job has stopped, inspect its +run, and remove only that stale `.git/copilot-verification-setup` directory before +retrying. Normal completion/cancellation uses the Action post cleanup. + Native composite execution is available in the generated **GitHub Actions** callbacks. The local CLI cannot execute `uses` steps: opted-in local verification fails with `native-workflow-required`. Run the authorized operation in Actions, diff --git a/scripts/coverage-budgets.json b/scripts/coverage-budgets.json index ed140a380..784bc5834 100644 --- a/scripts/coverage-budgets.json +++ b/scripts/coverage-budgets.json @@ -919,10 +919,12 @@ "src/infrastructure/verification_setup_runner.ts", "src/infrastructure/native_verification_protocol.ts", "src/infrastructure/native_verification_candidate.ts", - "src/infrastructure/native_verification_overlay.ts", + "src/infrastructure/native_verification_staging.ts", "src/infrastructure/native_verification_controller.ts", "src/infrastructure/native_verification_profiles.ts", - "src/infrastructure/native_verification_redaction.ts" + "src/infrastructure/native_verification_redaction.ts", + "src/infrastructure/verification_executable.ts", + "src/infrastructure/native_verification_source_runtime.ts" ], "mode": "aggregate", "thresholdProfile": "default" diff --git a/scripts/generate-native-verification-workflows.cjs b/scripts/generate-native-verification-workflows.cjs index cf3811fcb..b8110af05 100644 --- a/scripts/generate-native-verification-workflows.cjs +++ b/scripts/generate-native-verification-workflows.cjs @@ -3,7 +3,8 @@ const path = require('node:path'); const yaml = require('js-yaml'); const MAX_REQUESTS = 20; -const ACTION = './.github/actions/copilot-verification-setup'; +const ACTION = './.git/copilot-verification-setup'; +const { SOURCE_ACTION } = require('./prepare-native-source-action.cjs'); const MARKER = ' # copilot:native-verification:start'; const CALLBACKS = ['copilot_issue_comment.yml', 'copilot_pull_request_comment.yml']; @@ -20,14 +21,14 @@ function phaseSteps(action, inputAlias = 'copilot-inputs', id = 'copilot') { env: Object.fromEntries(['HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'PATH', 'JAVA_HOME', 'TMP', 'TEMP', 'TMPDIR'].map(key => [key, output(key.toLowerCase())])) }); steps.push({ name: `Continue repository verification ${count}`, id: continuation, if: `\${{ always() && ${pending} }}`, uses: action, with: { alias: inputAlias }, - env: { COPILOT_NATIVE_PHASE: 'continue', COPILOT_NATIVE_TICKET: `\${{ steps.${id}.outputs.verification-ticket }}`, + env: { COPILOT_NATIVE_PHASE: 'continue', COPILOT_NATIVE_SOURCE: `\${{ steps.${id}.outputs.verification-source }}`, COPILOT_NATIVE_DIGEST: `\${{ steps.${id}.outputs.verification-digest }}`, COPILOT_NATIVE_GIT: `\${{ steps.${id}.outputs.verification-git }}`, COPILOT_NATIVE_TICKET: `\${{ steps.${id}.outputs.verification-ticket }}`, COPILOT_NATIVE_REQUEST: `\${{ steps.${previous}.outputs.verification-request }}`, COPILOT_NATIVE_OUTCOME: `\${{ steps.${setup}.outcome }}` } }); previous = continuation; } steps.push({ name: 'Complete repository verification', id: 'verification_complete', if: `\${{ always() && steps.${id}.outputs.verification-ticket != '' }}`, uses: action, with: { alias: inputAlias }, - env: { COPILOT_NATIVE_PHASE: 'finish', COPILOT_NATIVE_TICKET: `\${{ steps.${id}.outputs.verification-ticket }}` } }); + env: { COPILOT_NATIVE_PHASE: 'finish', COPILOT_NATIVE_SOURCE: `\${{ steps.${id}.outputs.verification-source }}`, COPILOT_NATIVE_DIGEST: `\${{ steps.${id}.outputs.verification-digest }}`, COPILOT_NATIVE_GIT: `\${{ steps.${id}.outputs.verification-git }}`, COPILOT_NATIVE_TICKET: `\${{ steps.${id}.outputs.verification-ticket }}` } }); return steps; } @@ -39,7 +40,11 @@ function renderSteps(action, alias = 'copilot-inputs', id = 'copilot') { function generate(file) { let text = fs.readFileSync(file, 'utf8').split(MARKER)[0].trimEnd() + '\n'; - const action = file.startsWith('setup/') ? 'vypdev/copilot@v3' : './'; + const action = file.startsWith('setup/') ? 'vypdev/copilot@v3' : SOURCE_ACTION; + if (!file.startsWith('setup/')) { + text = text.replace(' - uses: ./\n', ` - name: Snapshot the Copilot runtime before candidate changes\n id: prepare_native_runtime\n run: node scripts/prepare-native-source-action.cjs\n shell: bash\n\n - uses: ${SOURCE_ACTION}\n`); + if (!text.includes(' COPILOT_NATIVE_RUNTIME_CACHE:')) text = text.replace(' COPILOT_NATIVE_PHASE: prepare\n', ' COPILOT_NATIVE_PHASE: prepare\n COPILOT_NATIVE_RUNTIME_CACHE: ${{ steps.prepare_native_runtime.outputs.path }}\n'); + } const start = ` - uses: ${action}\n`; text = text.replace(/(?: id: copilot\n)+/g, ''); if (text.includes(start)) text = text.replace(start, ` - uses: ${action}\n id: copilot\n`); @@ -55,4 +60,4 @@ function generate(file) { } if (require.main === module) for (const directory of ['.github/workflows', 'setup/workflows']) for (const file of CALLBACKS) generate(path.join(directory, file)); -module.exports = { MAX_REQUESTS, ACTION, CALLBACKS, phaseSteps, renderSteps }; +module.exports = { MAX_REQUESTS, ACTION, SOURCE_ACTION, CALLBACKS, phaseSteps, renderSteps }; diff --git a/scripts/prepare-native-source-action.cjs b/scripts/prepare-native-source-action.cjs new file mode 100644 index 000000000..8863d6c79 --- /dev/null +++ b/scripts/prepare-native-source-action.cjs @@ -0,0 +1,42 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const crypto = require('node:crypto'); + +const SOURCE_ACTION = './../.copilot-native-runtime'; +const FIXTURE_ACTION = './../.copilot-native-fixture-runtime'; +const RECEIPT = '.copilot-runtime-snapshot.json'; + +function prepareNativeSourceAction(workspace, fixture = false) { + workspace = fs.realpathSync(workspace); + const target = path.resolve(workspace, fixture ? FIXTURE_ACTION : SOURCE_ACTION); + const receipt = path.join(target, RECEIPT); + if (fs.existsSync(target)) { + const stat = fs.lstatSync(target); + if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('Native runtime snapshot path is not an owned directory.'); + let previous; + try { previous = JSON.parse(fs.readFileSync(receipt, 'utf8')); } catch { throw new Error('Native runtime snapshot directory is not owned by Copilot.'); } + if (previous?.kind !== 'copilot-native-runtime' || previous.workspace !== workspace) throw new Error('Native runtime snapshot ownership does not match.'); + fs.rmSync(target, { recursive: true, force: true }); + } + fs.mkdirSync(target, { mode: 0o700 }); + const source = fixture ? path.join(workspace, '.github/actions/copilot-native-fixture') : workspace; + const files = fixture ? ['action.yml', 'dist/index.js'] : ['action.yml', 'build/github_action/index.js']; + const hashes = {}; + for (const file of files) { + const from = path.join(source, file); + if (!fs.lstatSync(from).isFile() || fs.lstatSync(from).isSymbolicLink()) throw new Error('Native runtime source must be a regular file.'); + const data = fs.readFileSync(from); + fs.mkdirSync(path.dirname(path.join(target, file)), { recursive: true, mode: 0o700 }); + fs.writeFileSync(path.join(target, file), data, { mode: 0o600, flag: 'wx' }); + hashes[file] = crypto.createHash('sha256').update(data).digest('hex'); + } + if (!fixture && fs.existsSync(path.join(workspace, 'setup'))) fs.cpSync(path.join(workspace, 'setup'), path.join(target, 'setup'), { recursive: true, dereference: false }); + fs.writeFileSync(receipt, JSON.stringify({ kind: 'copilot-native-runtime', workspace, hashes }), { mode: 0o600, flag: 'wx' }); + return target; +} + +if (require.main === module) { + const target = prepareNativeSourceAction(process.cwd(), process.argv.includes('--fixture')); + if (process.env.GITHUB_OUTPUT) fs.appendFileSync(process.env.GITHUB_OUTPUT, `path=${target}\n`); +} +module.exports = { prepareNativeSourceAction, SOURCE_ACTION, FIXTURE_ACTION }; diff --git a/scripts/validate-workflow-contract.cjs b/scripts/validate-workflow-contract.cjs index 47073e9cf..93bd54194 100644 --- a/scripts/validate-workflow-contract.cjs +++ b/scripts/validate-workflow-contract.cjs @@ -144,9 +144,15 @@ function relativeWorkflow(file) { return path.relative(repositoryRoot, file).replaceAll(path.sep, '/'); } +function expectedCopilotAction(relativeFile) { + if (relativeFile.startsWith('setup/workflows/')) return DISTRIBUTED_COPILOT_ACTION; + const native = require('./generate-native-verification-workflows.cjs'); + return native.CALLBACKS.includes(path.basename(relativeFile)) ? native.SOURCE_ACTION : './'; +} + function isCopilotAction(step) { return typeof step?.uses === 'string' - && (step.uses === './' || /(?:^|\/)copilot@/.test(step.uses)); + && (step.uses === './' || step.uses === './../.copilot-native-runtime' || /(?:^|\/)copilot@/.test(step.uses)); } function shouldPersistCheckoutCredentials(relativeFile, jobId) { @@ -165,7 +171,7 @@ function assertMajorActionReferences(file, workflow) { .filter(value => typeof value === 'string'); for (const uses of actionUses) { if (isCopilotAction({ uses })) { - const expected = relativeFile.startsWith('setup/workflows/') ? DISTRIBUTED_COPILOT_ACTION : './'; + const expected = expectedCopilotAction(relativeFile); if (uses !== expected) { throw new Error(`${relativeFile} job ${jobId} must invoke Copilot with ${expected}.`); } @@ -1369,7 +1375,7 @@ function assertSequentialMutationWorkflow(file, workflow) { function assertIssueWorkflowProfileInput(file, workflow) { if (!ISSUE_WORKFLOW_PROFILE_FILES.has(path.basename(file))) return; const relativeFile = relativeWorkflow(file); - const expectedAction = relativeFile.startsWith('setup/workflows/') ? DISTRIBUTED_COPILOT_ACTION : './'; + const expectedAction = expectedCopilotAction(relativeFile); const actionSteps = Object.values(workflow.jobs ?? {}).flatMap(job => job?.steps ?? []) .filter(step => step?.uses === expectedAction); if (actionSteps.length === 0 || actionSteps.some(step => @@ -1406,12 +1412,17 @@ function assertNativeVerificationWorkflow(file, workflow) { const contract = require('./generate-native-verification-workflows.cjs'); if (!contract.CALLBACKS.includes(path.basename(file))) return; const relativeFile = relativeWorkflow(file); - const action = relativeFile.startsWith('setup/workflows/') ? DISTRIBUTED_COPILOT_ACTION : './'; + const action = expectedCopilotAction(relativeFile); const jobs = Object.values(workflow.jobs ?? {}).filter(job => (job.steps ?? []).some(step => step.id === 'copilot')); if (jobs.length !== 1) throw new Error(`${relativeFile} must have exactly one native verification operation.`); const steps = jobs[0].steps; const start = steps.findIndex(step => step.id === 'copilot'); const original = steps[start]; + if (!relativeFile.startsWith('setup/workflows/')) { + const snapshot = steps[start - 1]; + if (snapshot?.id !== 'prepare_native_runtime' || snapshot.run !== 'node scripts/prepare-native-source-action.cjs' + || original.env?.COPILOT_NATIVE_RUNTIME_CACHE !== '${{ steps.prepare_native_runtime.outputs.path }}') throw new Error(`${relativeFile} must snapshot its Action runtime before candidate changes.`); + } if (original.uses !== action || original.env?.COPILOT_NATIVE_PHASE !== 'prepare') throw new Error(`${relativeFile} must prepare the admitted native operation.`); const expected = contract.phaseSteps(action).map(step => ({ ...step, ...(step.with ? { with: original.with } : {}) })); const actual = steps.slice(start + 1); diff --git a/setup/workflows/copilot_issue_comment.yml b/setup/workflows/copilot_issue_comment.yml index 0cd6df82b..ea8c42d0a 100644 --- a/setup/workflows/copilot_issue_comment.yml +++ b/setup/workflows/copilot_issue_comment.yml @@ -121,7 +121,7 @@ jobs: - name: Prepare repository verification 1 id: verification_setup_1 if: ${{ !cancelled() && steps.copilot.outcome == 'success' && steps.copilot.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.copilot.outputs.verification-home }} @@ -140,13 +140,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.copilot.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_1.outcome }} - name: Prepare repository verification 2 id: verification_setup_2 if: ${{ !cancelled() && steps.verification_continue_1.outcome == 'success' && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_1.outputs.verification-home }} @@ -165,13 +168,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_1.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_2.outcome }} - name: Prepare repository verification 3 id: verification_setup_3 if: ${{ !cancelled() && steps.verification_continue_2.outcome == 'success' && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_2.outputs.verification-home }} @@ -190,13 +196,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_2.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_3.outcome }} - name: Prepare repository verification 4 id: verification_setup_4 if: ${{ !cancelled() && steps.verification_continue_3.outcome == 'success' && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_3.outputs.verification-home }} @@ -215,13 +224,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_3.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_4.outcome }} - name: Prepare repository verification 5 id: verification_setup_5 if: ${{ !cancelled() && steps.verification_continue_4.outcome == 'success' && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_4.outputs.verification-home }} @@ -240,13 +252,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_4.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_5.outcome }} - name: Prepare repository verification 6 id: verification_setup_6 if: ${{ !cancelled() && steps.verification_continue_5.outcome == 'success' && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_5.outputs.verification-home }} @@ -265,13 +280,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_5.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_6.outcome }} - name: Prepare repository verification 7 id: verification_setup_7 if: ${{ !cancelled() && steps.verification_continue_6.outcome == 'success' && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_6.outputs.verification-home }} @@ -290,13 +308,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_6.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_7.outcome }} - name: Prepare repository verification 8 id: verification_setup_8 if: ${{ !cancelled() && steps.verification_continue_7.outcome == 'success' && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_7.outputs.verification-home }} @@ -315,13 +336,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_7.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_8.outcome }} - name: Prepare repository verification 9 id: verification_setup_9 if: ${{ !cancelled() && steps.verification_continue_8.outcome == 'success' && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_8.outputs.verification-home }} @@ -340,13 +364,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_8.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_9.outcome }} - name: Prepare repository verification 10 id: verification_setup_10 if: ${{ !cancelled() && steps.verification_continue_9.outcome == 'success' && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_9.outputs.verification-home }} @@ -365,13 +392,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_9.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_10.outcome }} - name: Prepare repository verification 11 id: verification_setup_11 if: ${{ !cancelled() && steps.verification_continue_10.outcome == 'success' && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_10.outputs.verification-home }} @@ -390,13 +420,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_10.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_11.outcome }} - name: Prepare repository verification 12 id: verification_setup_12 if: ${{ !cancelled() && steps.verification_continue_11.outcome == 'success' && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_11.outputs.verification-home }} @@ -415,13 +448,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_11.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_12.outcome }} - name: Prepare repository verification 13 id: verification_setup_13 if: ${{ !cancelled() && steps.verification_continue_12.outcome == 'success' && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_12.outputs.verification-home }} @@ -440,13 +476,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_12.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_13.outcome }} - name: Prepare repository verification 14 id: verification_setup_14 if: ${{ !cancelled() && steps.verification_continue_13.outcome == 'success' && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_13.outputs.verification-home }} @@ -465,13 +504,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_13.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_14.outcome }} - name: Prepare repository verification 15 id: verification_setup_15 if: ${{ !cancelled() && steps.verification_continue_14.outcome == 'success' && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_14.outputs.verification-home }} @@ -490,13 +532,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_14.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_15.outcome }} - name: Prepare repository verification 16 id: verification_setup_16 if: ${{ !cancelled() && steps.verification_continue_15.outcome == 'success' && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_15.outputs.verification-home }} @@ -515,13 +560,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_15.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_16.outcome }} - name: Prepare repository verification 17 id: verification_setup_17 if: ${{ !cancelled() && steps.verification_continue_16.outcome == 'success' && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_16.outputs.verification-home }} @@ -540,13 +588,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_16.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_17.outcome }} - name: Prepare repository verification 18 id: verification_setup_18 if: ${{ !cancelled() && steps.verification_continue_17.outcome == 'success' && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_17.outputs.verification-home }} @@ -565,13 +616,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_17.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_18.outcome }} - name: Prepare repository verification 19 id: verification_setup_19 if: ${{ !cancelled() && steps.verification_continue_18.outcome == 'success' && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_18.outputs.verification-home }} @@ -590,13 +644,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_18.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_19.outcome }} - name: Prepare repository verification 20 id: verification_setup_20 if: ${{ !cancelled() && steps.verification_continue_19.outcome == 'success' && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_19.outputs.verification-home }} @@ -615,6 +672,9 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_19.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_20.outcome }} @@ -625,5 +685,8 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: finish + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} # copilot:native-verification:end diff --git a/setup/workflows/copilot_pull_request_comment.yml b/setup/workflows/copilot_pull_request_comment.yml index a9dc43339..c5e05f433 100644 --- a/setup/workflows/copilot_pull_request_comment.yml +++ b/setup/workflows/copilot_pull_request_comment.yml @@ -121,7 +121,7 @@ jobs: - name: Prepare repository verification 1 id: verification_setup_1 if: ${{ !cancelled() && steps.copilot.outcome == 'success' && steps.copilot.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.copilot.outputs.verification-home }} @@ -140,13 +140,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.copilot.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_1.outcome }} - name: Prepare repository verification 2 id: verification_setup_2 if: ${{ !cancelled() && steps.verification_continue_1.outcome == 'success' && steps.verification_continue_1.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_1.outputs.verification-home }} @@ -165,13 +168,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_1.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_2.outcome }} - name: Prepare repository verification 3 id: verification_setup_3 if: ${{ !cancelled() && steps.verification_continue_2.outcome == 'success' && steps.verification_continue_2.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_2.outputs.verification-home }} @@ -190,13 +196,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_2.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_3.outcome }} - name: Prepare repository verification 4 id: verification_setup_4 if: ${{ !cancelled() && steps.verification_continue_3.outcome == 'success' && steps.verification_continue_3.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_3.outputs.verification-home }} @@ -215,13 +224,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_3.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_4.outcome }} - name: Prepare repository verification 5 id: verification_setup_5 if: ${{ !cancelled() && steps.verification_continue_4.outcome == 'success' && steps.verification_continue_4.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_4.outputs.verification-home }} @@ -240,13 +252,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_4.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_5.outcome }} - name: Prepare repository verification 6 id: verification_setup_6 if: ${{ !cancelled() && steps.verification_continue_5.outcome == 'success' && steps.verification_continue_5.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_5.outputs.verification-home }} @@ -265,13 +280,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_5.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_6.outcome }} - name: Prepare repository verification 7 id: verification_setup_7 if: ${{ !cancelled() && steps.verification_continue_6.outcome == 'success' && steps.verification_continue_6.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_6.outputs.verification-home }} @@ -290,13 +308,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_6.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_7.outcome }} - name: Prepare repository verification 8 id: verification_setup_8 if: ${{ !cancelled() && steps.verification_continue_7.outcome == 'success' && steps.verification_continue_7.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_7.outputs.verification-home }} @@ -315,13 +336,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_7.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_8.outcome }} - name: Prepare repository verification 9 id: verification_setup_9 if: ${{ !cancelled() && steps.verification_continue_8.outcome == 'success' && steps.verification_continue_8.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_8.outputs.verification-home }} @@ -340,13 +364,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_8.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_9.outcome }} - name: Prepare repository verification 10 id: verification_setup_10 if: ${{ !cancelled() && steps.verification_continue_9.outcome == 'success' && steps.verification_continue_9.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_9.outputs.verification-home }} @@ -365,13 +392,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_9.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_10.outcome }} - name: Prepare repository verification 11 id: verification_setup_11 if: ${{ !cancelled() && steps.verification_continue_10.outcome == 'success' && steps.verification_continue_10.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_10.outputs.verification-home }} @@ -390,13 +420,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_10.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_11.outcome }} - name: Prepare repository verification 12 id: verification_setup_12 if: ${{ !cancelled() && steps.verification_continue_11.outcome == 'success' && steps.verification_continue_11.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_11.outputs.verification-home }} @@ -415,13 +448,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_11.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_12.outcome }} - name: Prepare repository verification 13 id: verification_setup_13 if: ${{ !cancelled() && steps.verification_continue_12.outcome == 'success' && steps.verification_continue_12.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_12.outputs.verification-home }} @@ -440,13 +476,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_12.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_13.outcome }} - name: Prepare repository verification 14 id: verification_setup_14 if: ${{ !cancelled() && steps.verification_continue_13.outcome == 'success' && steps.verification_continue_13.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_13.outputs.verification-home }} @@ -465,13 +504,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_13.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_14.outcome }} - name: Prepare repository verification 15 id: verification_setup_15 if: ${{ !cancelled() && steps.verification_continue_14.outcome == 'success' && steps.verification_continue_14.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_14.outputs.verification-home }} @@ -490,13 +532,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_14.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_15.outcome }} - name: Prepare repository verification 16 id: verification_setup_16 if: ${{ !cancelled() && steps.verification_continue_15.outcome == 'success' && steps.verification_continue_15.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_15.outputs.verification-home }} @@ -515,13 +560,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_15.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_16.outcome }} - name: Prepare repository verification 17 id: verification_setup_17 if: ${{ !cancelled() && steps.verification_continue_16.outcome == 'success' && steps.verification_continue_16.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_16.outputs.verification-home }} @@ -540,13 +588,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_16.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_17.outcome }} - name: Prepare repository verification 18 id: verification_setup_18 if: ${{ !cancelled() && steps.verification_continue_17.outcome == 'success' && steps.verification_continue_17.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_17.outputs.verification-home }} @@ -565,13 +616,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_17.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_18.outcome }} - name: Prepare repository verification 19 id: verification_setup_19 if: ${{ !cancelled() && steps.verification_continue_18.outcome == 'success' && steps.verification_continue_18.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_18.outputs.verification-home }} @@ -590,13 +644,16 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_18.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_19.outcome }} - name: Prepare repository verification 20 id: verification_setup_20 if: ${{ !cancelled() && steps.verification_continue_19.outcome == 'success' && steps.verification_continue_19.outputs.verification-setup-pending == 'true' }} - uses: ./.github/actions/copilot-verification-setup + uses: ./.git/copilot-verification-setup timeout-minutes: 5 env: HOME: ${{ steps.verification_continue_19.outputs.verification-home }} @@ -615,6 +672,9 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: continue + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} COPILOT_NATIVE_REQUEST: ${{ steps.verification_continue_19.outputs.verification-request }} COPILOT_NATIVE_OUTCOME: ${{ steps.verification_setup_20.outcome }} @@ -625,5 +685,8 @@ jobs: with: *copilot-inputs env: COPILOT_NATIVE_PHASE: finish + COPILOT_NATIVE_SOURCE: ${{ steps.copilot.outputs.verification-source }} + COPILOT_NATIVE_DIGEST: ${{ steps.copilot.outputs.verification-digest }} + COPILOT_NATIVE_GIT: ${{ steps.copilot.outputs.verification-git }} COPILOT_NATIVE_TICKET: ${{ steps.copilot.outputs.verification-ticket }} # copilot:native-verification:end diff --git a/specs/CATALOG.md b/specs/CATALOG.md index eebafa586..1500839c4 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -24,7 +24,7 @@ debt or convert unknown historic intent into a design decision. | `managed-issue-lifecycle` | As-built baseline | Convert typed issues into traceable work branches, project state, and lifecycle state | [Managed issue and branch lifecycle](./managed-issue-and-branch-lifecycle.md) | 71 paths · 2026-10-20 | | `comment-automation` | Implemented | Admit only explicit commands or exact mentions, then route them while protecting repository mutations | [Comment automation and authorization](./comment-automation-and-authorization.md) | 79 paths · 2026-10-11 | | `bugbot-analysis-and-autofix` | Implemented | Select one canonical PR, exhaustively analyze its bounded diff partitions, publish stable findings atomically, and apply authorized verified fixes | [Bugbot analysis, finding publication, and autofix](./bugbot-analysis-publication-and-autofix.md) + 2 companion | 109 paths · 2026-10-13 | -| `branch-synchronization` | Implemented | Observe parent drift with one localized status card and transition-only notifications, then safely merge a parent branch into a linked working branch | [Branch synchronization and conflict recovery](./branch-synchronization-and-conflict-recovery.md) + 2 companion | 113 paths · 2026-10-21 | +| `branch-synchronization` | Implemented | Observe parent drift with one localized status card and transition-only notifications, then safely merge a parent branch into a linked working branch | [Branch synchronization and conflict recovery](./branch-synchronization-and-conflict-recovery.md) + 2 companion | 119 paths · 2026-10-21 | | `pull-request-lifecycle` | Implemented | Enrich linked and unlinked pull requests with safe issue linkage, projects, metadata, reviewers, concise descriptions, and distinct workflow evidence | [Pull request lifecycle and enrichment](./pull-request-lifecycle-and-enrichment.md) | 77 paths · 2026-10-20 | | `agent-runtime` | Implemented | Resolve, provision, authenticate, authorize, and execute only the agent roles reachable by a run | [Agent runtime, provider, model, and role routing](./agent-runtime-provider-and-model-routing.md) + 1 companion | 99 paths · 2026-10-17 | | `cli-and-single-actions` | As-built baseline | Expose bounded local commands and workflow-dispatched operations through the shared application core | [CLI and single-action execution](./cli-and-single-action-execution.md) | 44 paths · 2026-10-19 | @@ -195,8 +195,8 @@ debt or convert unknown historic intent into a design decision. - Specifications: [`specs/branch-synchronization-and-conflict-recovery.md`](./branch-synchronization-and-conflict-recovery.md) · [`specs/repository-verification-runtime-prerequisites.md`](./repository-verification-runtime-prerequisites.md) · [`specs/repository-verification-setup-hooks.md`](./repository-verification-setup-hooks.md) - Workflows: [`.github/workflows/copilot_branch_sync.yml`](../.github/workflows/copilot_branch_sync.yml) · [`setup/workflows/copilot_branch_sync.yml`](../setup/workflows/copilot_branch_sync.yml) · [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`setup/workflows/copilot_issue.yml`](../setup/workflows/copilot_issue.yml) · [`setup/workflows/copilot_issue_comment.yml`](../setup/workflows/copilot_issue_comment.yml) · [`setup/workflows/copilot_pull_request_comment.yml`](../setup/workflows/copilot_pull_request_comment.yml) - Entrypoints: [`src/application/usecases/branch_sync/sync_branch_use_case.ts`](../src/application/usecases/branch_sync/sync_branch_use_case.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) -- Core code: [`src/domain/branch_sync_command.ts`](../src/domain/branch_sync_command.ts) · [`src/application/ports/branch_sync_ports.ts`](../src/application/ports/branch_sync_ports.ts) · [`src/application/policies/branch_sync_message_catalog.ts`](../src/application/policies/branch_sync_message_catalog.ts) · [`src/application/policies/branch_sync_notification_policy.ts`](../src/application/policies/branch_sync_notification_policy.ts) · [`src/application/usecases/branch_sync/branch_sync_execution_policy.ts`](../src/application/usecases/branch_sync/branch_sync_execution_policy.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/application/usecases/steps/common/transition_notification_workflow.ts`](../src/application/usecases/steps/common/transition_notification_workflow.ts) · [`src/data/repository/branch_sync/branch_dependency_policy.ts`](../src/data/repository/branch_sync/branch_dependency_policy.ts) · [`src/data/repository/branch_sync/branch_dependency_repository.ts`](../src/data/repository/branch_sync/branch_dependency_repository.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/infrastructure/composition/push_single_action_capability_port_binding.ts`](../src/infrastructure/composition/push_single_action_capability_port_binding.ts) · [`src/infrastructure/branch_sync_workspace_adapter.ts`](../src/infrastructure/branch_sync_workspace_adapter.ts) · [`src/prompts/branch_sync_conflicts.ts`](../src/prompts/branch_sync_conflicts.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`src/infrastructure/branch_sync_sdd_catalog.ts`](../src/infrastructure/branch_sync_sdd_catalog.ts) · [`src/domain/branch_sync_validation.ts`](../src/domain/branch_sync_validation.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/infrastructure/branch_sync_communication_budget.ts`](../src/infrastructure/branch_sync_communication_budget.ts) · [`src/application/usecases/actions/retire_branch_sync_use_case.ts`](../src/application/usecases/actions/retire_branch_sync_use_case.ts) · [`src/application/ports/issue_completion_ports.ts`](../src/application/ports/issue_completion_ports.ts) · [`src/application/usecases/pull_request_workflow.ts`](../src/application/usecases/pull_request_workflow.ts) · [`src/application/usecases/pull_request_workflow_context.ts`](../src/application/usecases/pull_request_workflow_context.ts) · [`src/infrastructure/composition/pull_request_use_case_composition_root.ts`](../src/infrastructure/composition/pull_request_use_case_composition_root.ts) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) · [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`scripts/validate-workflow-contract.cjs`](../scripts/validate-workflow-contract.cjs) · [`src/application/ports/reply_request_read_ports.ts`](../src/application/ports/reply_request_read_ports.ts) · [`src/domain/reply_request_digest.ts`](../src/domain/reply_request_digest.ts) · [`src/infrastructure/composition/reply_request_composition_root.ts`](../src/infrastructure/composition/reply_request_composition_root.ts) · [`src/data/repository/reply_request_repository.ts`](../src/data/repository/reply_request_repository.ts) · [`src/application/policies/verification_runtime_policy.ts`](../src/application/policies/verification_runtime_policy.ts) · [`src/infrastructure/verification_runtime_manifest.ts`](../src/infrastructure/verification_runtime_manifest.ts) · [`src/infrastructure/verification_runtime_preflight.ts`](../src/infrastructure/verification_runtime_preflight.ts) · [`src/infrastructure/untrusted_command_environment.ts`](../src/infrastructure/untrusted_command_environment.ts) · [`src/application/ports/git_ports.ts`](../src/application/ports/git_ports.ts) · [`src/application/ports/bugbot_git_ports.ts`](../src/application/ports/bugbot_git_ports.ts) · [`src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts`](../src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts) · [`src/application/usecases/steps/commit/bugbot/verify_command_runner.ts`](../src/application/usecases/steps/commit/bugbot/verify_command_runner.ts) · [`src/domain/closed_issue_reconciliation.ts`](../src/domain/closed_issue_reconciliation.ts) · [`src/application/errors/closed_issue_reconciliation_error.ts`](../src/application/errors/closed_issue_reconciliation_error.ts) · [`src/application/policies/verification_setup_policy.ts`](../src/application/policies/verification_setup_policy.ts) · [`src/application/ports/verification_setup_ports.ts`](../src/application/ports/verification_setup_ports.ts) · [`src/infrastructure/verification_setup_snapshot.ts`](../src/infrastructure/verification_setup_snapshot.ts) · [`src/infrastructure/verification_setup_result.ts`](../src/infrastructure/verification_setup_result.ts) · [`src/infrastructure/verification_setup_runner.ts`](../src/infrastructure/verification_setup_runner.ts) · [`src/infrastructure/native_verification_protocol.ts`](../src/infrastructure/native_verification_protocol.ts) · [`src/infrastructure/native_verification_candidate.ts`](../src/infrastructure/native_verification_candidate.ts) · [`src/infrastructure/native_verification_overlay.ts`](../src/infrastructure/native_verification_overlay.ts) · [`src/infrastructure/native_verification_controller.ts`](../src/infrastructure/native_verification_controller.ts) · [`src/infrastructure/native_verification_profiles.ts`](../src/infrastructure/native_verification_profiles.ts) · [`src/infrastructure/native_verification_redaction.ts`](../src/infrastructure/native_verification_redaction.ts) · [`scripts/generate-native-verification-workflows.cjs`](../scripts/generate-native-verification-workflows.cjs) -- Tests: [`src/application/policies/__tests__/branch_sync_notification_policy.test.ts`](../src/application/policies/__tests__/branch_sync_notification_policy.test.ts) · [`src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts) · [`src/application/usecases/branch_sync/__tests__/sync_branch_use_case.test.ts`](../src/application/usecases/branch_sync/__tests__/sync_branch_use_case.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/application/usecases/steps/common/__tests__/transition_notification_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/transition_notification_workflow.test.ts) · [`src/data/repository/branch_sync/__tests__/branch_dependency_policy.test.ts`](../src/data/repository/branch_sync/__tests__/branch_dependency_policy.test.ts) · [`src/data/repository/branch_sync/__tests__/branch_dependency_repository.test.ts`](../src/data/repository/branch_sync/__tests__/branch_dependency_repository.test.ts) · [`src/infrastructure/__tests__/branch_sync_workspace_adapter.test.ts`](../src/infrastructure/__tests__/branch_sync_workspace_adapter.test.ts) · [`src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts) · [`src/application/usecases/branch_sync/__tests__/branch_sync_execution_policy.test.ts`](../src/application/usecases/branch_sync/__tests__/branch_sync_execution_policy.test.ts) · [`src/application/errors/__tests__/application_error.test.ts`](../src/application/errors/__tests__/application_error.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/infrastructure/__tests__/git_commit_adapter.test.ts`](../src/infrastructure/__tests__/git_commit_adapter.test.ts) · [`src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts`](../src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts) · [`src/infrastructure/__tests__/branch_sync_generated_bundle_validation.test.ts`](../src/infrastructure/__tests__/branch_sync_generated_bundle_validation.test.ts) · [`src/application/policies/__tests__/branch_sync_lifecycle_recovery.test.ts`](../src/application/policies/__tests__/branch_sync_lifecycle_recovery.test.ts) · [`src/infrastructure/__tests__/branch_sync_resolved_workspace.test.ts`](../src/infrastructure/__tests__/branch_sync_resolved_workspace.test.ts) · [`src/infrastructure/__tests__/branch_sync_communication_budget.test.ts`](../src/infrastructure/__tests__/branch_sync_communication_budget.test.ts) · [`src/application/usecases/actions/__tests__/retire_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/retire_branch_sync_use_case.test.ts) · [`src/application/usecases/__tests__/pull_request_use_case.test.ts`](../src/application/usecases/__tests__/pull_request_use_case.test.ts) · [`src/actions/__tests__/common_action.test.ts`](../src/actions/__tests__/common_action.test.ts) · [`src/actions/__tests__/github_action.test.ts`](../src/actions/__tests__/github_action.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) · [`src/data/repository/__tests__/reply_request_repository.test.ts`](../src/data/repository/__tests__/reply_request_repository.test.ts) · [`src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts`](../src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts) · [`src/application/policies/__tests__/verification_runtime_policy.test.ts`](../src/application/policies/__tests__/verification_runtime_policy.test.ts) · [`src/infrastructure/__tests__/verification_runtime_preflight.test.ts`](../src/infrastructure/__tests__/verification_runtime_preflight.test.ts) · [`src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts`](../src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts) · [`src/infrastructure/__tests__/untrusted_command_environment.test.ts`](../src/infrastructure/__tests__/untrusted_command_environment.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts) · [`src/tooling/__tests__/verification_runtime_guidance.test.ts`](../src/tooling/__tests__/verification_runtime_guidance.test.ts) · [`src/application/policies/__tests__/verification_setup_policy.test.ts`](../src/application/policies/__tests__/verification_setup_policy.test.ts) · [`src/infrastructure/__tests__/verification_setup_snapshot.test.ts`](../src/infrastructure/__tests__/verification_setup_snapshot.test.ts) · [`src/infrastructure/__tests__/verification_setup_runner.test.ts`](../src/infrastructure/__tests__/verification_setup_runner.test.ts) · [`src/infrastructure/__tests__/verification_setup_configuration.test.ts`](../src/infrastructure/__tests__/verification_setup_configuration.test.ts) · [`src/infrastructure/__tests__/verification_setup_sync.test.ts`](../src/infrastructure/__tests__/verification_setup_sync.test.ts) · [`src/infrastructure/__tests__/native_verification_runner.test.ts`](../src/infrastructure/__tests__/native_verification_runner.test.ts) · [`src/infrastructure/__tests__/native_verification_protocol.test.ts`](../src/infrastructure/__tests__/native_verification_protocol.test.ts) · [`src/infrastructure/__tests__/native_verification_controller.test.ts`](../src/infrastructure/__tests__/native_verification_controller.test.ts) · [`src/infrastructure/__tests__/native_verification_redaction.test.ts`](../src/infrastructure/__tests__/native_verification_redaction.test.ts) · [`src/infrastructure/__tests__/native_verification_workflow.test.ts`](../src/infrastructure/__tests__/native_verification_workflow.test.ts) · [`src/infrastructure/__tests__/native_verification_profiles.test.ts`](../src/infrastructure/__tests__/native_verification_profiles.test.ts) +- Core code: [`src/domain/branch_sync_command.ts`](../src/domain/branch_sync_command.ts) · [`src/application/ports/branch_sync_ports.ts`](../src/application/ports/branch_sync_ports.ts) · [`src/application/policies/branch_sync_message_catalog.ts`](../src/application/policies/branch_sync_message_catalog.ts) · [`src/application/policies/branch_sync_notification_policy.ts`](../src/application/policies/branch_sync_notification_policy.ts) · [`src/application/usecases/branch_sync/branch_sync_execution_policy.ts`](../src/application/usecases/branch_sync/branch_sync_execution_policy.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/application/usecases/steps/common/transition_notification_workflow.ts`](../src/application/usecases/steps/common/transition_notification_workflow.ts) · [`src/data/repository/branch_sync/branch_dependency_policy.ts`](../src/data/repository/branch_sync/branch_dependency_policy.ts) · [`src/data/repository/branch_sync/branch_dependency_repository.ts`](../src/data/repository/branch_sync/branch_dependency_repository.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/infrastructure/composition/push_single_action_capability_port_binding.ts`](../src/infrastructure/composition/push_single_action_capability_port_binding.ts) · [`src/infrastructure/branch_sync_workspace_adapter.ts`](../src/infrastructure/branch_sync_workspace_adapter.ts) · [`src/prompts/branch_sync_conflicts.ts`](../src/prompts/branch_sync_conflicts.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`src/infrastructure/branch_sync_sdd_catalog.ts`](../src/infrastructure/branch_sync_sdd_catalog.ts) · [`src/domain/branch_sync_validation.ts`](../src/domain/branch_sync_validation.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/infrastructure/branch_sync_communication_budget.ts`](../src/infrastructure/branch_sync_communication_budget.ts) · [`src/application/usecases/actions/retire_branch_sync_use_case.ts`](../src/application/usecases/actions/retire_branch_sync_use_case.ts) · [`src/application/ports/issue_completion_ports.ts`](../src/application/ports/issue_completion_ports.ts) · [`src/application/usecases/pull_request_workflow.ts`](../src/application/usecases/pull_request_workflow.ts) · [`src/application/usecases/pull_request_workflow_context.ts`](../src/application/usecases/pull_request_workflow_context.ts) · [`src/infrastructure/composition/pull_request_use_case_composition_root.ts`](../src/infrastructure/composition/pull_request_use_case_composition_root.ts) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) · [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`scripts/validate-workflow-contract.cjs`](../scripts/validate-workflow-contract.cjs) · [`src/application/ports/reply_request_read_ports.ts`](../src/application/ports/reply_request_read_ports.ts) · [`src/domain/reply_request_digest.ts`](../src/domain/reply_request_digest.ts) · [`src/infrastructure/composition/reply_request_composition_root.ts`](../src/infrastructure/composition/reply_request_composition_root.ts) · [`src/data/repository/reply_request_repository.ts`](../src/data/repository/reply_request_repository.ts) · [`src/application/policies/verification_runtime_policy.ts`](../src/application/policies/verification_runtime_policy.ts) · [`src/infrastructure/verification_runtime_manifest.ts`](../src/infrastructure/verification_runtime_manifest.ts) · [`src/infrastructure/verification_runtime_preflight.ts`](../src/infrastructure/verification_runtime_preflight.ts) · [`src/infrastructure/untrusted_command_environment.ts`](../src/infrastructure/untrusted_command_environment.ts) · [`src/application/ports/git_ports.ts`](../src/application/ports/git_ports.ts) · [`src/application/ports/bugbot_git_ports.ts`](../src/application/ports/bugbot_git_ports.ts) · [`src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts`](../src/application/usecases/steps/commit/bugbot/commit_and_push_preflight.ts) · [`src/application/usecases/steps/commit/bugbot/verify_command_runner.ts`](../src/application/usecases/steps/commit/bugbot/verify_command_runner.ts) · [`src/domain/closed_issue_reconciliation.ts`](../src/domain/closed_issue_reconciliation.ts) · [`src/application/errors/closed_issue_reconciliation_error.ts`](../src/application/errors/closed_issue_reconciliation_error.ts) · [`src/application/policies/verification_setup_policy.ts`](../src/application/policies/verification_setup_policy.ts) · [`src/application/ports/verification_setup_ports.ts`](../src/application/ports/verification_setup_ports.ts) · [`src/infrastructure/verification_setup_snapshot.ts`](../src/infrastructure/verification_setup_snapshot.ts) · [`src/infrastructure/verification_setup_result.ts`](../src/infrastructure/verification_setup_result.ts) · [`src/infrastructure/verification_setup_runner.ts`](../src/infrastructure/verification_setup_runner.ts) · [`src/infrastructure/native_verification_protocol.ts`](../src/infrastructure/native_verification_protocol.ts) · [`src/infrastructure/native_verification_candidate.ts`](../src/infrastructure/native_verification_candidate.ts) · [`src/infrastructure/native_verification_staging.ts`](../src/infrastructure/native_verification_staging.ts) · [`src/infrastructure/native_verification_controller.ts`](../src/infrastructure/native_verification_controller.ts) · [`src/infrastructure/native_verification_profiles.ts`](../src/infrastructure/native_verification_profiles.ts) · [`src/infrastructure/native_verification_redaction.ts`](../src/infrastructure/native_verification_redaction.ts) · [`scripts/generate-native-verification-workflows.cjs`](../scripts/generate-native-verification-workflows.cjs) · [`src/infrastructure/verification_executable.ts`](../src/infrastructure/verification_executable.ts) · [`src/infrastructure/native_verification_source_runtime.ts`](../src/infrastructure/native_verification_source_runtime.ts) · [`scripts/prepare-native-source-action.cjs`](../scripts/prepare-native-source-action.cjs) +- Tests: [`src/application/policies/__tests__/branch_sync_notification_policy.test.ts`](../src/application/policies/__tests__/branch_sync_notification_policy.test.ts) · [`src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts) · [`src/application/usecases/branch_sync/__tests__/sync_branch_use_case.test.ts`](../src/application/usecases/branch_sync/__tests__/sync_branch_use_case.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/application/usecases/steps/common/__tests__/transition_notification_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/transition_notification_workflow.test.ts) · [`src/data/repository/branch_sync/__tests__/branch_dependency_policy.test.ts`](../src/data/repository/branch_sync/__tests__/branch_dependency_policy.test.ts) · [`src/data/repository/branch_sync/__tests__/branch_dependency_repository.test.ts`](../src/data/repository/branch_sync/__tests__/branch_dependency_repository.test.ts) · [`src/infrastructure/__tests__/branch_sync_workspace_adapter.test.ts`](../src/infrastructure/__tests__/branch_sync_workspace_adapter.test.ts) · [`src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts) · [`src/application/usecases/branch_sync/__tests__/branch_sync_execution_policy.test.ts`](../src/application/usecases/branch_sync/__tests__/branch_sync_execution_policy.test.ts) · [`src/application/errors/__tests__/application_error.test.ts`](../src/application/errors/__tests__/application_error.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/infrastructure/__tests__/git_commit_adapter.test.ts`](../src/infrastructure/__tests__/git_commit_adapter.test.ts) · [`src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts`](../src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts) · [`src/infrastructure/__tests__/branch_sync_generated_bundle_validation.test.ts`](../src/infrastructure/__tests__/branch_sync_generated_bundle_validation.test.ts) · [`src/application/policies/__tests__/branch_sync_lifecycle_recovery.test.ts`](../src/application/policies/__tests__/branch_sync_lifecycle_recovery.test.ts) · [`src/infrastructure/__tests__/branch_sync_resolved_workspace.test.ts`](../src/infrastructure/__tests__/branch_sync_resolved_workspace.test.ts) · [`src/infrastructure/__tests__/branch_sync_communication_budget.test.ts`](../src/infrastructure/__tests__/branch_sync_communication_budget.test.ts) · [`src/application/usecases/actions/__tests__/retire_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/retire_branch_sync_use_case.test.ts) · [`src/application/usecases/__tests__/pull_request_use_case.test.ts`](../src/application/usecases/__tests__/pull_request_use_case.test.ts) · [`src/actions/__tests__/common_action.test.ts`](../src/actions/__tests__/common_action.test.ts) · [`src/actions/__tests__/github_action.test.ts`](../src/actions/__tests__/github_action.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) · [`src/data/repository/__tests__/reply_request_repository.test.ts`](../src/data/repository/__tests__/reply_request_repository.test.ts) · [`src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts`](../src/infrastructure/composition/__tests__/reply_request_composition_root.test.ts) · [`src/application/policies/__tests__/verification_runtime_policy.test.ts`](../src/application/policies/__tests__/verification_runtime_policy.test.ts) · [`src/infrastructure/__tests__/verification_runtime_preflight.test.ts`](../src/infrastructure/__tests__/verification_runtime_preflight.test.ts) · [`src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts`](../src/infrastructure/__tests__/verification_runtime_branch_sync.test.ts) · [`src/infrastructure/__tests__/untrusted_command_environment.test.ts`](../src/infrastructure/__tests__/untrusted_command_environment.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/verify_command_runner.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/commit_verification_workspace.test.ts) · [`src/tooling/__tests__/verification_runtime_guidance.test.ts`](../src/tooling/__tests__/verification_runtime_guidance.test.ts) · [`src/application/policies/__tests__/verification_setup_policy.test.ts`](../src/application/policies/__tests__/verification_setup_policy.test.ts) · [`src/infrastructure/__tests__/verification_setup_snapshot.test.ts`](../src/infrastructure/__tests__/verification_setup_snapshot.test.ts) · [`src/infrastructure/__tests__/verification_setup_runner.test.ts`](../src/infrastructure/__tests__/verification_setup_runner.test.ts) · [`src/infrastructure/__tests__/verification_setup_configuration.test.ts`](../src/infrastructure/__tests__/verification_setup_configuration.test.ts) · [`src/infrastructure/__tests__/verification_setup_sync.test.ts`](../src/infrastructure/__tests__/verification_setup_sync.test.ts) · [`src/infrastructure/__tests__/native_verification_runner.test.ts`](../src/infrastructure/__tests__/native_verification_runner.test.ts) · [`src/infrastructure/__tests__/native_verification_protocol.test.ts`](../src/infrastructure/__tests__/native_verification_protocol.test.ts) · [`src/infrastructure/__tests__/native_verification_controller.test.ts`](../src/infrastructure/__tests__/native_verification_controller.test.ts) · [`src/infrastructure/__tests__/native_verification_redaction.test.ts`](../src/infrastructure/__tests__/native_verification_redaction.test.ts) · [`src/infrastructure/__tests__/native_verification_workflow.test.ts`](../src/infrastructure/__tests__/native_verification_workflow.test.ts) · [`src/infrastructure/__tests__/native_verification_profiles.test.ts`](../src/infrastructure/__tests__/native_verification_profiles.test.ts) · [`src/infrastructure/__tests__/verification_executable.test.ts`](../src/infrastructure/__tests__/verification_executable.test.ts) · [`src/infrastructure/__tests__/native_verification_post.test.ts`](../src/infrastructure/__tests__/native_verification_post.test.ts) · [`src/infrastructure/__tests__/native_verification_source_runtime.test.ts`](../src/infrastructure/__tests__/native_verification_source_runtime.test.ts) - User documentation: [`docs/issues/branch-synchronization.mdx`](../docs/issues/branch-synchronization.mdx) · [`docs/issues/comment-commands.mdx`](../docs/issues/comment-commands.mdx) · [`docs/issues/notifications-and-auto-close.mdx`](../docs/issues/notifications-and-auto-close.mdx) · [`docs/development/architecture.mdx`](../docs/development/architecture.mdx) · [`docs/security-operations/operations/verification.mdx`](../docs/security-operations/operations/verification.mdx) · [`docs/security-operations/operations/troubleshooting.mdx`](../docs/security-operations/operations/troubleshooting.mdx) · [`docs/agents/repository-collaboration.mdx`](../docs/agents/repository-collaboration.mdx) ### `pull-request-lifecycle` — Pull request lifecycle and enrichment diff --git a/specs/catalog.json b/specs/catalog.json index 56a550880..e9832f860 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -2009,11 +2009,14 @@ "src/infrastructure/verification_setup_runner.ts", "src/infrastructure/native_verification_protocol.ts", "src/infrastructure/native_verification_candidate.ts", - "src/infrastructure/native_verification_overlay.ts", + "src/infrastructure/native_verification_staging.ts", "src/infrastructure/native_verification_controller.ts", "src/infrastructure/native_verification_profiles.ts", "src/infrastructure/native_verification_redaction.ts", - "scripts/generate-native-verification-workflows.cjs" + "scripts/generate-native-verification-workflows.cjs", + "src/infrastructure/verification_executable.ts", + "src/infrastructure/native_verification_source_runtime.ts", + "scripts/prepare-native-source-action.cjs" ], "tests": [ "src/application/policies/__tests__/branch_sync_notification_policy.test.ts", @@ -2058,7 +2061,10 @@ "src/infrastructure/__tests__/native_verification_controller.test.ts", "src/infrastructure/__tests__/native_verification_redaction.test.ts", "src/infrastructure/__tests__/native_verification_workflow.test.ts", - "src/infrastructure/__tests__/native_verification_profiles.test.ts" + "src/infrastructure/__tests__/native_verification_profiles.test.ts", + "src/infrastructure/__tests__/verification_executable.test.ts", + "src/infrastructure/__tests__/native_verification_post.test.ts", + "src/infrastructure/__tests__/native_verification_source_runtime.test.ts" ], "documentation": [ "docs/issues/branch-synchronization.mdx", diff --git a/specs/repository-verification-setup-hooks.md b/specs/repository-verification-setup-hooks.md index 2a1a51f09..9b9d2848d 100644 --- a/specs/repository-verification-setup-hooks.md +++ b/specs/repository-verification-setup-hooks.md @@ -92,11 +92,11 @@ sequenceDiagram R->>Y: Execute pinned YAML and assets Y-->>R: Native success or failure, runtime paths R->>C: Continue the existing operation once - C->>C: Restore candidate action, validate candidate and runtime paths + C->>C: Validate unchanged candidate and runtime paths C->>C: Preflight, verify, apply publication guards ``` -Text equivalent: Copilot prepares the candidate, pauses, the runner executes trusted setup, and the same operation resumes only after restoring and validating the candidate. No agent is rerun. +Text equivalent: Copilot prepares the candidate, pauses, the runner executes trusted setup, and the same operation resumes only after validating the unchanged candidate. No agent is rerun. ## 6. Functional behavior and state model @@ -105,10 +105,10 @@ Text equivalent: Copilot prepares the candidate, pauses, the runner executes tru 1. Composition MUST capture immutable trusted Git objects before candidate mutation. The action directory is limited to 32 regular files, 64 KiB per file and 256 KiB total. Its root metadata MUST be `action.yml`, valid composite YAML with 1–32 steps. Asset paths MUST use portable ASCII letters/digits/underscore/dot/dash/slash, at most 200 characters, without empty/parent segments or Windows reserved names. Symlinks, submodules, unsafe paths and nested local `uses` actions are rejected. Native external actions and reviewed auxiliary scripts are supported. 2. At the existing verifier boundary, a private same-job worker MUST retain the admitted call stack. Generated workflows do not rerun the full operation to continue it. 3. The worker MUST bind HEAD, symbolic branch, index, tracked changes and publishable untracked contents. Git observations are bounded to 16 MiB, 256 untracked files and 16 MiB aggregate untracked contents; oversized candidates fail closed. Ignored dependency caches are excluded. -4. The candidate's action directory MUST be backed up privately and temporarily overlaid with the trusted snapshot. The native step runs from the candidate workspace with the verifier's fresh HOME. After setup, the candidate directory MUST be restored byte for byte before verification/publication. +4. The trusted snapshot MUST be staged at the reserved local metadata path `.git/copilot-verification-setup`, outside publishable candidate files. Native execution requires the normal directory-based `.git` checkout produced by generated `actions/checkout` steps. The candidate's consumer action directory MUST never be rewritten. Staging MUST persist until native post steps finish. Runner state MUST retain the initial source SHA and digest of all captured file paths, bytes and modes. Before each composite post phase, a trusted Copilot post step MUST use its initially pinned absolute Git executable to restore that exact snapshot from Git objects at the pinned SHA and verify the saved digest, never a moved branch or candidate file. An unknown preexisting staging directory MUST fail closed without being overwritten. 5. The continuation MUST match the session, current request, run, attempt, job and workspace. Duplicate/stale responses fail closed. The candidate identity MUST still match. 6. Only new existing PATH directories within the workspace, private HOME or trusted tool cache, and an approved JAVA_HOME, MAY reach the verifier. The existing closed 16-KiB result validation and eight-directory ceiling remain in force. -7. Runtime preflight and the configured command MUST execute in that same fresh environment. Subsequent commands repeat setup with a new HOME and the current manifests. +7. Runtime preflight and the configured command MUST execute in that same fresh environment. Basename resolution MUST use its validated PATH, preserving virtual-environment invocation paths rather than the worker PATH or canonical binary target. Subsequent commands repeat setup with a new HOME and the current manifests. ### 6.2 Alternative paths @@ -147,13 +147,13 @@ The 20-command limit, five-minute native setup timeout, snapshot/result/path bou | Pure policy/application ports | Closed limits/errors, immutable prepared snapshot | Runner APIs or process implementation | | Existing application use cases | Admission, candidate preparation, verification and publication decisions | Native YAML interpretation or duplicate phase orchestration | | Git adapter/setup runner | Pause precisely before probes/command, validate resume | New mutation authorization | -| Infrastructure snapshot/overlay/protocol/controller | Trusted objects, temporary native action, same-job process lifecycle | Provider business policy or model execution replay | +| Infrastructure snapshot/staging/protocol/controller | Trusted objects, persistent native staging, same-job process lifecycle | Provider business policy or model execution replay | | Entrypoint/generated workflows | Dispatch native phases and native `uses` | Reimplement verifier/publication rules | | Presentation | Existing localized result/error surfaces | Executable helper output as policy | ### 8.2 Contracts, state, and trust boundaries -The session contains bounded non-secret metadata and private operational logs. Worker stdout/stderr redact configured secrets and dynamic masking commands before bytes reach disk; PAT/model credentials are never serialized as log content. Credentials remain in the admitted worker's memory/environment as in the existing Action; child verifier environments remain allowlisted. Session provenance, request UUIDs and one-use responses prevent accidental replay across jobs/candidates. Root Action post-job cleanup owns cancellation and private directory removal. Dependency-only profiles remain private until nested native post steps finish, then the original Action post step removes them; no later verifier reuses them. No artifacts or remote checkpoints are created. +The session contains bounded non-secret metadata and private operational logs. Worker stdout/stderr redact configured secrets and dynamic masking commands before bytes reach disk; PAT/model credentials are never serialized as log content. Credentials remain in the admitted worker's memory/environment as in the existing Action; child verifier environments remain allowlisted. Session provenance, request UUIDs and one-use responses prevent accidental replay across jobs/candidates. Root Action post-job cleanup owns cancellation and private directory removal. Source callbacks MUST copy their Action manifest, bundled runtime and setup assets outside the mutable candidate before execution; their continuations and post steps use that snapshot. Remote installed Actions already have an independent runner cache. Post hooks run only for the native callback phases, preserving ordinary direct Action behavior. Dependency-only profiles remain private until nested native post steps finish, then the original Action post step removes them; no later verifier reuses them. No artifacts or remote checkpoints are created. The workflow's fixed native phases support at most 20 setup boundaries per operation. Worker log forwarding preserves complete runner-command lines, with a 1-MiB single-line memory bound; this is distinct from native setup logs. Worker outputs are bounded to 256 KiB. @@ -190,19 +190,19 @@ Existing localized result cards and plain-text error fallback remain in force. N | Failure | Impact and action | Cleanup/retry | |---|---|---| | Missing/invalid trusted action | No verifier/push; review trusted directory | No automatic retry | -| Native failure or timeout | No verifier/push; inspect native step logs and repair | Worker abort/finally restores action and existing sync recovery | +| Native failure or timeout | No verifier/push; inspect native step logs and repair | Worker abort/finally preserves candidate files and uses existing sync recovery | | Stale/cross-job/replayed response | No continuation; rerun fresh operation | Private session canceled | -| Candidate changed during setup | No verifier/push; make setup dependency-only | Original action restored; existing recovery | +| Candidate changed during setup | No verifier/push; make setup dependency-only | Candidate action unchanged; existing recovery | | Invalid runtime output | No verifier/push; install under approved roots | Fresh HOME disposed | -| Job canceled/worker exits | No new publication; inspect retained operation result | Post action signals cancellation, waits up to 10 seconds, terminates remaining worker and restores overlay | +| Job canceled/worker exits | No new publication; inspect retained operation result | Post action signals cancellation, waits up to 10 seconds, terminates remaining worker and cleans owned staging after native posts | -Native setup steps have `timeout-minutes: 5`; the worker also rejects readiness after 300 seconds. The enclosing existing job timeout remains authoritative. A force-killed worker cannot promise application finally blocks ran: the Action restores its own overlay, and normal runner checkout cleanup/operator recovery owns remaining job-local candidate state. Already-completed remote effects MUST retain their existing truthful result semantics. +Native setup steps have `timeout-minutes: 5`; the worker also rejects readiness after 300 seconds. The enclosing existing job timeout remains authoritative. A force-killed worker cannot promise application finally blocks ran: the Action cleans its own staging, and normal runner checkout cleanup/operator recovery owns remaining job-local candidate state. Already-completed remote effects MUST retain their existing truthful result semantics. ## 11. Security, permissions, and privacy Native setup receives only runtime metadata from the private verifier environment, never Copilot's PAT/model inputs or evidence token. Generated workflows MUST keep provider secrets scoped to the initial Copilot Action step, not the job or consumer action. Checkout credentials remain unpersisted. Native actions can use runner facilities required for normal Actions execution; they are reviewed executable maintainer code. -This preserves the trusted-runner boundary and makes no OS-sandbox claim: same-user processes and service-level credentials remain an operator trust concern. PATH/JAVA_HOME are validated after setup; arbitrary `GITHUB_ENV` keys do not enter the verifier. Native external action versions and dependency caches are maintained by the repository owner. Nested local `uses` is excluded because its deferred post scripts could otherwise refer to restored candidate assets. +This preserves the trusted-runner boundary and makes no OS-sandbox claim: same-user processes and service-level credentials remain an operator trust concern. PATH/JAVA_HOME are validated after setup; arbitrary `GITHUB_ENV` keys do not enter the verifier. Native external action versions and dependency caches are maintained by the repository owner. Nested local `uses` is excluded because its deferred post scripts could otherwise refer to candidate assets outside the trusted staging tree. ## 12. Observability and operational UX @@ -224,10 +224,10 @@ Minimum **150 distinct automated cases**, allocated without double counting: | Candidate/runner orchestration | 24 | Ordering, fresh profiles, candidate identity, failure gates | | Workflows/setup contracts | 18 | Native step structure, env, limits, source/distribution, preservation | | Real Git/composition integration | 8 | Exact managed ref, merge manifests, abort/push guards | -| Runtime result filesystem security | 12 | Bounds, links, containment, races, atomic overlays | +| Runtime result filesystem security | 12 | Bounds, links, containment, races, owned staging | | **Total** | **150** | Native rewrite adds at least 70 cases beyond retained applicable baseline | -Repository-wide thresholds remain in force. New pure policy targets 100% branches; changed setup adapters target aggregate 95% lines/statements and 90% branches/functions. Deterministic unit fakes cover runner responses without interpreting YAML. Separate native CI acceptance MUST execute real composite `uses`/`run` steps with Python/pytest and Node/pnpm on hosted Linux, Windows and macOS, asserting candidate changes, trusted-source selection, fresh profiles and restoration. Controlled local Git acceptance covers the exact same managed branch and no release. Native CI evidence is additional to the distinct Jest budget. +Repository-wide thresholds remain in force. New pure policy targets 100% branches; changed setup adapters target aggregate 95% lines/statements and 90% branches/functions. Deterministic unit fakes cover runner responses without interpreting YAML. Separate native CI acceptance MUST execute real composite `uses`/`run` steps with Python/pytest and Node/pnpm on hosted Linux, Windows and macOS, asserting candidate changes, trusted-source selection, fresh profiles candidate preservation, source-runtime immutability even after candidate bundle changes, and final post cleanup. Controlled local Git acceptance covers the exact same managed branch and no release. Native CI evidence is additional to the distinct Jest budget. ## 15. Documentation and discoverability @@ -241,9 +241,9 @@ Repository-wide thresholds remain in force. New pure policy targets 100% branche 1. Disabled repositories run existing verification unchanged. 2. Enabled authorized sync prepares dependencies from the temporary merged manifests, verifies and pushes only its existing managed branch. -3. A candidate/model change to the action does not execute; trusted YAML/assets run, then original candidate bytes return. +3. A candidate/model change to the action does not execute; trusted YAML/assets run, the original candidate bytes remain unchanged. 4. Setup success precedes runtime probes; every command gets a fresh HOME. Read-only, dry-run, no-op and empty-command paths execute no setup. -5. Failed/canceled/timed-out setup, changed candidate, malformed output or replayed/cross-job continuation blocks verification and publication and restores the overlay. +5. Failed/canceled/timed-out setup, changed candidate, malformed output or replayed/cross-job continuation blocks verification and publication and cleans owned staging. 6. Native Python/pytest and Node/pnpm acceptance passes on all three hosted platforms. No PAT/model credentials are forwarded to the consumer action or verifier. 7. Updating Copilot twice preserves the consumer directory. Old/missing native callback support and CLI opt-in fail with actionable help. 8. Existing post-verification remote-head/workspace guards and truthful partial-success reporting remain authoritative. @@ -253,7 +253,7 @@ Repository-wide thresholds remain in force. New pure policy targets 100% branche | Requirements | Implementation/evidence | Documentation | |---|---|---| | §§4–7 source/defaults/routes | Policy, snapshot, config/composition and sync tests | Verification/setup reference | -| §6 native handoff/candidate | Runner, protocol, overlay, controller tests | Architecture/recovery | +| §6 native handoff/candidate | Runner, protocol, staging, controller tests | Architecture/recovery | | §§8–11 workflows/security/cleanup | Structural mutation tests, result filesystem tests, platform fixture | Native steps and trust boundary | | §§12–16 compatibility/acceptance | Setup preservation, real Git integration, hosted native acceptance | Migration/operator guide | diff --git a/src/infrastructure/__tests__/native_verification_controller.test.ts b/src/infrastructure/__tests__/native_verification_controller.test.ts index 252d495d7..fa35164ec 100644 --- a/src/infrastructure/__tests__/native_verification_controller.test.ts +++ b/src/infrastructure/__tests__/native_verification_controller.test.ts @@ -5,7 +5,7 @@ import { join } from 'node:path'; import * as core from '@actions/core'; import * as protocol from '../native_verification_protocol'; import { awaitNativeBoundary, cleanupNativeVerification, continueNativeVerification, drainNativeWorkerLog, runNativeVerificationEntry } from '../native_verification_controller'; -import { stageNativeVerificationAction } from '../native_verification_overlay'; +import { stageNativeVerificationAction } from '../native_verification_staging'; import { nativeVerificationFixture } from './native_verification_fixture'; jest.mock('@actions/core', () => ({ setOutput: jest.fn(), saveState: jest.fn(), getState: jest.fn(() => ''), getInput: jest.fn(() => 'true'), setFailed: jest.fn(), setSecret: jest.fn() })); @@ -15,7 +15,7 @@ describe('native Actions phase controller', () => { const roots: string[] = []; const done = (exitCode = 0) => protocol.writeNativeRecord(f.session, 'done.json', { exitCode }); const request = (edit: Record = {}) => protocol.writeNativeRecord(f.session, 'request.json', { - id: f.id, sourceSha: 'a'.repeat(40), environment: f.runtime.environment, ...edit, + id: f.id, sourceSha: 'a'.repeat(40), digest: 'b'.repeat(64), environment: f.runtime.environment, ...edit, }); beforeEach(() => { before = { ...process.env }; f = nativeVerificationFixture(); jest.clearAllMocks(); jest.mocked(core.getState).mockReturnValue(''); jest.mocked(core.getInput).mockReturnValue('true'); }); afterEach(async () => { @@ -139,7 +139,7 @@ describe('native Actions phase controller', () => { await runNativeVerificationEntry(jest.fn()); expect(core.setFailed).toHaveBeenCalled(); }); it('uses the post phase only for cleanup', async () => { - jest.mocked(core.getState).mockImplementation(key => key === 'copilot-native-post' ? 'true' : f.session); done(); const run = jest.fn(); + jest.mocked(core.getState).mockImplementation(key => key === 'copilot-native-post' || key === 'native-verification-owner' ? 'true' : key === 'native-verification-ticket' ? f.session : ''); done(); const run = jest.fn(); await runNativeVerificationEntry(run); expect(run).not.toHaveBeenCalled(); expect(fs.existsSync(f.session)).toBe(false); }); it('allows post phases without an active verification session', async () => { diff --git a/src/infrastructure/__tests__/native_verification_fixture.ts b/src/infrastructure/__tests__/native_verification_fixture.ts index 9c7c6f8a2..e5691f832 100644 --- a/src/infrastructure/__tests__/native_verification_fixture.ts +++ b/src/infrastructure/__tests__/native_verification_fixture.ts @@ -32,7 +32,7 @@ export function nativeVerificationFixture() { if (!request) throw new Error('No pending native setup'); writeNativeRecord(session, `response-${request.id}.json`, { id: request.id, success: true, path: request.environment.PATH ?? '', ...edit?.(request) }); }; - return { root, runtime, session, git, setup, respond, action: join(root, VERIFICATION_SETUP_DIRECTORY), + return { root, runtime, session, git, setup, respond, action: join(root, VERIFICATION_SETUP_DIRECTORY), staged: join(root, '.git/copilot-verification-setup'), id: randomUUID(), cleanup: () => { cwd.mockRestore(); if (previous === undefined) delete process.env.COPILOT_NATIVE_WORKER_SESSION; else process.env.COPILOT_NATIVE_WORKER_SESSION = previous; diff --git a/src/infrastructure/__tests__/native_verification_post.test.ts b/src/infrastructure/__tests__/native_verification_post.test.ts new file mode 100644 index 000000000..50f6bd0d6 --- /dev/null +++ b/src/infrastructure/__tests__/native_verification_post.test.ts @@ -0,0 +1,46 @@ +import { verificationExecutable } from '../verification_executable'; +import fs from 'node:fs'; +import { join } from 'node:path'; +import * as core from '@actions/core'; +import { runNativeVerificationEntry } from '../native_verification_controller'; +import { captureVerificationSetupAtCommit, verificationSetupDigest } from '../verification_setup_snapshot'; +import { stageNativeVerificationAction } from '../native_verification_staging'; +import { nativeVerificationFixture, COMPOSITE } from './native_verification_fixture'; + +jest.mock('@actions/core', () => ({getState: jest.fn(), saveState: jest.fn(), setOutput: jest.fn(), setFailed: jest.fn(), setSecret: jest.fn()})); + +describe('trusted snapshot during native post phases', () => { + let f: ReturnType; let source: string; let digest: string; let gitPath: string; + beforeEach(() => { + f = nativeVerificationFixture(); gitPath = verificationExecutable('git', f.runtime.environment); fs.mkdirSync(f.action, {recursive: true}); fs.writeFileSync(join(f.action, 'action.yml'), COMPOSITE); + f.git('add', '.'); f.git('commit', '-m', 'reviewed source'); source = f.git('rev-parse', 'HEAD'); + const snapshot = captureVerificationSetupAtCommit(source, f.root); if (snapshot.kind !== 'ready') throw new Error('snapshot'); + digest = verificationSetupDigest(snapshot); stageNativeVerificationAction(snapshot, f.session); + jest.mocked(core.getState).mockImplementation(key => ({'copilot-native-post': 'true', 'native-verification-ticket': f.session, + 'native-verification-source': source, 'native-verification-git': gitPath, 'native-verification-digest': digest}[key] ?? '')); + }); + afterEach(() => { jest.restoreAllMocks(); f.cleanup(); }); + it('keeps trusted composite metadata available for deferred post steps without replaying the operation', async () => { + fs.writeFileSync(join(f.action, 'action.yml'), 'candidate'); fs.writeFileSync(join(f.staged, 'action.yml'), 'tampered staging'); + const run = jest.fn(); await runNativeVerificationEntry(run); + expect(run).not.toHaveBeenCalled(); expect(fs.readFileSync(join(f.staged, 'action.yml'), 'utf8')).toBe(COMPOSITE); + expect(fs.readFileSync(join(f.action, 'action.yml'), 'utf8')).toBe('candidate'); + }); + it('rereads the original commit rather than a later candidate or moved branch', async () => { + fs.writeFileSync(join(f.action, 'action.yml'), 'runs: {using: composite, steps: [{run: "exit 99", shell: bash}]}'); + f.git('add', '.'); f.git('commit', '-m', 'candidate'); f.git('update-ref', 'refs/remotes/origin/main', f.git('rev-parse', 'HEAD')); + await runNativeVerificationEntry(jest.fn()); expect(fs.readFileSync(join(f.staged, 'action.yml'), 'utf8')).toBe(COMPOSITE); + }); + it('pins Git before native PATH changes instead of executing a tool supplied by the candidate', async () => { + const before = process.env.PATH; + try { process.env.PATH = f.root; await runNativeVerificationEntry(jest.fn()); } + finally { process.env.PATH = before; } + expect(fs.readFileSync(join(f.staged, 'action.yml'), 'utf8')).toBe(COMPOSITE); + }); + it('removes executable staging before failing a mismatched original content digest', async () => { + digest = '0'.repeat(64); await expect(runNativeVerificationEntry(jest.fn())).rejects.toThrow('helper-invalid'); expect(fs.existsSync(f.staged)).toBe(false); + }); + it('removes executable staging when pinned source objects are unavailable', async () => { + source = '0'.repeat(40); await expect(runNativeVerificationEntry(jest.fn())).rejects.toThrow('helper-invalid'); expect(fs.existsSync(f.staged)).toBe(false); + }); +}); diff --git a/src/infrastructure/__tests__/native_verification_protocol.test.ts b/src/infrastructure/__tests__/native_verification_protocol.test.ts index 552ce021a..fc0e2dc7e 100644 --- a/src/infrastructure/__tests__/native_verification_protocol.test.ts +++ b/src/infrastructure/__tests__/native_verification_protocol.test.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import { join } from 'node:path'; import { nativeSession, readNativeRecord, writeNativeRecord } from '../native_verification_protocol'; import { verificationCandidateIdentity } from '../native_verification_candidate'; -import { stageNativeVerificationAction, restoreNativeVerificationAction } from '../native_verification_overlay'; +import { stageNativeVerificationAction, cleanupNativeVerificationAction } from '../native_verification_staging'; import { nativeVerificationFixture } from './native_verification_fixture'; describe('native session provenance and filesystem contracts', () => { @@ -41,20 +41,26 @@ describe('native session provenance and filesystem contracts', () => { }); it('restores originals on explicit cancellation and makes cleanup idempotent', () => { fs.mkdirSync(f.action, { recursive: true }); fs.writeFileSync(join(f.action, 'action.yml'), 'original'); - stageNativeVerificationAction(f.setup as never, f.session); restoreNativeVerificationAction(f.session, f.root); restoreNativeVerificationAction(f.session, f.root); + stageNativeVerificationAction(f.setup as never, f.session); cleanupNativeVerificationAction(f.session, f.root); cleanupNativeVerificationAction(f.session, f.root); expect(fs.readFileSync(join(f.action, 'action.yml'), 'utf8')).toBe('original'); }); - it('rejects unsafe action ancestors', () => { - fs.writeFileSync(join(f.root, '.github'), 'file'); - expect(() => stageNativeVerificationAction(f.setup as never, f.session)).toThrow('helper-invalid'); + it('rejects a nonstandard Git metadata checkout', () => { + fs.renameSync(join(f.root, '.git'), join(f.root, '.git-original')); fs.writeFileSync(join(f.root, '.git'), 'gitdir: elsewhere'); + expect(() => stageNativeVerificationAction(f.setup as never, f.session)).toThrow('native-workflow-required'); }); - it('rejects a non-directory action without overwriting it', () => { - fs.mkdirSync(join(f.root, '.github/actions'), { recursive: true }); fs.writeFileSync(f.action, 'original'); - expect(() => stageNativeVerificationAction(f.setup as never, f.session)).toThrow('helper-invalid'); expect(fs.readFileSync(f.action, 'utf8')).toBe('original'); + it('never overwrites a preexisting unowned staging directory', () => { + fs.mkdirSync(f.staged); fs.writeFileSync(join(f.staged, 'action.yml'), 'unrelated'); + expect(() => stageNativeVerificationAction(f.setup as never, f.session)).toThrow('handoff-invalid'); + expect(fs.readFileSync(join(f.staged, 'action.yml'), 'utf8')).toBe('unrelated'); }); - it('rejects malformed overlay recovery records without deleting candidate content', () => { - writeNativeRecord(f.session, 'overlay.json', { target: '/other', backup: '/other', created: [] }); - expect(() => restoreNativeVerificationAction(f.session, f.root)).toThrow('handoff-invalid'); + it('preserves an unowned broken staging link', () => { + fs.symlinkSync(join(f.root, 'missing'), f.staged, process.platform === 'win32' ? 'junction' : 'dir'); + expect(() => stageNativeVerificationAction(f.setup as never, f.session)).toThrow('handoff-invalid'); + expect(fs.lstatSync(f.staged).isSymbolicLink()).toBe(true); + }); + it('rejects malformed staging records without deleting unrelated content', () => { + writeNativeRecord(f.session, 'staging.json', { target: '/other' }); + expect(() => cleanupNativeVerificationAction(f.session, f.root)).toThrow('handoff-invalid'); }); it('binds tracked contents, branch and staged content, not filesystem timestamps', () => { const before = verificationCandidateIdentity(f.root, f.runtime.environment); diff --git a/src/infrastructure/__tests__/native_verification_runner.test.ts b/src/infrastructure/__tests__/native_verification_runner.test.ts index ed5fd5620..b44ac5e94 100644 --- a/src/infrastructure/__tests__/native_verification_runner.test.ts +++ b/src/infrastructure/__tests__/native_verification_runner.test.ts @@ -32,15 +32,16 @@ describe('native verification boundary', () => { it('stages only pinned YAML and assets, then restores the candidate byte for byte', async () => { fs.mkdirSync(f.action, { recursive: true }); fs.writeFileSync(join(f.action, 'action.yml'), 'candidate'); const running = prepareRepositoryVerification(f.setup, f.runtime.environment); - expect(fs.readFileSync(join(f.action, 'action.yml'), 'utf8')).toBe(COMPOSITE); - expect(fs.readFileSync(join(f.action, 'scripts/prepare.sh'), 'utf8')).toBe('echo reviewed'); + expect(fs.readFileSync(join(f.staged, 'action.yml'), 'utf8')).toBe(COMPOSITE); + expect(fs.readFileSync(join(f.action, 'action.yml'), 'utf8')).toBe('candidate'); + expect(fs.readFileSync(join(f.staged, 'scripts/prepare.sh'), 'utf8')).toBe('echo reviewed'); f.respond(); await running; expect(fs.readFileSync(join(f.action, 'action.yml'), 'utf8')).toBe('candidate'); expect(fs.existsSync(join(f.action, 'scripts'))).toBe(false); }); - it('removes a temporary action absent from the candidate', async () => { + it('retains native metadata outside the publishable candidate for deferred post steps', async () => { const running = prepareRepositoryVerification(f.setup, f.runtime.environment); f.respond(); await running; - expect(fs.existsSync(f.action)).toBe(false); expect(f.git('status', '--porcelain')).toBe(''); + expect(fs.existsSync(f.action)).toBe(false); expect(fs.existsSync(f.staged)).toBe(true); expect(f.git('status', '--porcelain')).toBe(''); }); it('prepares each changed candidate with a fresh HOME before preflight and verification', async () => { const homes: string[] = []; const order: string[] = []; diff --git a/src/infrastructure/__tests__/native_verification_source_runtime.test.ts b/src/infrastructure/__tests__/native_verification_source_runtime.test.ts new file mode 100644 index 000000000..790ac49b8 --- /dev/null +++ b/src/infrastructure/__tests__/native_verification_source_runtime.test.ts @@ -0,0 +1,42 @@ +import fs from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { cleanupNativeSourceRuntime } from '../native_verification_source_runtime'; +const {prepareNativeSourceAction} = require('../../../scripts/prepare-native-source-action.cjs') as {prepareNativeSourceAction(root: string, fixture?: boolean): string}; + +describe('immutable source Action runtime snapshot', () => { + let container: string; let workspace: string; + beforeEach(() => { + container = fs.realpathSync(fs.mkdtempSync(join(tmpdir(), 'copilot-source-action-'))); workspace = join(container, 'workspace'); + fs.mkdirSync(join(workspace, 'build/github_action'), {recursive: true}); fs.writeFileSync(join(workspace, 'action.yml'), 'runs: {using: node24, main: build/github_action/index.js}'); + fs.writeFileSync(join(workspace, 'build/github_action/index.js'), 'original'); + }); + afterEach(() => { jest.restoreAllMocks(); fs.rmSync(container, {recursive: true, force: true}); }); + it('captures the manifest and runtime outside the mutable candidate', () => { + const root = prepareNativeSourceAction(workspace); fs.writeFileSync(join(workspace, 'build/github_action/index.js'), 'candidate'); + expect(root).toBe(join(container, '.copilot-native-runtime')); expect(fs.readFileSync(join(root, 'build/github_action/index.js'), 'utf8')).toBe('original'); + cleanupNativeSourceRuntime(root, workspace); expect(fs.existsSync(root)).toBe(false); cleanupNativeSourceRuntime(root, workspace); + }); + it('replaces an owned stale snapshot and preserves setup assets', () => { + fs.mkdirSync(join(workspace, 'setup')); fs.writeFileSync(join(workspace, 'setup/file.yml'), 'asset'); const root = prepareNativeSourceAction(workspace); + fs.writeFileSync(join(workspace, 'build/github_action/index.js'), 'reviewed-new-version'); prepareNativeSourceAction(workspace); + expect(fs.readFileSync(join(root, 'build/github_action/index.js'), 'utf8')).toBe('reviewed-new-version'); expect(fs.readFileSync(join(root, 'setup/file.yml'), 'utf8')).toBe('asset'); + }); + it('supports the native CI fixture with the same isolation contract', () => { + const source = join(workspace, '.github/actions/copilot-native-fixture'); fs.mkdirSync(join(source, 'dist'), {recursive: true}); + fs.writeFileSync(join(source, 'action.yml'), 'fixture'); fs.writeFileSync(join(source, 'dist/index.js'), 'fixture-code'); + const root = prepareNativeSourceAction(workspace, true); expect(fs.readFileSync(join(root, 'dist/index.js'), 'utf8')).toBe('fixture-code'); cleanupNativeSourceRuntime(root, workspace); + }); + it('refuses to overwrite an unrelated directory', () => { + fs.mkdirSync(join(container, '.copilot-native-runtime')); fs.writeFileSync(join(container, '.copilot-native-runtime/keep'), 'original'); + expect(() => prepareNativeSourceAction(workspace)).toThrow('not owned'); expect(fs.readFileSync(join(container, '.copilot-native-runtime/keep'), 'utf8')).toBe('original'); + }); + it('refuses a receipt from another workspace', () => { + const root = prepareNativeSourceAction(workspace); fs.writeFileSync(join(root, '.copilot-runtime-snapshot.json'), JSON.stringify({kind: 'copilot-native-runtime', workspace: 'other'})); + expect(() => prepareNativeSourceAction(workspace)).toThrow('ownership'); expect(() => cleanupNativeSourceRuntime(root, workspace)).toThrow('handoff-invalid'); + }); + it('rejects unsafe source assets and cleanup paths', () => { + fs.rmSync(join(workspace, 'build/github_action/index.js')); fs.mkdirSync(join(workspace, 'build/github_action/index.js')); + expect(() => prepareNativeSourceAction(workspace)).toThrow('regular file'); expect(() => cleanupNativeSourceRuntime(workspace, workspace)).toThrow('handoff-invalid'); + }); +}); diff --git a/src/infrastructure/__tests__/native_verification_workflow.test.ts b/src/infrastructure/__tests__/native_verification_workflow.test.ts index bfb2d3cee..a127183ad 100644 --- a/src/infrastructure/__tests__/native_verification_workflow.test.ts +++ b/src/infrastructure/__tests__/native_verification_workflow.test.ts @@ -13,7 +13,7 @@ describe('native verification workflow contract', () => { }); it('has exactly twenty native setup slots and one completion, with five-minute native timeouts', () => { const steps = phaseSteps('./'); expect(MAX_REQUESTS).toBe(20); expect(steps).toHaveLength(41); - expect(steps.filter(step => step.uses === './.github/actions/copilot-verification-setup').every(step => step['timeout-minutes'] === 5)).toBe(true); + expect(steps.filter(step => step.uses === './.git/copilot-verification-setup').every(step => step['timeout-minutes'] === 5)).toBe(true); }); it.each(['timeout', 'env', 'condition', 'reference', 'request', 'outcome', 'finish', 'extra', 'remove', 'prepare'])('rejects a weakened %s contract', kind => { const file = path.join(process.cwd(), 'setup/workflows/copilot_issue_comment.yml'); const workflow = load(fs.readFileSync(file, 'utf8')) as any; diff --git a/src/infrastructure/__tests__/verification_executable.test.ts b/src/infrastructure/__tests__/verification_executable.test.ts new file mode 100644 index 000000000..b8fc671c2 --- /dev/null +++ b/src/infrastructure/__tests__/verification_executable.test.ts @@ -0,0 +1,38 @@ +import fs from 'node:fs'; +import { join, delimiter } from 'node:path'; +import { tmpdir } from 'node:os'; +import { verificationExecutable } from '../verification_executable'; + +describe('verification executable selection', () => { + let root: string; + beforeEach(() => { root = fs.mkdtempSync(join(tmpdir(), 'copilot-executable-')); }); + afterEach(() => { jest.restoreAllMocks(); fs.rmSync(root, { recursive: true, force: true }); }); + it('selects the executable from the isolated PATH rather than the worker PATH', () => { + const binary = join(root, process.platform === 'win32' ? 'python.EXE' : 'python'); fs.writeFileSync(binary, 'fixture', {mode: 0o700}); + expect(verificationExecutable('python', {PATH: root})).toBe(binary); + }); + it('preserves an explicit invocation path rather than resolving away virtual-environment links', () => { + const binary = join(root, 'tool.exe'); fs.writeFileSync(binary, 'fixture', {mode: 0o700}); + expect(verificationExecutable(binary, {PATH: ''}, 'win32')).toBe(binary); + }); + it('skips non-files and unavailable candidates without falling back to the host PATH', () => { + fs.mkdirSync(join(root, process.platform === 'win32' ? 'node.EXE' : 'node')); expect(() => verificationExecutable('node', {PATH: root})).toThrow('isolated PATH'); + }); + it('honors Windows executable extensions', () => { + const binary = join(root, 'pnpm.CMD'); fs.writeFileSync(binary, 'fixture', {mode: 0o700}); + expect(verificationExecutable('pnpm', {PATH: root, PATHEXT: '.EXE;.CMD'}, 'win32')).toBe(binary); + }); + it('selects the Windows command shim rather than its extensionless Unix sibling', () => { + fs.writeFileSync(join(root, 'pnpm'), 'Unix shell wrapper', {mode: 0o700}); fs.writeFileSync(join(root, 'pnpm.CMD'), 'Windows command wrapper', {mode: 0o700}); + expect(verificationExecutable('pnpm', {PATH: root, PATHEXT: '.EXE;.CMD'}, 'win32')).toBe(join(root, 'pnpm.CMD')); + }); + it('supports the Windows default extension list', () => { + const binary = join(root, 'tool.EXE'); fs.writeFileSync(binary, 'fixture', {mode: 0o700}); + expect(verificationExecutable('tool', {PATH: root}, 'win32')).toBe(binary); + }); + it('fails closed for absent PATH and inaccessible tools', () => { + expect(() => verificationExecutable('python', {})).toThrow('isolated PATH'); + const file = join(root, process.platform === 'win32' ? 'tool.EXE' : 'tool'); fs.writeFileSync(file, 'fixture', {mode: 0o700}); jest.spyOn(fs, 'accessSync').mockImplementation(() => { throw new Error('denied'); }); + expect(() => verificationExecutable('tool', {PATH: `${root}${delimiter}/missing`})).toThrow('isolated PATH'); + }); +}); diff --git a/src/infrastructure/__tests__/verification_setup_configuration.test.ts b/src/infrastructure/__tests__/verification_setup_configuration.test.ts index 431bbf85c..1f4350a48 100644 --- a/src/infrastructure/__tests__/verification_setup_configuration.test.ts +++ b/src/infrastructure/__tests__/verification_setup_configuration.test.ts @@ -41,7 +41,7 @@ describe('verification setup installation contract', () => { 'keeps source and distributed %s opt-in explicit and disabled by default', name => { for (const directory of ['.github/workflows', 'setup/workflows']) { const workflow = yaml.load(fs.readFileSync(path.resolve(__dirname, '../../..', directory, name), 'utf8')) as {jobs: Record}[]}>}; - const actions = Object.values(workflow.jobs).flatMap(job => job.steps).filter(step => step.uses === './' || step.uses?.startsWith('vypdev/copilot@')); + const actions = Object.values(workflow.jobs).flatMap(job => job.steps).filter(step => step.uses === './' || step.uses === './../.copilot-native-runtime' || step.uses?.startsWith('vypdev/copilot@')); expect(actions.length).toBeGreaterThan(0); for (const action of actions) expect(action.with?.['verification-setup']).toBe("${{ vars.VERIFICATION_SETUP || 'false' }}"); } diff --git a/src/infrastructure/git_commit_adapter.ts b/src/infrastructure/git_commit_adapter.ts index 349c51a38..9149aa1e8 100644 --- a/src/infrastructure/git_commit_adapter.ts +++ b/src/infrastructure/git_commit_adapter.ts @@ -1,3 +1,4 @@ +import { verificationExecutable } from './verification_executable'; import { nativeVerificationRetainsProfile } from './native_verification_profiles'; import * as exec from '@actions/exec'; import type { GitCommitPort } from '../application/ports/git_ports'; @@ -8,13 +9,13 @@ import { assertVerificationRuntimePrerequisites } from './verification_runtime_p import { DISABLED_VERIFICATION_SETUP, type PreparedVerificationSetup } from '../application/ports/verification_setup_ports'; import { prepareRepositoryVerification } from './verification_setup_runner'; -type GitCommandOptions = { stdout?: (data: Buffer) => void; env?: Record; untrusted?: boolean; verifyRuntimePrerequisites?: boolean }; +type GitCommandOptions = { stdout?: (data: Buffer) => void; env?: Record; untrusted?: boolean; verifyRuntimePrerequisites?: boolean; resolveFromEnvironment?: boolean }; type GitCommandExecutor = (program: string, args: string[], options?: GitCommandOptions) => Promise; export class GitCommitAdapter implements GitCommitPort { constructor( private readonly executeCommand: GitCommandExecutor = (program, args, options) => options - ? exec.exec(program, args, { + ? exec.exec(options.resolveFromEnvironment && options.env ? verificationExecutable(program, options.env) : program, args, { // Captured Git reads can contain NUL-delimited records that // hide subsequent lines in GitHub's Actions log viewer. ...(options.stdout ? { silent: true } : {}), @@ -39,6 +40,7 @@ export class GitCommitAdapter implements GitCommitPort { return await this.executeCommand(program, args, { ...(options.stdout ? { stdout: options.stdout } : {}), env: runtime.environment, + ...(options.verifyRuntimePrerequisites && this.verificationSetup.kind === 'ready' ? { resolveFromEnvironment: true } : {}), }); } finally { if (!nativeVerificationRetainsProfile(runtime.environment.HOME)) runtime.cleanup(); diff --git a/src/infrastructure/native_verification_controller.ts b/src/infrastructure/native_verification_controller.ts index bf028f6ae..9f712516c 100644 --- a/src/infrastructure/native_verification_controller.ts +++ b/src/infrastructure/native_verification_controller.ts @@ -1,3 +1,5 @@ +import { cleanupNativeSourceRuntime } from './native_verification_source_runtime'; +import { verificationExecutable } from './verification_executable'; import { nativeWorkerSecrets, redactNativeWorkerLogs } from './native_verification_redaction'; import { removeNativeVerificationProfiles } from './native_verification_profiles'; import * as core from '@actions/core'; @@ -9,7 +11,8 @@ import { trustedWindowsSystemTool } from './agents/agent_trusted_system_tools'; import { getGithubActionInput } from './github_action_input'; import { VerificationSetupError, VERIFICATION_SETUP_LIMITS } from '../application/policies/verification_setup_policy'; import { nativePause, nativeSession, readNativeRecord, writeNativeRecord, type NativeVerificationRequest, type NativeVerificationResponse } from './native_verification_protocol'; -import { restoreNativeVerificationAction } from './native_verification_overlay'; +import { cleanupNativeVerificationAction, stageNativeVerificationAction } from './native_verification_staging'; +import { captureVerificationSetupAtCommit, verificationSetupDigest } from './verification_setup_snapshot'; function emitWorkerOutput(root: string): void { const file = join(root, 'worker-output'); @@ -70,8 +73,12 @@ export async function awaitNativeBoundary(root: string, timeoutMs = 7_200_000): const request = readNativeRecord(root, 'request.json', 32_768); if (request) { if (!/^[a-f0-9-]{36}$/u.test(request.id) || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(request.sourceSha) - || !request.environment || typeof request.environment.HOME !== 'string') throw new VerificationSetupError('handoff-invalid'); + || !/^[a-f0-9]{64}$/u.test(request.digest) || !request.environment || typeof request.environment.HOME !== 'string') throw new VerificationSetupError('handoff-invalid'); core.setOutput('verification-request', request.id); + core.setOutput('verification-source', request.sourceSha); + core.setOutput('verification-digest', request.digest); + core.saveState('native-verification-digest', request.digest); + core.saveState('native-verification-source', request.sourceSha); for (const key of ['HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'PATH', 'JAVA_HOME', 'TMP', 'TEMP', 'TMPDIR']) { const value = request.environment[key] ?? ''; if (typeof value !== 'string' || /[\r\n\0]/u.test(value)) throw new VerificationSetupError('handoff-invalid'); @@ -113,13 +120,16 @@ export async function cleanupNativeVerification(root: string, waitMs = 10_000, d } catch { /* Worker may have exited while cancellation completed. */ } } } - restoreNativeVerificationAction(root, session.workspace); + if (!deferProfileCleanup) cleanupNativeVerificationAction(root, session.workspace); drainNativeWorkerLog(root, true); if (!deferProfileCleanup) { removeNativeVerificationProfiles(root); rmSync(root, { recursive: true, force: true }); } } async function startNativeVerification(): Promise { for (const secret of nativeWorkerSecrets(process.env)) core.setSecret(secret); + const git = verificationExecutable('git', { PATH: process.env.PATH ?? '', PATHEXT: process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM' }); + core.saveState('native-verification-git', git); + core.setOutput('verification-git', git); const root = realpathSync(createPrivateVerificationRuntimeRoot()); for (const name of ['worker-output', 'worker-env', 'worker-path', 'worker-log', 'worker-summary']) writeFileSync(join(root, name), '', { flag: 'wx', mode: 0o600 }); const log = openSync(join(root, 'worker-log'), 'a'); @@ -132,6 +142,7 @@ async function startNativeVerification(): Promise { writeNativeRecord(root, 'session.json', { schemaVersion: 1, workspace: realpathSync(process.cwd()), run: process.env.GITHUB_RUN_ID ?? '', attempt: process.env.GITHUB_RUN_ATTEMPT ?? '', job: process.env.GITHUB_JOB ?? '', pid: worker.pid }); core.saveState('native-verification-ticket', root); + core.saveState('native-verification-owner', 'true'); core.setOutput('verification-ticket', root); await awaitNativeBoundary(root); } catch (error) { @@ -146,10 +157,25 @@ export async function runNativeVerificationEntry(run: () => Promise): Prom const post = core.getState('copilot-native-post'); if (post === 'true') { const root = core.getState('native-verification-ticket'); - if (root) await cleanupNativeVerification(root); + if (root && core.getState('native-verification-owner') === 'true') await cleanupNativeVerification(root); + else if (root && existsSync(root) && core.getState('native-verification-source')) { + const session = nativeSession(root); + try { + const snapshot = captureVerificationSetupAtCommit(core.getState('native-verification-source'), session.workspace, core.getState('native-verification-git')); + if (snapshot.kind !== 'ready' || verificationSetupDigest(snapshot) !== core.getState('native-verification-digest')) throw new VerificationSetupError('helper-invalid'); + stageNativeVerificationAction(snapshot, root); + } catch { + // A later native post must fail to load rather than execute candidate-modified metadata. + cleanupNativeVerificationAction(root, session.workspace); + throw new VerificationSetupError('helper-invalid'); + } + } + const sourceRuntime = core.getState('native-source-runtime'); + if (sourceRuntime) cleanupNativeSourceRuntime(sourceRuntime, realpathSync(process.cwd())); return; } core.saveState('copilot-native-post', 'true'); + if (process.env.COPILOT_NATIVE_PHASE === 'prepare' && process.env.COPILOT_NATIVE_RUNTIME_CACHE) core.saveState('native-source-runtime', process.env.COPILOT_NATIVE_RUNTIME_CACHE); const worker = process.env.COPILOT_NATIVE_WORKER_SESSION; if (worker) { const deadline = Date.now() + 30_000; @@ -169,6 +195,11 @@ export async function runNativeVerificationEntry(run: () => Promise): Prom if (phase === 'prepare') { await startNativeVerification(); return; } const root = process.env.COPILOT_NATIVE_TICKET; if (!root) throw new VerificationSetupError('handoff-invalid'); + nativeSession(root); + core.saveState('native-verification-ticket', root); + core.saveState('native-verification-source', process.env.COPILOT_NATIVE_SOURCE ?? ''); + core.saveState('native-verification-digest', process.env.COPILOT_NATIVE_DIGEST ?? ''); + core.saveState('native-verification-git', process.env.COPILOT_NATIVE_GIT ?? ''); if (phase === 'continue') { await continueNativeVerification(root, process.env.COPILOT_NATIVE_REQUEST ?? '', process.env.COPILOT_NATIVE_OUTCOME ?? ''); return; diff --git a/src/infrastructure/native_verification_overlay.ts b/src/infrastructure/native_verification_overlay.ts deleted file mode 100644 index d9921d5a1..000000000 --- a/src/infrastructure/native_verification_overlay.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { existsSync, lstatSync, mkdirSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from 'node:fs'; -import { dirname, join, relative, sep } from 'node:path'; -import type { PreparedVerificationSetup } from '../application/ports/verification_setup_ports'; -import { VERIFICATION_SETUP_DIRECTORY, VerificationSetupError } from '../application/policies/verification_setup_policy'; -import { readNativeRecord, writeNativeRecord } from './native_verification_protocol'; - -interface OverlayRecord { readonly target: string; readonly backup: string; readonly existed: boolean; readonly created: string[] } - -/** A temporary reviewed action overlays the candidate path only while the operation is paused. */ -export function stageNativeVerificationAction(setup: Extract, session: string): void { - const target = join(setup.workspace, VERIFICATION_SETUP_DIRECTORY); - const backup = join(session, 'candidate-action'); - const created: string[] = []; - let parent = setup.workspace; - for (const part of VERIFICATION_SETUP_DIRECTORY.split('/').slice(0, -1)) { - parent = join(parent, part); - if (existsSync(parent)) { - if (lstatSync(parent).isSymbolicLink() || !lstatSync(parent).isDirectory() || realpathSync(parent) !== parent) throw new VerificationSetupError('helper-invalid'); - } else { mkdirSync(parent, { mode: 0o700 }); created.push(parent); } - } - const targetStat = lstatSync(target, { throwIfNoEntry: false }); - const existed = !!targetStat; - if (targetStat && (targetStat.isSymbolicLink() || !targetStat.isDirectory())) throw new VerificationSetupError('helper-invalid'); - const record: OverlayRecord = { target, backup, existed, created }; - writeNativeRecord(session, 'overlay.json', record); - if (existed) renameSync(target, backup); - mkdirSync(target, { mode: 0o700 }); - for (const file of setup.files) { - const destination = join(target, file.path); - const remainder = relative(target, destination); - if (!remainder || remainder.startsWith(`..${sep}`) || remainder === '..') throw new VerificationSetupError('helper-invalid'); - mkdirSync(dirname(destination), { recursive: true, mode: 0o700 }); - writeFileSync(destination, Buffer.from(file.base64, 'base64'), { mode: file.executable ? 0o700 : 0o600, flag: 'wx' }); - } -} - -export function restoreNativeVerificationAction(session: string, workspace: string): void { - const record = readNativeRecord(session, 'overlay.json'); - if (!record) return; - if (record.target !== join(workspace, VERIFICATION_SETUP_DIRECTORY) || record.backup !== join(session, 'candidate-action') - || !Array.isArray(record.created) || record.created.some(path => ![join(workspace, '.github'), join(workspace, '.github/actions')].includes(path))) throw new VerificationSetupError('handoff-invalid'); - for (const parent of [join(workspace, '.github'), join(workspace, '.github/actions')]) { - if (!existsSync(parent) || lstatSync(parent).isSymbolicLink() || !lstatSync(parent).isDirectory() || realpathSync(parent) !== parent) throw new VerificationSetupError('handoff-invalid'); - } - // A backup is the authoritative original. A second cleanup must never remove it. - if (record.existed && !existsSync(record.backup)) return; - rmSync(record.target, { recursive: true, force: true }); - if (record.existed) renameSync(record.backup, record.target); - for (const directory of [...record.created].reverse()) { try { rmdirSync(directory); } catch { /* Other candidate content remains. */ } } - rmSync(join(session, 'overlay.json'), { force: true }); -} diff --git a/src/infrastructure/native_verification_profiles.ts b/src/infrastructure/native_verification_profiles.ts index 9f2f616b8..d3db3bd8b 100644 --- a/src/infrastructure/native_verification_profiles.ts +++ b/src/infrastructure/native_verification_profiles.ts @@ -1,3 +1,4 @@ +import { createHash } from 'node:crypto'; import { lstatSync, realpathSync, rmSync, writeFileSync, readFileSync } from 'node:fs'; import { basename, dirname, join } from 'node:path'; import { VerificationSetupError } from '../application/policies/verification_setup_policy'; @@ -17,7 +18,7 @@ export function registerNativeVerificationProfile(root: string, profile: string) if (previous.length >= 20 || previous.includes(profile) || dirname(profile) !== dirname(root) || profile === root || !/^copilot-verify-runtime-[A-Za-z0-9]+$/u.test(basename(profile)) || realpathSync(profile) !== profile || !lstatSync(profile).isDirectory() || lstatSync(profile).isSymbolicLink()) throw new VerificationSetupError('handoff-invalid'); - writeFileSync(join(profile, 'native-session-owner'), root, { mode: 0o600, flag: 'wx' }); + writeFileSync(join(profile, 'native-session-owner'), createHash('sha256').update(root).digest('hex'), { mode: 0o600, flag: 'wx' }); writeNativeRecord(root, 'profiles.json', [...previous, profile]); } @@ -30,7 +31,7 @@ export function removeNativeVerificationProfiles(root: string): void { for (const path of nativeVerificationProfilePaths(root)) { let stat; try { stat = lstatSync(path); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue; throw error; } - if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync(path) !== path || readFileSync(join(path, 'native-session-owner'), 'utf8') !== root) throw new VerificationSetupError('handoff-invalid'); + if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync(path) !== path || readFileSync(join(path, 'native-session-owner'), 'utf8') !== createHash('sha256').update(root).digest('hex')) throw new VerificationSetupError('handoff-invalid'); rmSync(path, { recursive: true, force: true }); } } diff --git a/src/infrastructure/native_verification_protocol.ts b/src/infrastructure/native_verification_protocol.ts index 45718b84c..086e0754f 100644 --- a/src/infrastructure/native_verification_protocol.ts +++ b/src/infrastructure/native_verification_protocol.ts @@ -14,6 +14,7 @@ export interface NativeVerificationSession { export interface NativeVerificationRequest { readonly id: string; readonly sourceSha: string; + readonly digest: string; readonly environment: Record; } export interface NativeVerificationResponse { diff --git a/src/infrastructure/native_verification_source_runtime.ts b/src/infrastructure/native_verification_source_runtime.ts new file mode 100644 index 000000000..ce00f01d8 --- /dev/null +++ b/src/infrastructure/native_verification_source_runtime.ts @@ -0,0 +1,15 @@ +import { existsSync, lstatSync, readFileSync, realpathSync, rmSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { VerificationSetupError } from '../application/policies/verification_setup_policy'; + +/** Only source workflows use this snapshot; installed remote Actions already live outside the candidate. */ +export function cleanupNativeSourceRuntime(root: string, workspace: string): void { + if (!root || !existsSync(root)) return; + const allowed = ['.copilot-native-runtime', '.copilot-native-fixture-runtime'].map(name => join(dirname(workspace), name)); + if (!allowed.includes(root) || lstatSync(root).isSymbolicLink() || realpathSync(root) !== root) throw new VerificationSetupError('handoff-invalid'); + let receipt: {kind?: string; workspace?: string}; + try { receipt = JSON.parse(readFileSync(join(root, '.copilot-runtime-snapshot.json'), 'utf8')) as typeof receipt; } + catch { throw new VerificationSetupError('handoff-invalid'); } + if (receipt?.kind !== 'copilot-native-runtime' || receipt.workspace !== workspace) throw new VerificationSetupError('handoff-invalid'); + rmSync(root, { recursive: true, force: true }); +} diff --git a/src/infrastructure/native_verification_staging.ts b/src/infrastructure/native_verification_staging.ts new file mode 100644 index 000000000..08967a8e4 --- /dev/null +++ b/src/infrastructure/native_verification_staging.ts @@ -0,0 +1,41 @@ +import { lstatSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; +import { dirname, join, relative, sep } from 'node:path'; +import type { PreparedVerificationSetup } from '../application/ports/verification_setup_ports'; +import { VerificationSetupError } from '../application/policies/verification_setup_policy'; +import { readNativeRecord, writeNativeRecord } from './native_verification_protocol'; + +export const NATIVE_VERIFICATION_ACTION = '.git/copilot-verification-setup'; +interface StagingRecord { readonly target: string } + +function targetPath(workspace: string): string { + const metadata = join(workspace, '.git'); + const stat = lstatSync(metadata); + if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync(metadata) !== metadata) throw new VerificationSetupError('native-workflow-required'); + return join(workspace, NATIVE_VERIFICATION_ACTION); +} + +/** Native composite metadata must remain available until its deferred post steps have finished. */ +export function stageNativeVerificationAction(setup: Extract, session: string): void { + const target = targetPath(setup.workspace); + const record = readNativeRecord(session, 'staging.json'); + if (record && record.target !== target || !record && !!lstatSync(target, { throwIfNoEntry: false })) throw new VerificationSetupError('handoff-invalid'); + writeNativeRecord(session, 'staging.json', { target }); + rmSync(target, { recursive: true, force: true }); + mkdirSync(target, { mode: 0o700 }); + for (const file of setup.files) { + const destination = join(target, file.path); + const remainder = relative(target, destination); + if (!remainder || remainder.startsWith(`..${sep}`) || remainder === '..') throw new VerificationSetupError('helper-invalid'); + mkdirSync(dirname(destination), { recursive: true, mode: 0o700 }); + writeFileSync(destination, Buffer.from(file.base64, 'base64'), { mode: file.executable ? 0o700 : 0o600, flag: 'wx' }); + } +} + +export function cleanupNativeVerificationAction(session: string, workspace: string): void { + const record = readNativeRecord(session, 'staging.json'); + if (!record) return; + const target = targetPath(workspace); + if (record.target !== target) throw new VerificationSetupError('handoff-invalid'); + rmSync(target, { recursive: true, force: true }); + rmSync(join(session, 'staging.json'), { force: true }); +} diff --git a/src/infrastructure/verification_executable.ts b/src/infrastructure/verification_executable.ts new file mode 100644 index 000000000..ffcaa52d0 --- /dev/null +++ b/src/infrastructure/verification_executable.ts @@ -0,0 +1,17 @@ +import { accessSync, constants, statSync } from 'node:fs'; +import { delimiter, isAbsolute, join, resolve } from 'node:path'; + +/** @actions/exec resolves basenames against its own process PATH; native tools belong to the verifier PATH. */ +export function verificationExecutable(program: string, environment: Record, platform = process.platform): string { + const explicit = isAbsolute(program) || /[\\/]/u.test(program); + const candidates = explicit ? [resolve(program)] : (environment.PATH ?? '').split(delimiter).filter(Boolean).map(directory => resolve(join(directory, program))); + const windowsExtensions = (environment.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean); + const hasWindowsExtension = windowsExtensions.some(extension => program.toLowerCase().endsWith(extension.toLowerCase())); + const extensions = platform === 'win32' && !hasWindowsExtension ? windowsExtensions : ['']; + for (const candidate of candidates) for (const extension of extensions) { + const file = candidate + extension; + try { if (statSync(file).isFile()) { accessSync(file, constants.X_OK); return file; } } + catch { /* Try only the next executable in the isolated PATH. */ } + } + throw new Error('Verification executable is unavailable in the isolated PATH.'); +} diff --git a/src/infrastructure/verification_setup_runner.ts b/src/infrastructure/verification_setup_runner.ts index cf7112832..7b8f801b6 100644 --- a/src/infrastructure/verification_setup_runner.ts +++ b/src/infrastructure/verification_setup_runner.ts @@ -1,3 +1,4 @@ +import { verificationSetupDigest } from './verification_setup_snapshot'; import { registerNativeVerificationProfile, nativeVerificationProfilePaths } from './native_verification_profiles'; import { realpathSync, rmSync, writeFileSync } from 'node:fs'; import { delimiter, join, relative, isAbsolute, sep } from 'node:path'; @@ -7,7 +8,7 @@ import { VerificationSetupError, VERIFICATION_SETUP_LIMITS, VERIFICATION_SETUP_P import { applyVerificationSetupResult } from './verification_setup_result'; import { logInfo } from '../application/ports/logging_ports'; import { nativePause, nativeSession, readNativeRecord, writeNativeRecord, type NativeVerificationResponse } from './native_verification_protocol'; -import { stageNativeVerificationAction, restoreNativeVerificationAction } from './native_verification_overlay'; +import { stageNativeVerificationAction } from './native_verification_staging'; import { verificationCandidateIdentity } from './native_verification_candidate'; @@ -27,7 +28,7 @@ export async function prepareRepositoryVerification(setup: PreparedVerificationS const identity = verificationCandidateIdentity(setup.workspace, environment); stageNativeVerificationAction(setup, session); const id = randomUUID(); - writeNativeRecord(session, 'request.json', { id, sourceSha: setup.sourceSha, environment }); + writeNativeRecord(session, 'request.json', { id, sourceSha: setup.sourceSha, digest: verificationSetupDigest(setup), environment }); logInfo(`Preparing repository verification (${VERIFICATION_SETUP_PATH}; trusted source SHA: ${setup.sourceSha}).`); const deadline = Date.now() + VERIFICATION_SETUP_LIMITS.timeoutMs; let response: NativeVerificationResponse | undefined; @@ -36,7 +37,6 @@ export async function prepareRepositoryVerification(setup: PreparedVerificationS if (Date.now() >= deadline) throw new VerificationSetupError('timeout'); await nativePause(100); } - restoreNativeVerificationAction(session, setup.workspace); if (response.id !== id || response.success !== true) throw new VerificationSetupError('process-failed'); if (verificationCandidateIdentity(setup.workspace, environment) !== identity) throw new VerificationSetupError('candidate-changed'); if (typeof response.path !== 'string' || response.path.length > 16_384 || response.javaHome !== undefined && typeof response.javaHome !== 'string') throw new VerificationSetupError('result-invalid'); @@ -55,7 +55,6 @@ export async function prepareRepositoryVerification(setup: PreparedVerificationS } catch (error) { throw new VerificationSetupError(error instanceof VerificationSetupError ? error.reason : 'handoff-invalid', setup.sourceSha); } finally { - restoreNativeVerificationAction(session, setup.workspace); rmSync(join(session, 'request.json'), { force: true }); } } diff --git a/src/infrastructure/verification_setup_snapshot.ts b/src/infrastructure/verification_setup_snapshot.ts index cc7e85457..90314f7d9 100644 --- a/src/infrastructure/verification_setup_snapshot.ts +++ b/src/infrastructure/verification_setup_snapshot.ts @@ -1,3 +1,4 @@ +import { isAbsolute } from 'node:path'; import { spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { realpathSync } from 'node:fs'; @@ -8,7 +9,20 @@ import { VERIFICATION_SETUP_DIRECTORY, VERIFICATION_SETUP_LIMITS, VerificationSe import { prepareUntrustedCommandEnvironment } from './untrusted_command_environment'; /** Only immutable Git objects are read. Errors remain deferred on non-verifying routes. */ +export function verificationSetupDigest(setup: Extract): string { + return createHash('sha256').update(JSON.stringify(setup.files)).digest('hex'); +} + export function captureVerificationSetup(enabled: boolean, mainBranch: string, root = process.cwd()): PreparedVerificationSetup { + return capture(enabled, mainBranch, root); +} + +/** Post steps use the already pinned commit from runner state, never a mutable branch ref. */ +export function captureVerificationSetupAtCommit(commit: string, root = process.cwd(), gitExecutable?: string): PreparedVerificationSetup { + return capture(true, '', root, commit, gitExecutable); +} + +function capture(enabled: boolean, mainBranch: string, root: string, pinned?: string, gitExecutable?: string): PreparedVerificationSetup { if (!enabled) return DISABLED_VERIFICATION_SETUP; let sourceSha: string | undefined; let runtime: ReturnType | undefined; @@ -16,16 +30,17 @@ export function captureVerificationSetup(enabled: boolean, mainBranch: string, r try { runtime = prepareUntrustedCommandEnvironment(); const environment = runtime.environment; - if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/u.test(mainBranch) || mainBranch.includes('..') - || mainBranch.includes('//') || mainBranch.endsWith('/') || mainBranch.endsWith('.lock')) throw new Error(); + if (!pinned && (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/u.test(mainBranch) || mainBranch.includes('..') + || mainBranch.includes('//') || mainBranch.endsWith('/') || mainBranch.endsWith('.lock'))) throw new Error(); + if (gitExecutable !== undefined && !isAbsolute(gitExecutable)) throw new Error(); const workspace = realpathSync(root); const git = (...args: string[]): Buffer => { - const result = spawnSync('git', ['--no-replace-objects', ...args], { cwd: workspace, env: environment, + const result = spawnSync(gitExecutable ?? 'git', ['--no-replace-objects', ...args], { cwd: workspace, env: environment, shell: false, windowsHide: true, timeout: 5_000, maxBuffer: VERIFICATION_SETUP_LIMITS.snapshotBytes + 4096 }); if (result.error || result.status !== 0) throw new Error(); return result.stdout; }; - const sha = git('rev-parse', '--verify', `refs/remotes/origin/${mainBranch}^{commit}`).toString('utf8').trim(); + const sha = pinned ?? git('rev-parse', '--verify', `refs/remotes/origin/${mainBranch}^{commit}`).toString('utf8').trim(); if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(sha)) throw new Error(); sourceSha = sha; const entries = git('ls-tree', '-rz', sourceSha, '--', VERIFICATION_SETUP_DIRECTORY).toString('utf8').split('\0').filter(Boolean); diff --git a/src/tooling/__tests__/fixtures/verification_native_fixture.ts b/src/tooling/__tests__/fixtures/verification_native_fixture.ts index f55e4d339..835e7402c 100644 --- a/src/tooling/__tests__/fixtures/verification_native_fixture.ts +++ b/src/tooling/__tests__/fixtures/verification_native_fixture.ts @@ -13,7 +13,9 @@ runs: using: composite steps: - uses: actions/setup-python@v6 + id: python with: + update-environment: false python-version: '3.12' - uses: pnpm/action-setup@v5 with: @@ -22,7 +24,7 @@ runs: standalone: false - name: Install candidate test dependencies privately run: | - python - <<'PYTHON' + "$PYTHON_EXECUTABLE" - <<'PYTHON' import os, pathlib, subprocess, venv home = pathlib.Path(os.environ['HOME']) / 'venv' venv.create(home, with_pip=True) @@ -33,6 +35,8 @@ runs: print(tools, file=paths) PYTHON shell: bash + env: + PYTHON_EXECUTABLE: \${{ steps.python.outputs.python-path }} - run: python -c "from pathlib import Path; assert Path('.copilot-native-candidate').read_text() == 'after-agent'" shell: bash `; @@ -63,6 +67,7 @@ async function fixture(): Promise { await adapter.execute('python', ['-m', 'pytest', '-q', testFile, '-p', 'no:cacheprovider'], { untrusted: true, verifyRuntimePrerequisites: true }); await adapter.execute('pnpm', ['--version'], { untrusted: true, verifyRuntimePrerequisites: true }); if (readFileSync(VERIFICATION_SETUP_PATH, 'utf8') !== candidateYaml) throw new Error('Candidate action was not restored'); + writeFileSync(join(process.cwd(), '.github/actions/copilot-native-fixture/dist/index.js'), 'throw new Error("Candidate runtime must never execute during continuation or post");'); core.info('Native composite acceptance passed: candidate manifests, trusted snapshot, Python/pytest and Node/pnpm, two fresh profiles.'); } finally { rmSync(candidateAction, { recursive: true, force: true }); rmSync(marker, { force: true }); rmSync(testFile, { force: true }); rmSync(requirements, { force: true }); diff --git a/src/tooling/__tests__/validate_workflow_contract.test.ts b/src/tooling/__tests__/validate_workflow_contract.test.ts index d93f6a1a7..e050dcb1f 100644 --- a/src/tooling/__tests__/validate_workflow_contract.test.ts +++ b/src/tooling/__tests__/validate_workflow_contract.test.ts @@ -156,7 +156,7 @@ describe('workflow contract validator', () => { it.each(['issue-managed-branches', 'pre-branch-sdd', 'verification-setup'])( 'rejects a PR comment callback that omits or disables the installed %s policy', input => { const { file, workflow } = loadMutationWorkflow('.github/workflows', 'copilot_pull_request_comment.yml'); - const action = workflow.jobs['copilot-pull-requests'].steps.find((step: { uses?: string }) => step.uses === './'); + const action = workflow.jobs['copilot-pull-requests'].steps.find((step: { id?: string }) => step.id === 'copilot'); delete action.with[input]; expect(() => assertIssueBranchConfigurationInputs(file, workflow)).toThrow('must pass consistent'); action.with[input] = false; From 41bfdc128731d5dabb6ce2a0182a3a39e46fbda5 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 21 Oct 2026 18:16:55 +0200 Subject: [PATCH 5/8] test: identify native fixture candidate changes on Windows --- .../__tests__/fixtures/verification_native_fixture.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tooling/__tests__/fixtures/verification_native_fixture.ts b/src/tooling/__tests__/fixtures/verification_native_fixture.ts index 835e7402c..5a140579c 100644 --- a/src/tooling/__tests__/fixtures/verification_native_fixture.ts +++ b/src/tooling/__tests__/fixtures/verification_native_fixture.ts @@ -69,6 +69,12 @@ async function fixture(): Promise { if (readFileSync(VERIFICATION_SETUP_PATH, 'utf8') !== candidateYaml) throw new Error('Candidate action was not restored'); writeFileSync(join(process.cwd(), '.github/actions/copilot-native-fixture/dist/index.js'), 'throw new Error("Candidate runtime must never execute during continuation or post");'); core.info('Native composite acceptance passed: candidate manifests, trusted snapshot, Python/pytest and Node/pnpm, two fresh profiles.'); + } catch (error) { + // This controlled fixture contains no credentials. File names, rather than file contents, + // identify dependency installers that unexpectedly alter the publishable candidate. + core.info(`Native fixture candidate status: ${git('status', '--porcelain=v1').slice(0, 8_192)}`); + core.info(`Native fixture tracked changes: ${git('diff', '--no-ext-diff', '--no-textconv', '--numstat', 'HEAD', '--').slice(0, 8_192)}`); + throw error; } finally { rmSync(candidateAction, { recursive: true, force: true }); rmSync(marker, { force: true }); rmSync(testFile, { force: true }); rmSync(requirements, { force: true }); rmSync(join(process.cwd(), '__pycache__'), { recursive: true, force: true }); From 448775417afd8559df96b2d6d4f6903c68ba480f Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 21 Oct 2026 18:20:32 +0200 Subject: [PATCH 6/8] fix: keep Python setup caches out of the candidate tree --- docs/security-operations/operations/verification.mdx | 5 ++++- .../__tests__/fixtures/verification_native_fixture.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/security-operations/operations/verification.mdx b/docs/security-operations/operations/verification.mdx index 2b2f080e2..84be5a499 100644 --- a/docs/security-operations/operations/verification.mdx +++ b/docs/security-operations/operations/verification.mdx @@ -181,7 +181,7 @@ runs: tools = home / ('Scripts' if os.name == 'nt' else 'bin') python = tools / ('python.exe' if os.name == 'nt' else 'python') subprocess.check_call([str(python), '-m', 'pip', 'install', - '--require-hashes', '-r', 'requirements-test.lock']) + '--no-cache-dir', '--require-hashes', '-r', 'requirements-test.lock']) with open(os.environ['GITHUB_PATH'], 'a') as paths: print(tools, file=paths) PYTHON @@ -193,6 +193,9 @@ runs: Keep pytest and all transitive dependencies pinned with hashes in the candidate's `requirements-test.lock`. The verifier can remain `python -m pytest`. Adapt runtime versions and dependency commands to your project; these examples do not infer them. +The example disables pip's cache so platform-specific cache discovery cannot create +publishable files in the candidate. To use a cache, set its path explicitly under +the private HOME or an ignored dependency directory. For Node/pnpm: diff --git a/src/tooling/__tests__/fixtures/verification_native_fixture.ts b/src/tooling/__tests__/fixtures/verification_native_fixture.ts index 5a140579c..3f8f77ec2 100644 --- a/src/tooling/__tests__/fixtures/verification_native_fixture.ts +++ b/src/tooling/__tests__/fixtures/verification_native_fixture.ts @@ -30,7 +30,7 @@ runs: venv.create(home, with_pip=True) tools = home / ('Scripts' if os.name == 'nt' else 'bin') python = tools / ('python.exe' if os.name == 'nt' else 'python') - subprocess.check_call([str(python), '-m', 'pip', 'install', '--disable-pip-version-check', '-r', 'requirements-native-fixture.txt']) + subprocess.check_call([str(python), '-m', 'pip', 'install', '--no-cache-dir', '--disable-pip-version-check', '-r', 'requirements-native-fixture.txt']) with open(os.environ['GITHUB_PATH'], 'a') as paths: print(tools, file=paths) PYTHON From 920335ab91cca2bedcd2e81c7e4a13e54b26ef23 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 21 Oct 2026 18:27:18 +0200 Subject: [PATCH 7/8] test: remove generated fixture bundle after deferred post steps --- .github/actions/copilot-native-cleanup-check/post.cjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/actions/copilot-native-cleanup-check/post.cjs b/.github/actions/copilot-native-cleanup-check/post.cjs index 6348ce94d..5e7aec929 100644 --- a/.github/actions/copilot-native-cleanup-check/post.cjs +++ b/.github/actions/copilot-native-cleanup-check/post.cjs @@ -1,6 +1,9 @@ const fs = require('node:fs'); const path = require('node:path'); const workspace = process.env.STATE_workspace; +// Keep the poisoned candidate bundle through every deferred post step, then remove +// only this job's ignored generated fixture output so subsequent runs start clean. +fs.rmSync(path.join(workspace, '.github/actions/copilot-native-fixture/dist'), { recursive: true, force: true }); for (const file of [path.join(workspace, '.git/copilot-verification-setup'), path.join(path.dirname(workspace), '.copilot-native-fixture-runtime')]) { if (fs.existsSync(file)) throw new Error('Native acceptance left owned staging or runtime snapshots after post cleanup.'); } From 5ba7f589c0babf03c5cc483201745423ed304492 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 21 Oct 2026 18:45:25 +0200 Subject: [PATCH 8/8] fix: bind verification readiness to untracked file modes --- build/cli/index.js | 4 ++-- build/github_action/index.js | 4 ++-- docs/security-operations/operations/verification.mdx | 2 +- .../__tests__/native_verification_runner.test.ts | 8 ++++++++ .../__tests__/verification_setup_sync.test.ts | 2 +- src/infrastructure/native_verification_candidate.ts | 4 ++-- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/build/cli/index.js b/build/cli/index.js index 3230732a4..c4b0d3a60 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -98284,9 +98284,9 @@ function verificationCandidateIdentity(workspace, environment) { throw new verification_setup_policy_1.VerificationSetupError('candidate-changed'); hash.update(path).update('\0'); if (stat.isSymbolicLink()) - hash.update((0, node_fs_1.readlinkSync)(file)); + hash.update('120000\0').update((0, node_fs_1.readlinkSync)(file)); else if (stat.isFile()) - hash.update((0, node_fs_1.readFileSync)(file)); + hash.update(stat.mode & 0o100 ? '100755\0' : '100644\0').update((0, node_fs_1.readFileSync)(file)); else throw new verification_setup_policy_1.VerificationSetupError('candidate-changed'); hash.update('\0'); diff --git a/build/github_action/index.js b/build/github_action/index.js index 9bab40461..5a70afc29 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -88381,9 +88381,9 @@ function verificationCandidateIdentity(workspace, environment) { throw new verification_setup_policy_1.VerificationSetupError('candidate-changed'); hash.update(path).update('\0'); if (stat.isSymbolicLink()) - hash.update((0, node_fs_1.readlinkSync)(file)); + hash.update('120000\0').update((0, node_fs_1.readlinkSync)(file)); else if (stat.isFile()) - hash.update((0, node_fs_1.readFileSync)(file)); + hash.update(stat.mode & 0o100 ? '100755\0' : '100644\0').update((0, node_fs_1.readFileSync)(file)); else throw new verification_setup_policy_1.VerificationSetupError('candidate-changed'); hash.update('\0'); diff --git a/docs/security-operations/operations/verification.mdx b/docs/security-operations/operations/verification.mdx index 84be5a499..baadf536a 100644 --- a/docs/security-operations/operations/verification.mdx +++ b/docs/security-operations/operations/verification.mdx @@ -259,7 +259,7 @@ pinned trusted SHA, never copied raw setup output. Setup must install dependencies without changing publishable code or lockfiles. Copilot compares HEAD, branch, index, tracked edits and publishable untracked file -contents before allowing the verifier to continue. Executable lookup uses that verifier's +contents, types and executable modes before allowing the verifier to continue. Executable lookup uses that verifier's validated PATH, so its private Python/pnpm takes precedence over the worker tools. Observations are bounded to 16 MiB, 256 untracked files and 16 MiB aggregate untracked contents. Ignore dependency caches and keep source changes within these bounds. Failure, changed candidate, diff --git a/src/infrastructure/__tests__/native_verification_runner.test.ts b/src/infrastructure/__tests__/native_verification_runner.test.ts index b44ac5e94..e0adfaa24 100644 --- a/src/infrastructure/__tests__/native_verification_runner.test.ts +++ b/src/infrastructure/__tests__/native_verification_runner.test.ts @@ -82,6 +82,14 @@ describe('native verification boundary', () => { f.respond(); await expect(running).rejects.toMatchObject({ reason: 'candidate-changed' }); expect(fs.existsSync(f.action)).toBe(false); }); + (process.platform === 'win32' ? it.skip : it).each(['executable mode', 'file type'])('withholds verification when setup changes an untracked candidate %s without changing its bytes', async kind => { + const file = join(f.root, 'new-script'); + fs.writeFileSync(file, 'same'); fs.chmodSync(file, 0o644); + const running = prepareRepositoryVerification(f.setup, f.runtime.environment); + if (kind === 'executable mode') fs.chmodSync(file, 0o755); + else { fs.rmSync(file); fs.symlinkSync('same', file); } + f.respond(); await expect(running).rejects.toMatchObject({ reason: 'candidate-changed' }); + }); it.each([{success: false}, {id: 'wrong'}, {path: 1}, {javaHome: 1}, {path: '/outside/invalid'}])('rejects failed or malformed native outcomes %j', async response => { const running = prepareRepositoryVerification(f.setup, f.runtime.environment); f.respond(() => response); await expect(running).rejects.toThrow('Verification, commit and push are withheld'); diff --git a/src/infrastructure/__tests__/verification_setup_sync.test.ts b/src/infrastructure/__tests__/verification_setup_sync.test.ts index 9e60d4d06..31e3047e0 100644 --- a/src/infrastructure/__tests__/verification_setup_sync.test.ts +++ b/src/infrastructure/__tests__/verification_setup_sync.test.ts @@ -102,7 +102,7 @@ describe('repository setup during same-managed-branch synchronization', () => { expect(git('show', `${remoteHead()}:${VERIFICATION_SETUP_PATH}`)).toContain('candidate helper must never execute'); expect(git('--git-dir', remote, 'for-each-ref', '--format=%(refname)', 'refs/heads').split('\n').sort()) .toEqual(['refs/heads/develop', `refs/heads/${branch}`, 'refs/heads/main']); - }); + }, process.platform === 'win32' ? 30_000 : 5_000); it('skips setup in dry runs even when the trusted helper is invalid', async () => { git('update-ref', 'refs/remotes/origin/main', childSha); const result = await synchronize(true, ['node verify.mjs'], true); diff --git a/src/infrastructure/native_verification_candidate.ts b/src/infrastructure/native_verification_candidate.ts index 1c60e851e..f65d61132 100644 --- a/src/infrastructure/native_verification_candidate.ts +++ b/src/infrastructure/native_verification_candidate.ts @@ -27,8 +27,8 @@ export function verificationCandidateIdentity(workspace: string, environment: Re total += stat.size; if (total > 16 * 1024 * 1024) throw new VerificationSetupError('candidate-changed'); hash.update(path).update('\0'); - if (stat.isSymbolicLink()) hash.update(readlinkSync(file)); - else if (stat.isFile()) hash.update(readFileSync(file)); + if (stat.isSymbolicLink()) hash.update('120000\0').update(readlinkSync(file)); + else if (stat.isFile()) hash.update(stat.mode & 0o100 ? '100755\0' : '100644\0').update(readFileSync(file)); else throw new VerificationSetupError('candidate-changed'); hash.update('\0'); }