Commit aded37e
feat(copilot-app): hybrid WS-IPC + SQLite project scoping for workflows (#1431)
* deps: add websockets>=12 for Copilot App IPC
Required by the hybrid project-scoping path (PR A). When the App is
running, APM prefers the localhost WS-IPC surface so workflow rows
fire the live WorkflowsChanged broadcast instead of relying on the
webview re-reading SQLite.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(copilot-app): add project resolver with SQLite fallback
New module copilot_app_project.py: frozen dataclasses for RepoContext /
ProjectRecipe / ResolvedProject, pure derivers (derive_repo_context,
derive_project_recipe), and resolve_or_register_project_sqlite that
SELECTs by main_repo_path or INSERTs a fresh row inside BEGIN
IMMEDIATE. Race-collision recovery: on sqlite3.IntegrityError, re-
SELECT inside the same transaction to return the winning id.
Backed by 14 unit tests covering the no-repo / HTTPS / SSH / non-
github / subdir-walk-up derive paths and HIT / MISS / external-row /
race-recovery / missing-db / schema-too-new resolver branches.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(copilot-app): add sync WS-IPC client for live App integration
New module copilot_app_ws.py: WsClient context manager using
websockets.sync.client (no asyncio in install loop), ws_available()
liveness probe bounded to 100ms, and create_project_from_path /
create_workflow / update_workflow RPCs. Custom exceptions
(WsAppNotRunning / WsAuthError / WsProtocolError) let the caller
distinguish silent fallback from warn-and-fallback.
Reads ~/.copilot/run/ws.{port,token}, presents Origin: tauri://
localhost (the App rejects any other Origin), and walks past
interleaved server push frames to find the response to our request.
Permissive JSON parser tolerates unknown / added top-level fields so
upstream schema additions don't break us.
Backed by 19 unit tests against a real websockets.sync.server harness
covering handshake / auth / origin / round-trip / drain / timeout.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(copilot-app): stamp project_id on every workflow row
Adds project_id: str | None to WorkflowRow and threads it through both
INSERT and UPDATE paths in deploy_workflow. The execution-changed
UPDATE includes project_id alongside the other execution fields; the
name-only UPDATE also writes project_id so pre-PR-A rows with NULL
project_id self-heal on the next install without forcing a content
change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(copilot-app): hybrid WS+SQLite dispatch with project scoping
prompt_integrator.integrate_prompts_for_target now accepts scope= and
forwards user-scope detection to _integrate_prompts_for_copilot_app,
which:
1. Parses all candidate workflow-shape prompts up front so the path
decision (WS / SQLite / global-warn) is made once per package.
2. Emits a warn-and-proceed diagnostic when --global is combined with
workflow-shape prompts, explaining that those workflows will run
with CWD=~/.copilot and pointing at the App's Workflows tab to
attach them to a project manually.
3. Derives a RepoContext from project_root; when the App is running
AND we have a real repo, tries the WS-IPC path first
(create_project_from_path + per-workflow create_workflow). On any
WsError, warns and falls back to SQLite.
4. SQLite fallback: resolve_or_register_project_sqlite, stamp
project_id on every WorkflowRow, suffix display name with
' (<repo>)' so workflows from different repos are distinguishable
in the App UI.
5. When the project row was freshly created (was_created=True), emits
a one-time restart-the-App info diagnostic referencing the upstream
live-refresh gap (github/github-app#5483).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(install): thread scope into integrator dispatch
services.py now passes scope= to integrate_*_for_target so the
prompt integrator can distinguish project-scope from user-scope at
the copilot-app branch. The four sibling integrators (agent, command,
hook, instruction) accept scope= as a keyword-only no-op default to
keep the call site uniform without changing their behavior.
Also refreshes the copilot-app explainer in phases/targets.py to
document the PR A auto-register + restart-once UX.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(copilot-app): cover project_id stamping, restart hint, global warn
- Extends the seed schema fixture with a projects table so the
resolver can run end-to-end.
- Asserts on the project-scope roundtrip that the workflow row carries
a non-NULL project_id pointing at a projects row with main_repo_path
equal to the consumer directory, and that the display name carries
the '(<repo>)' suffix.
- git init's the consumer directory so derive_repo_context returns a
real RepoContext.
- Asserts the restart-the-App hint is emitted on the first install
into a new repo.
- New test_global_install_with_workflow_emits_warning: --global plus
workflow-shape prompts warn-and-proceeds (exit 0, row inserted,
warning mentions --global + 'attach') instead of the prior hard-fail.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(copilot-app): address panel follow-ups on PR #1431
Path B narrow WS surface (project-registration only), token hardening,
fallback regression test, and doc/PR-body honesty pass.
Source:
- copilot_app_ws.py: delete create_workflow/update_workflow/
WorkflowCreated/_extract_workflow_id; add _scrub_token regex that
redacts ?token=... in wrapped exception text; add _token_file_mode_ok
check refusing ws.token if group/other-readable (matches App's 0o600
posture); raise WsError from None so library exceptions with the
unscrubbed URL are not reachable via __context__.
- prompt_integrator.py: rewrite _integrate_prompts_for_copilot_app as
a single-loop dispatcher. Resolve project_id once (try WS, fall back
to SQLite on WsError; silently swallow WsAppNotRunning and
WsAuthError so stale tokens after a restart never warn). Write every
workflow row via SQLite with the resolved project_id. One restart-
once hint when was_created. Drop the WS/IPC jargon from user-facing
wording. Delete _deploy_via_ws.
- pyproject.toml + uv.lock: pin websockets>=12,<17.
Tests:
- tests/unit/integration/test_copilot_app_ws.py: drop TestCreateWorkflow
+ TestUpdateWorkflow; port TestDrainAndInterleavedPush and
TestRecvTimeout onto create_project_from_path; chmod 0o600 in the
_write_creds fixture so the new mode check passes; add TestTokenScrub
(3) and TestTokenFileMode (3).
- tests/integration/test_copilot_app_ws_fallback.py (new): regression
trap for WS-error -> SQLite fallback. WsProtocolError mid-deploy
still stamps project_id on the workflow row; WsAppNotRunning and
WsAuthError stay silent.
Docs:
- CHANGELOG.md: corrected Unreleased entries (Added/Security).
- docs/.../integrations/copilot-app.md: Project scoping + One-time
restart hint + --global warn-and-proceed sections.
- packages/apm-guide/.apm/skills/apm-usage/commands.md: project-scope
+ restart-hint + --global warn callouts in the experimental section.
PR body: rewritten to drop misleading mode-allowlist bullet and the
'live workflows_changed broadcast' framing; sequence diagram now shows
WS for project registration and SQLite for workflow rows; scenario
table extended with WS-fallback and token-hardening evidence.
Validation: ruff check + format --check silent; 2766 unit
integration+install tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(copilot-app): extract workflow integrator + fold Copilot review nits
The copilot-app target deploys *.prompt.md as Copilot App workflow rows
(SQLite + optional WS-IPC) instead of files. That path shared NOTHING
with the file-based prompt deploy except the source artefact, but it
was squatting inside PromptIntegrator (~300 lines of unrelated logic
plus its own module-level frontmatter helpers). Extract into a sibling
integrator so the file matches how agent_integrator / command_integrator
/ hook_integrator / instruction_integrator are organised.
Refactor:
* New src/apm_cli/integration/copilot_app_workflow_integrator.py with
CopilotAppWorkflowIntegrator(BaseIntegrator). Inherits BaseIntegrator
for find_files_by_glob only -- the file-based collision /
link-resolution machinery is irrelevant on the workflow surface
(deploy_workflow is an UPSERT keyed on a namespaced id; sync deletes
by id from the lockfile).
* PromptIntegrator.integrate_prompts_for_target and .sync_for_target
keep one trivially small branch per method that delegates to the
workflow integrator -- grep copilot-app in prompt_integrator.py and
one call site appears for each direction.
* Module-level frontmatter helpers (_is_workflow_shape, Schedule,
_parse_workflow_frontmatter, _parse_schedule, _derive_package_owner)
move with the path that uses them. Re-exported from prompt_integrator
for back-compat with tests / external callers.
* copilot_app_db / copilot_app_project / copilot_app_ws stay put --
they are already at the right level. Only prompt_integrator was
oversized.
* Test fixtures unchanged; the integration test that patches
copilot_app_ws.WsClient.create_project_from_path still works because
the import path did not move.
Folded Copilot reviewer nits (PR #1431):
* B1: when derive_repo_context(project_root) returns None AND workflow
prompts are present AND scope is not user, emit a parallel warn to
the existing --global warn. project_id=NULL workflows have the same
CWD=~/.copilot pivot risk; the user deserves the same heads-up plus
the fix recipe (run inside a git repo or attach from the App UI).
* B2: WsClient.create_project_from_path docstring rewritten to match
the implementation -- was_created is inferred from the reply type
(project_created -> True, project_updated -> False) via
_extract_project_fields, not 'always True'. Restart hint only fires
on the project_created branch.
* B3: _find_repo_root docstring no longer claims symlinks-in-the-chain
are blocked. The cheap fix: drop the false claim. The 'right' fix
(walk + reject symlinked parents) would break macOS /tmp ->
/private/tmp and other legitimate symlinked-ancestor setups, and the
threat model (user's own CWD, not adversary input) does not justify
the cost. The .git-marker is_symlink() check stays as
defence-in-depth.
Behavior change: one new warn diagnostic when copilot-app workflows
install without a detected git repo. Three pre-existing unit tests in
test_copilot_app_error_ux.py updated to filter the new warn out of
their per-prompt deploy-error counts (the no-repo warn fires once for
the whole install; deploy-error warns are per-prompt).
Test gate: 54 targeted + 2766 broad -- all pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(copilot-app): dedupe DB write-prelude + add websockets NOTICE entry
- Extract _open_write_txn() helper in copilot_app_db so both
deploy_workflow and copilot_app_project.resolve_or_register_project
share the missing-DB / version-guard / BEGIN-IMMEDIATE prelude.
Closes the pylint R0801 duplicate-code finding without changing
behavior or error wording.
- Drop the now-unused _connect / _check_user_version /
_begin_immediate_with_retry imports from copilot_app_project.
- Update test_race_collision_resolves_to_winning_id to patch
copilot_app_db._connect (the real entry point) rather than the
project module re-export, which is gone.
- Add websockets entry to scripts/notice-metadata.yaml and regenerate
NOTICE so the drift check passes for the new runtime dependency.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>1 parent 66a9a11 commit aded37e
23 files changed
Lines changed: 3024 additions & 394 deletions
File tree
- docs/src/content/docs/integrations
- packages/apm-guide/.apm/skills/apm-usage
- scripts
- src/apm_cli
- install
- phases
- integration
- tests
- integration
- unit
- install
- integration
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
10 | 14 | | |
11 | 15 | | |
12 | 16 | | |
13 | 17 | | |
14 | 18 | | |
15 | 19 | | |
16 | 20 | | |
17 | | - | |
18 | 21 | | |
19 | 22 | | |
20 | 23 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1252 | 1252 | | |
1253 | 1253 | | |
1254 | 1254 | | |
| 1255 | + | |
| 1256 | + | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
| 1263 | + | |
| 1264 | + | |
| 1265 | + | |
| 1266 | + | |
| 1267 | + | |
| 1268 | + | |
| 1269 | + | |
| 1270 | + | |
| 1271 | + | |
| 1272 | + | |
| 1273 | + | |
| 1274 | + | |
| 1275 | + | |
| 1276 | + | |
| 1277 | + | |
| 1278 | + | |
| 1279 | + | |
| 1280 | + | |
| 1281 | + | |
| 1282 | + | |
| 1283 | + | |
| 1284 | + | |
| 1285 | + | |
| 1286 | + | |
| 1287 | + | |
| 1288 | + | |
| 1289 | + | |
| 1290 | + | |
| 1291 | + | |
| 1292 | + | |
| 1293 | + | |
| 1294 | + | |
1255 | 1295 | | |
1256 | 1296 | | |
1257 | 1297 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
81 | 116 | | |
82 | 117 | | |
83 | 118 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
141 | 141 | | |
142 | 142 | | |
143 | 143 | | |
144 | | - | |
| 144 | + | |
145 | 145 | | |
146 | 146 | | |
147 | 147 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| 40 | + | |
40 | 41 | | |
41 | 42 | | |
42 | 43 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
277 | 277 | | |
278 | 278 | | |
279 | 279 | | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
257 | 257 | | |
258 | 258 | | |
259 | 259 | | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
260 | 273 | | |
261 | 274 | | |
262 | 275 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
272 | 272 | | |
273 | 273 | | |
274 | 274 | | |
| 275 | + | |
275 | 276 | | |
276 | 277 | | |
277 | 278 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
| 97 | + | |
97 | 98 | | |
98 | 99 | | |
99 | 100 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
455 | 455 | | |
456 | 456 | | |
457 | 457 | | |
| 458 | + | |
458 | 459 | | |
459 | 460 | | |
460 | 461 | | |
| |||
0 commit comments