@polygraph/codex-plugin 0.4.43 → 0.4.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/agents/polygraph-delegate-subagent.toml +2 -2
- package/hooks/hooks.json +0 -10
- package/package.json +1 -1
- package/skills/polygraph/SKILL.md +22 -86
- package/skills/polygraph/reference/ci-job-logs.md +38 -0
- package/skills/polygraph/reference/sandboxing.md +33 -0
- package/skills/polygraph/reference/shallow-clone-history.md +9 -0
- package/hooks/check-plugin-version.mjs +0 -294
|
@@ -16,12 +16,12 @@ The main agent provides these parameters in the prompt:
|
|
|
16
16
|
| `sessionId` | The Polygraph session ID |
|
|
17
17
|
| `repo` | Repository to delegate to (e.g., `org/repo-name`) |
|
|
18
18
|
| `instruction` | The task instruction for the child agent |
|
|
19
|
-
| `role` | (Optional) Agent slot within the repo; omit for the default role. Pass the SAME role on every `spawn_agent`/`show_agent`/`stop_agent` call for this delegation. |
|
|
19
|
+
| `role` | (Optional) Agent slot within the repo; omit for the default role and never invent one. Pass the SAME role on every `spawn_agent`/`show_agent`/`stop_agent` call for this delegation. |
|
|
20
20
|
| `context` | (Optional) Additional context to pass to the child agent |
|
|
21
21
|
|
|
22
22
|
## Delegating work
|
|
23
23
|
|
|
24
|
-
Call the `spawn_agent` tool to start a child agent on the repo or to send a follow-up to an active task. Follow-up routing is automatic per (repo, role): if that (repo, role) already has an active child task (working or paused on input), the orchestrator delivers your `instruction` to that task as a follow-up message rather than starting a second run; otherwise it starts a new child run. A repo therefore has at most one active child per role
|
|
24
|
+
Call the `spawn_agent` tool to start a child agent on the repo or to send a follow-up to an active task. Follow-up routing is automatic per (repo, role): if that (repo, role) already has an active child task (working or paused on input), the orchestrator delivers your `instruction` to that task as a follow-up message rather than starting a second run; otherwise it starts a new child run. A repo therefore has at most one active child per role.
|
|
25
25
|
|
|
26
26
|
`repo` must be a repository other than the one the parent agent is working in — never delegate into the parent's own repo.
|
|
27
27
|
|
package/hooks/hooks.json
CHANGED
|
@@ -15,16 +15,6 @@
|
|
|
15
15
|
"statusMessage": "Recording Polygraph agent capture mapping"
|
|
16
16
|
}
|
|
17
17
|
]
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"matcher": "startup|resume",
|
|
21
|
-
"hooks": [
|
|
22
|
-
{
|
|
23
|
-
"type": "command",
|
|
24
|
-
"command": "node ${PLUGIN_ROOT}/hooks/check-plugin-version.mjs codex",
|
|
25
|
-
"statusMessage": "Checking Polygraph plugin version"
|
|
26
|
-
}
|
|
27
|
-
]
|
|
28
18
|
}
|
|
29
19
|
]
|
|
30
20
|
}
|
package/package.json
CHANGED
|
@@ -25,27 +25,9 @@ Polygraph connects repositories and the agent work happening across them. Its ce
|
|
|
25
25
|
|
|
26
26
|
## Sandboxing in Polygraph Sessions
|
|
27
27
|
|
|
28
|
-
Polygraph
|
|
28
|
+
Polygraph **may** run an agent session inside an OS-level sandbox, but not every session is sandboxed — whether it is on depends on the user's config. When it is on, writes are limited to the repository working tree and session root, network access is restricted to allowlisted hosts, and binding a listening socket (dev servers) fails with `EPERM`. The user may not know whether this session is sandboxed.
|
|
29
29
|
|
|
30
|
-
**
|
|
31
|
-
|
|
32
|
-
**Warn before attempting known-blocked operations.** Before starting a dev server, anything else that listens on a port, or an operation that needs writes or network access outside the allowlist, tell the user up front that it will not work while sandboxing is on and offer the options below instead of attempting it.
|
|
33
|
-
|
|
34
|
-
**What to tell the user.** Explain that Polygraph runs this session in a sandbox, then present both options. Each takes effect on the next agent launch, so the Polygraph session must be relaunched afterwards:
|
|
35
|
-
|
|
36
|
-
1. **Keep the sandbox on and allow the specific operation** (preferred). The sandbox belongs to the agent harness, so exceptions live in harness settings committed to the repository; array settings merge with Polygraph's generated allowlist rather than replacing it. Add writable roots or network access to `.codex/config.toml`:
|
|
37
|
-
|
|
38
|
-
```toml
|
|
39
|
-
[sandbox_workspace_write]
|
|
40
|
-
network_access = true
|
|
41
|
-
writable_roots = ["<path>"]
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
The `.codex/` directory is read-only inside the sandbox, so give the user the exact snippet to commit — you cannot apply it yourself.
|
|
45
|
-
|
|
46
|
-
2. **Turn sandboxing off.** The user quits the agent, runs `polygraph config`, toggles **Agent Options → Codex → sandbox** off (or per-repo under **Repo Options**), and relaunches the Polygraph session. Non-interactive alternative: set `agentOptions.codex.sandbox: false` (or `repoOptions."org/repo".sandbox: false`) in `~/.polygraph/config.json`. Warn that this removes filesystem isolation — the agent can then write anywhere the OS user can.
|
|
47
|
-
|
|
48
|
-
**Never blame the tooling.** A sandbox denial means the environment blocked the operation — not that the repo, tool, or framework is broken. Do not record conclusions like "X is unusable" in memory, session descriptions, or messages based on sandboxed failures.
|
|
30
|
+
**When something fails in a sandbox-shaped way** — `EPERM` binding a port, a blocked network host, a denied write to an ordinary path — the sandbox blocked it. Do NOT retry variations, work around it, or route the command through `!`-prefixed user commands (those run in the same sandbox); one failure is enough evidence. Stop, and read [`reference/sandboxing.md`](reference/sandboxing.md) for how to warn the user, the two remediation options (allow the specific operation via committed harness settings, or turn sandboxing off), and the exact per-harness config snippets. Never conclude the repo, tool, or framework is broken based on a sandboxed failure.
|
|
49
31
|
|
|
50
32
|
## Available Tools
|
|
51
33
|
|
|
@@ -55,24 +37,24 @@ Polygraph functionality is available via both MCP tools and CLI commands. Use wh
|
|
|
55
37
|
| --- | --- | --- |
|
|
56
38
|
| `list_repos` | `polygraph repo list` | Discover candidate repositories. Candidate entries do not include repository descriptions; use `semanticQuery` for natural-language discovery. |
|
|
57
39
|
| `start_session` | `polygraph session start --repo <ids>` | Initialize a Polygraph session with selected repositories |
|
|
58
|
-
| `spawn_agent` | — | Start a
|
|
59
|
-
| `show_agent` | — | Poll one repo's child status (
|
|
60
|
-
| `stop_agent` | — | Cancel an in-progress child
|
|
40
|
+
| `spawn_agent` | — | Start a child task, or send a follow-up to an active task, in another repository. A repeat call for the same (repo, role) is delivered to that task as a follow-up; otherwise a new child starts. Roles and resume behavior are under "Multi-turn tasks". |
|
|
41
|
+
| `show_agent` | — | Poll one repo's child status (one repo per call; `role` narrows to that agent). Status enum and the poll/state-machine flow are under "Multi-turn tasks". |
|
|
42
|
+
| `stop_agent` | — | Cancel an in-progress child; its session is preserved for later read-only context restoration. |
|
|
61
43
|
| `push_branch` | — | Push a local git branch to the remote repository. For the repo you are in, this pushes from your current checkout. Requires a session description. |
|
|
62
44
|
| `create_pr` | — | Create draft PRs with session metadata linking related PRs |
|
|
63
45
|
| `show_session` | `polygraph session show <id> [--details]` | Query status of the current session. Use details when session summary, repo IDs, PR URLs, and PR descriptions are needed. |
|
|
64
|
-
| `update_session` | `polygraph session update --session <id> [--title] [--description]` | Update the session title and/or description (at least one required)
|
|
46
|
+
| `update_session` | `polygraph session update --session <id> [--title] [--description]` | Update the session title and/or description (at least one required); metadata only, independent of PR creation or mark-ready. |
|
|
65
47
|
| `link_reference` | — | Link an external reference to a session. |
|
|
66
48
|
| `mark_pr_ready` | — | Mark draft PRs as ready for review |
|
|
67
49
|
| `associate_pr` | — | Associate an existing PR with a session |
|
|
68
|
-
| `add_repo` | — | Add repositories to a running
|
|
50
|
+
| `add_repo` | — | Add repositories to a running session (pass exact refs directly, skipping `list_repos`). See "Add Repositories to a Session". |
|
|
69
51
|
| `archive_session` | `polygraph session archive <id>` | Archive a session, hiding it from active lists (it can still be resumed) |
|
|
70
52
|
| `get_ci_logs` | — | Retrieve full plain-text log for a specific CI job |
|
|
71
|
-
| `git_fetch` | `polygraph git fetch` | Fetch
|
|
53
|
+
| `git_fetch` | `polygraph git fetch` | Fetch git history for a shallow session clone when git fails with "bad object" or missing-commit errors. See "Fetching Git History for Shallow Clones". |
|
|
72
54
|
| `login` | `polygraph auth login [--token]` | Authenticate with Polygraph (use `--token` for headless/CI) |
|
|
73
55
|
| `logout` | `polygraph auth logout` | Log out of Polygraph |
|
|
74
56
|
| `list_sessions` | `polygraph session list` | List sessions. By default only active sessions created by the current git user; pass `recommendedFilters: false` for all sessions. |
|
|
75
|
-
| `search_sessions` | `polygraph session search` | Find sessions by free-text `query` OR by commit `sha` — pass
|
|
57
|
+
| `search_sessions` | `polygraph session search` | Find sessions by free-text `query` OR by commit `sha` — pass exactly one. See "Finding the Session Behind a Commit or Line". |
|
|
76
58
|
| `list_accounts` | `polygraph account list` | List available organizations |
|
|
77
59
|
| `select_account` | `polygraph account select` | Select the organization that future commands run against |
|
|
78
60
|
| `whoami` | `polygraph whoami` | Show current auth status and org |
|
|
@@ -106,7 +88,7 @@ After logging in (or if logged in but no org is selected), use `polygraph accoun
|
|
|
106
88
|
The delegate/monitor/stop steps apply only when working across repos. A single-repo session skips them and still benefits from shared progress, resume, and CI visibility.
|
|
107
89
|
|
|
108
90
|
0. **Initialize or join Polygraph session** - If you were spawned inside an existing session (the startup banner names a session ID), reuse it. Call `show_session` first; if it already has repos and the user did not ask to add more, you're done. If the user asks to add exact repo refs, call `add_repo` directly with those refs and skip candidate discovery. If the session has no repos and no exact refs were provided, launch the `polygraph-init-subagent` with that `sessionId` so it discovers candidates and uses `add_repo` (NOT `start_session`). Only when there is no session ID at all should the init subagent create a new session.
|
|
109
|
-
1. **Delegate work to each repo** - Use the `polygraph-delegate-subagent` to start child agents in other repositories. Delegate only to *other* repos — never to the repo you are in; work on it directly (your regular subagents are fine for local work — only Polygraph delegation is reserved for other repos). Parallel delegation across repos is encouraged
|
|
91
|
+
1. **Delegate work to each repo** - Use the `polygraph-delegate-subagent` to start child agents in other repositories. Delegate only to *other* repos — never to the repo you are in; work on it directly (your regular subagents are fine for local work — only Polygraph delegation is reserved for other repos). Parallel delegation across repos is encouraged. Choose the Simple (fire-and-forget) or Multi-turn (interactive) pattern described below based on whether the child may need clarification.
|
|
110
92
|
|
|
111
93
|
4. **Monitor child agents** - Use `show_agent` to poll one repo's children (`repo` is required; pass `role` to narrow to one agent) and read each entry's `status` and `lastOutputLines` from the `children[]` array.
|
|
112
94
|
5. **Stop child agents** (if needed) - Use `stop_agent` (with `role` when targeting a non-default agent) to cancel an in-progress child agent. The agent's session is preserved for later read-only context restoration; after a resume, wait for explicit user instructions before making changes.
|
|
@@ -120,7 +102,7 @@ The delegate/monitor/stop steps apply only when working across repos. A single-r
|
|
|
120
102
|
|
|
121
103
|
## Step-by-Step Guide
|
|
122
104
|
|
|
123
|
-
###
|
|
105
|
+
### Initialize or Join Polygraph Session
|
|
124
106
|
|
|
125
107
|
There are three cases. Pick exactly one before calling any tool. The case labels are internal routing shorthand — never mention them in anything you show the user.
|
|
126
108
|
|
|
@@ -256,6 +238,7 @@ polygraph session search --sha a1b2c3d
|
|
|
256
238
|
|
|
257
239
|
A repository in a session can host multiple child agents at once, distinguished by **role**:
|
|
258
240
|
|
|
241
|
+
- **Omit by default.** Set a `role` only when the user very explicitly asked for a named one, or when a skill the user invoked prescribes one (e.g. `adversarial-review` uses `reviewer`). Never pick one yourself.
|
|
259
242
|
- **Purpose.** Roles let independent streams of work run concurrently in one repo — e.g. a default agent implementing a feature while a `reviewer` or `ci-investigator` runs alongside. Each (repo, role) pair has at most one active child.
|
|
260
243
|
- **Default role.** An omitted `role` means the default role: `spawn_agent` without `role` starts or follows up with the repo's default-role agent.
|
|
261
244
|
- **Logs.** Only default-role agents upload logs to the cloud and appear in the multiplexed log stream (`polygraph session logs`). Inspect non-default agents locally with `polygraph agent attach --role <role>`.
|
|
@@ -349,7 +332,9 @@ If you call `allow_agent` while the dialog is already open, you create a race: t
|
|
|
349
332
|
|
|
350
333
|
The `allow_agent` and `deny_agent` tools exist for parents whose MCP clients do NOT advertise elicitation capability (opencode TUI today). They are not part of your flow.
|
|
351
334
|
|
|
352
|
-
|
|
335
|
+
## Publishing and Session Management
|
|
336
|
+
|
|
337
|
+
### Publish Changes (Push Branches, Create PRs, Mark Ready)
|
|
353
338
|
|
|
354
339
|
Publishing covers the branch-to-PR flow: `push_branch` (push local commits; must precede PR creation), `create_pr` (linked draft PRs, including fork PRs via `targetRepository`), `mark_pr_ready` (transition drafts to OPEN), and `associate_pr` (link PRs created outside Polygraph).
|
|
355
340
|
|
|
@@ -361,7 +346,7 @@ Publishing covers the branch-to-PR flow: `push_branch` (push local commits; must
|
|
|
361
346
|
|
|
362
347
|
**Whenever you write or update a session description, read [`reference/session-description.md`](reference/session-description.md) first.** That reference file holds the full policy: the canonical Markdown-heading template (`## Goal` / `## Current progress` / `## What worked` / `## Next steps`), the dual-audience guidance (humans in the web UI now, agents reconstructing history later), and the formatting building blocks the app renders (callouts, tables, mermaid, links, `link_reference`).
|
|
363
348
|
|
|
364
|
-
###
|
|
349
|
+
### Get Current Polygraph Session
|
|
365
350
|
|
|
366
351
|
Check the details of a session using `show_session` or `polygraph session show --details <session-id>`. Returns the full session state — basic metadata like id, url & description timeline, plus the connected repositories, `pullRequests[]`, per-PR `ciStatus`, and `session.linkedReferences`.
|
|
367
352
|
|
|
@@ -419,7 +404,7 @@ link_reference({
|
|
|
419
404
|
|
|
420
405
|
The canonical MCP parameters are `{ sessionId, reference }`. There is no unlink command.
|
|
421
406
|
|
|
422
|
-
###
|
|
407
|
+
### Add Repositories to a Session
|
|
423
408
|
|
|
424
409
|
Use `add_repo` to add repositories to an existing Polygraph session after it has already started.
|
|
425
410
|
|
|
@@ -439,27 +424,11 @@ add_repo(
|
|
|
439
424
|
)
|
|
440
425
|
```
|
|
441
426
|
|
|
442
|
-
###
|
|
427
|
+
### Archive Session
|
|
443
428
|
|
|
444
429
|
**IMPORTANT: Only call this tool when the user explicitly asks to archive or close the session.** Do not archive sessions automatically as part of the workflow.
|
|
445
430
|
|
|
446
|
-
Use `archive_session` (CLI: `polygraph session archive <id>`) to archive the session. Archiving only hides the session from active lists — it can still be resumed and interacted with afterwards. It is idempotent — archiving an already-archived session returns success.
|
|
447
|
-
|
|
448
|
-
**Parameters:**
|
|
449
|
-
|
|
450
|
-
- `sessionId` (required): The Polygraph session ID
|
|
451
|
-
- `clean` (optional): Remove the local clones Polygraph created for delegated repos after archiving
|
|
452
|
-
|
|
453
|
-
**Returns:**
|
|
454
|
-
|
|
455
|
-
- `sessionId`: The session ID
|
|
456
|
-
- `completed`: Boolean indicating the session is archived
|
|
457
|
-
|
|
458
|
-
```
|
|
459
|
-
archive_session(
|
|
460
|
-
sessionId: "<session-id>"
|
|
461
|
-
)
|
|
462
|
-
```
|
|
431
|
+
Use `archive_session` (CLI: `polygraph session archive <id>`) to archive the session. Archiving only hides the session from active lists — it can still be resumed and interacted with afterwards. It is idempotent — archiving an already-archived session returns success. Pass the optional `clean` flag to also remove the local clones Polygraph created for delegated repos.
|
|
463
432
|
|
|
464
433
|
**When to call:** all work is finished, PRs are created and marked ready, and the user explicitly confirms they are done with the session.
|
|
465
434
|
|
|
@@ -467,46 +436,13 @@ archive_session(
|
|
|
467
436
|
|
|
468
437
|
### Retrieving CI Job Logs
|
|
469
438
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
**ONLY use this tool when NO CIPE (CI Pipeline Execution) exists for the PR.** When a CIPE exists (`ciStatus[prId].cipeUrl` is non-null), logs and failure data are available through the CIPE system (Nx Cloud) via the Nx MCP `ci_information` tool — do NOT call `get_ci_logs`, and do NOT fetch or poll the `cipeUrl` over HTTP (it is a browser link for the user, not an API). This tool is specifically for PRs where only external CI runs exist (e.g., GitHub Actions runs without an Nx Cloud CIPE).
|
|
473
|
-
|
|
474
|
-
**Parameters:**
|
|
475
|
-
|
|
476
|
-
- `sessionId` (required): The Polygraph session ID
|
|
477
|
-
- `repoId` (required): Repository ID (MongoDB ObjectId hex string, from the session repository entry)
|
|
478
|
-
- `jobId` (required): GitHub Actions job ID (from `ciStatus[prId].externalCIRuns[].jobs[].jobId` in the `show_session` response)
|
|
479
|
-
|
|
480
|
-
**Returns:**
|
|
481
|
-
|
|
482
|
-
- On success: `{ success: true, jobId: number, logFile: string, sizeBytes: number }`
|
|
483
|
-
- On failure: `{ success: false, error: string }`
|
|
484
|
-
|
|
485
|
-
The tool saves the log to a local temp file and returns the path in `logFile`. Use the `Read` tool to examine the file contents. For large logs, use `offset` and `limit` parameters to read specific sections.
|
|
486
|
-
|
|
487
|
-
```
|
|
488
|
-
get_ci_logs(
|
|
489
|
-
sessionId: "<session-id>",
|
|
490
|
-
repoId: "<repo-id>",
|
|
491
|
-
jobId: 12345678
|
|
492
|
-
)
|
|
493
|
-
// Returns: { success: true, jobId: 12345678, logFile: "/tmp/ci-logs/job-12345678.log", sizeBytes: 152340 }
|
|
494
|
-
// Then: Read(logFile) to examine the log
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
**Typical flow:**
|
|
498
|
-
|
|
499
|
-
1. Use `show_session` to see PR CI status
|
|
500
|
-
2. Check `ciStatus[prId].cipeUrl` — if a CIPE exists, use `ci_information` for logs and skip this tool
|
|
501
|
-
3. If NO CIPE exists, check `ciStatus[prId].externalCIRuns` — examine runs and jobs directly from the session data
|
|
502
|
-
4. For a failed job, call `get_ci_logs(sessionId, repoId, jobId)` to save the log to a file
|
|
503
|
-
5. Use `Read(logFile)` to examine the log content — use `offset`/`limit` for large files
|
|
439
|
+
`get_ci_logs` retrieves the full plain-text log for a specific CI job — the drill-in tool for investigating a failed job. **ONLY use it when NO CIPE (CI Pipeline Execution) exists for the PR** (`ciStatus[prId].cipeUrl` is null); when a CIPE exists, use the Nx MCP `ci_information` tool instead, and do NOT fetch or poll the `cipeUrl` over HTTP.
|
|
504
440
|
|
|
505
|
-
|
|
441
|
+
When you need to fetch and read a failed job's log, read [`reference/ci-job-logs.md`](reference/ci-job-logs.md) for the parameters, return shape, and the full flow (identify the job from `externalCIRuns`, call `get_ci_logs`, then `Read` the saved log file).
|
|
506
442
|
|
|
507
443
|
### Fetching Git History for Shallow Clones
|
|
508
444
|
|
|
509
|
-
Session repos are shallow (`--depth 1`) clones
|
|
445
|
+
Session repos are shallow (`--depth 1`) clones. When git fails on missing history (`bad object` from `git revert`, `git log`, `git blame`, etc.), call `git_fetch({ sessionId, repo })` and retry. Read [`reference/shallow-clone-history.md`](reference/shallow-clone-history.md) for the CLI form, the `depth`/`refs` options, and the redundant-call behavior.
|
|
510
446
|
|
|
511
447
|
### Update Session Description
|
|
512
448
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Retrieving CI Job Logs
|
|
2
|
+
|
|
3
|
+
Use `get_ci_logs` to retrieve the full plain-text log for a specific CI job. This is the drill-in tool for investigating CI failures after identifying a failed job from the session's CI status.
|
|
4
|
+
|
|
5
|
+
**ONLY use this tool when NO CIPE (CI Pipeline Execution) exists for the PR.** When a CIPE exists (`ciStatus[prId].cipeUrl` is non-null), logs and failure data are available through the CIPE system (Nx Cloud) via the Nx MCP `ci_information` tool — do NOT call `get_ci_logs`, and do NOT fetch or poll the `cipeUrl` over HTTP (it is a browser link for the user, not an API). This tool is specifically for PRs where only external CI runs exist (e.g., GitHub Actions runs without an Nx Cloud CIPE).
|
|
6
|
+
|
|
7
|
+
**Parameters:**
|
|
8
|
+
|
|
9
|
+
- `sessionId` (required): The Polygraph session ID
|
|
10
|
+
- `repoId` (required): Repository ID (MongoDB ObjectId hex string, from the session repository entry)
|
|
11
|
+
- `jobId` (required): GitHub Actions job ID (from `ciStatus[prId].externalCIRuns[].jobs[].jobId` in the `show_session` response)
|
|
12
|
+
|
|
13
|
+
**Returns:**
|
|
14
|
+
|
|
15
|
+
- On success: `{ success: true, jobId: number, logFile: string, sizeBytes: number }`
|
|
16
|
+
- On failure: `{ success: false, error: string }`
|
|
17
|
+
|
|
18
|
+
The tool saves the log to a local temp file and returns the path in `logFile`. Use the `Read` tool to examine the file contents. For large logs, use `offset` and `limit` parameters to read specific sections.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
get_ci_logs(
|
|
22
|
+
sessionId: "<session-id>",
|
|
23
|
+
repoId: "<repo-id>",
|
|
24
|
+
jobId: 12345678
|
|
25
|
+
)
|
|
26
|
+
// Returns: { success: true, jobId: 12345678, logFile: "/tmp/ci-logs/job-12345678.log", sizeBytes: 152340 }
|
|
27
|
+
// Then: Read(logFile) to examine the log
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Typical flow:**
|
|
31
|
+
|
|
32
|
+
1. Use `show_session` to see PR CI status
|
|
33
|
+
2. Check `ciStatus[prId].cipeUrl` — if a CIPE exists, use `ci_information` for logs and skip this tool
|
|
34
|
+
3. If NO CIPE exists, check `ciStatus[prId].externalCIRuns` — examine runs and jobs directly from the session data
|
|
35
|
+
4. For a failed job, call `get_ci_logs(sessionId, repoId, jobId)` to save the log to a file
|
|
36
|
+
5. Use `Read(logFile)` to examine the log content — use `offset`/`limit` for large files
|
|
37
|
+
|
|
38
|
+
**Important:** Logs can be large (100KB+). Only fetch logs for failed or relevant jobs, and read only the sections you need.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Sandboxing in Polygraph Sessions
|
|
2
|
+
|
|
3
|
+
Polygraph may run an agent session inside an OS-level sandbox, but not every session is sandboxed — whether it is on depends on the user's config (globally, per-agent, or per-repo). When it is on, writes are limited to the repository working tree, the session root (`~/.polygraph/sessions/<session-id>/`), the system temp directory, and a few allowlisted directories; network access is restricted to allowlisted hosts. In practice: binding a listening socket is denied (dev servers fail with `EPERM`), localhost servers are unreachable, and writes outside the allowlist are rejected. The user may not know whether this session is sandboxed.
|
|
4
|
+
|
|
5
|
+
**Recognize sandbox denials — do not retry or work around them.** When a command fails in a sandbox-shaped way (`EPERM` binding a port, a blocked network host, a denied write to an ordinary path), the sandbox blocked it. Do NOT retry variations, escalate through workarounds, or route the command through `!`-prefixed user commands — those run inside the same sandbox. One failure is enough evidence; stop and inform the user.
|
|
6
|
+
|
|
7
|
+
**Warn before attempting known-blocked operations.** Before starting a dev server, anything else that listens on a port, or an operation that needs writes or network access outside the allowlist, tell the user up front that it will not work while sandboxing is on and offer the options below instead of attempting it.
|
|
8
|
+
|
|
9
|
+
**What to tell the user.** Explain that Polygraph runs this session in a sandbox, then present both options. Each takes effect on the next agent launch, so the Polygraph session must be relaunched afterwards:
|
|
10
|
+
|
|
11
|
+
1. **Keep the sandbox on and allow the specific operation** (preferred). The sandbox belongs to the agent harness, so exceptions live in harness settings committed to the repository; array settings merge with Polygraph's generated allowlist rather than replacing it.
|
|
12
|
+
|
|
13
|
+
**If you are the Claude parent** — add writable paths to `.claude/settings.json` (or `.claude/settings.local.json`):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{ "sandbox": { "filesystem": { "allowWrite": ["<path>"] } } }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The `.claude/` directory is read-only inside the sandbox, so give the user the exact snippet to commit — you cannot apply it yourself.
|
|
20
|
+
|
|
21
|
+
**If you are the Codex parent** — add writable roots or network access to `.codex/config.toml`:
|
|
22
|
+
|
|
23
|
+
```toml
|
|
24
|
+
[sandbox_workspace_write]
|
|
25
|
+
network_access = true
|
|
26
|
+
writable_roots = ["<path>"]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The `.codex/` directory is read-only inside the sandbox, so give the user the exact snippet to commit — you cannot apply it yourself.
|
|
30
|
+
|
|
31
|
+
2. **Turn sandboxing off.** The user quits the agent, runs `polygraph config`, toggles **Agent Options → Claude** (or **→ Codex**) **→ sandbox** off (or per-repo under **Repo Options**), and relaunches the Polygraph session. Non-interactive alternative: set `agentOptions.claude.sandbox: false` (or `agentOptions.codex.sandbox: false`, or `repoOptions."org/repo".sandbox: false`) in `~/.polygraph/config.json`. Warn that this removes filesystem isolation — the agent can then write anywhere the OS user can.
|
|
32
|
+
|
|
33
|
+
**Never blame the tooling.** A sandbox denial means the environment blocked the operation — not that the repo, tool, or framework is broken. Do not record conclusions like "X is unusable" in memory, session descriptions, or messages based on sandboxed failures.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Fetching Git History for Shallow Clones
|
|
2
|
+
|
|
3
|
+
Session repos are shallow (`--depth 1`) clones, and plain `git fetch --unshallow` fails on private repos (the clone-time credential is not retained).
|
|
4
|
+
|
|
5
|
+
When git fails on missing history — `bad object` from `git revert`, `git log`, `git blame`, etc. — call `git_fetch({ sessionId, repo })` (CLI: `polygraph git fetch <repo> --session <id> --json`), then retry the git command.
|
|
6
|
+
|
|
7
|
+
- Defaults fetch the default branch's full history.
|
|
8
|
+
- Pass `depth` for a bounded fetch, or `refs` to include extra branches.
|
|
9
|
+
- Safe to call redundantly — returns `alreadyComplete: true` when the history is already present.
|
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
// SessionStart hook — checks whether the installed Polygraph plugin is
|
|
2
|
-
// outdated and, when it is, emits a single stdout message so the agent
|
|
3
|
-
// surfaces the problem to the user. Stale plugin versions have silently
|
|
4
|
-
// caused incorrect Polygraph behavior in the past; this makes it visible
|
|
5
|
-
// for agent launches that bypass the polygraph CLI (e.g. desktop apps).
|
|
6
|
-
//
|
|
7
|
-
// Unlike the sibling hooks, this one deliberately writes to stdout — but
|
|
8
|
-
// ONLY when the plugin is outdated. When current, unknown, offline, or on
|
|
9
|
-
// any error it prints nothing and exits 0.
|
|
10
|
-
//
|
|
11
|
-
// The harness ('claude' | 'codex') is passed as the first CLI argument so
|
|
12
|
-
// the same script ships in both plugin artifacts.
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
appendFileSync,
|
|
16
|
-
mkdirSync,
|
|
17
|
-
readFileSync,
|
|
18
|
-
realpathSync,
|
|
19
|
-
renameSync,
|
|
20
|
-
statSync,
|
|
21
|
-
writeFileSync,
|
|
22
|
-
} from 'node:fs';
|
|
23
|
-
import { homedir } from 'node:os';
|
|
24
|
-
import { dirname, join } from 'node:path';
|
|
25
|
-
import { fileURLToPath } from 'node:url';
|
|
26
|
-
|
|
27
|
-
const HOOK_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
28
|
-
const CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
29
|
-
const FETCH_TIMEOUT_MS = 3000;
|
|
30
|
-
const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
|
31
|
-
|
|
32
|
-
const PACKAGE_BY_HARNESS = {
|
|
33
|
-
claude: '@polygraph/claude-plugin',
|
|
34
|
-
codex: '@polygraph/codex-plugin',
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const REMEDIATION_BY_HARNESS = {
|
|
38
|
-
claude: 'run `claude plugins update polygraph@polygraph-plugins`',
|
|
39
|
-
codex:
|
|
40
|
-
'run `npx --prefer-online @polygraph/codex-plugin@latest install` then `codex plugin add polygraph@polygraph-plugins`',
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Append a one-line JSON record of a hook failure to ~/.polygraph/logs/hooks.log.
|
|
44
|
-
// This hook swallows its errors silently, so this on-disk log is the only
|
|
45
|
-
// record that something went wrong. The logger is itself failure-proof.
|
|
46
|
-
function logHookFailure(
|
|
47
|
-
hook,
|
|
48
|
-
error,
|
|
49
|
-
meta = {},
|
|
50
|
-
home = process.env.HOME?.trim() || homedir()
|
|
51
|
-
) {
|
|
52
|
-
try {
|
|
53
|
-
const logsDir = join(home, '.polygraph', 'logs');
|
|
54
|
-
mkdirSync(logsDir, { recursive: true });
|
|
55
|
-
const logFile = join(logsDir, 'hooks.log');
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
if (statSync(logFile).size > HOOK_LOG_MAX_BYTES) {
|
|
59
|
-
renameSync(logFile, `${logFile}.1`);
|
|
60
|
-
}
|
|
61
|
-
} catch {
|
|
62
|
-
// no prior log, or rotation failed — ignore
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const entry = {
|
|
66
|
-
time: new Date().toISOString(),
|
|
67
|
-
hook,
|
|
68
|
-
pid: process.pid,
|
|
69
|
-
...meta,
|
|
70
|
-
error: error instanceof Error ? error.message : String(error),
|
|
71
|
-
...(error instanceof Error && error.stack ? { stack: error.stack } : {}),
|
|
72
|
-
};
|
|
73
|
-
appendFileSync(logFile, JSON.stringify(entry) + '\n');
|
|
74
|
-
} catch {
|
|
75
|
-
// Logging must never throw — a failing logger must not break the hook.
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function tryParseJson(str) {
|
|
80
|
-
try {
|
|
81
|
-
return JSON.parse(str);
|
|
82
|
-
} catch {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function parseSemver(version) {
|
|
88
|
-
if (typeof version !== 'string') return null;
|
|
89
|
-
const match = version
|
|
90
|
-
.trim()
|
|
91
|
-
.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
92
|
-
if (!match) return null;
|
|
93
|
-
return {
|
|
94
|
-
major: Number(match[1]),
|
|
95
|
-
minor: Number(match[2]),
|
|
96
|
-
patch: Number(match[3]),
|
|
97
|
-
prerelease: match[4] ? match[4].split('.') : [],
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Returns -1, 0, or 1 when a is lower than, equal to, or higher than b.
|
|
102
|
-
// Returns null when either version is unparseable.
|
|
103
|
-
export function compareSemver(a, b) {
|
|
104
|
-
const pa = parseSemver(a);
|
|
105
|
-
const pb = parseSemver(b);
|
|
106
|
-
if (!pa || !pb) return null;
|
|
107
|
-
|
|
108
|
-
for (const key of ['major', 'minor', 'patch']) {
|
|
109
|
-
if (pa[key] !== pb[key]) return pa[key] < pb[key] ? -1 : 1;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Same core version: a prerelease sorts below a release.
|
|
113
|
-
if (pa.prerelease.length && !pb.prerelease.length) return -1;
|
|
114
|
-
if (!pa.prerelease.length && pb.prerelease.length) return 1;
|
|
115
|
-
|
|
116
|
-
const len = Math.max(pa.prerelease.length, pb.prerelease.length);
|
|
117
|
-
for (let i = 0; i < len; i++) {
|
|
118
|
-
const ia = pa.prerelease[i];
|
|
119
|
-
const ib = pb.prerelease[i];
|
|
120
|
-
if (ia === undefined) return -1;
|
|
121
|
-
if (ib === undefined) return 1;
|
|
122
|
-
if (ia === ib) continue;
|
|
123
|
-
const na = /^\d+$/.test(ia) ? Number(ia) : null;
|
|
124
|
-
const nb = /^\d+$/.test(ib) ? Number(ib) : null;
|
|
125
|
-
if (na !== null && nb !== null) return na < nb ? -1 : 1;
|
|
126
|
-
if (na !== null) return -1; // numeric identifiers sort below alphanumeric
|
|
127
|
-
if (nb !== null) return 1;
|
|
128
|
-
return ia < ib ? -1 : 1;
|
|
129
|
-
}
|
|
130
|
-
return 0;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Resolve the installed plugin version from the manifest shipped alongside
|
|
134
|
-
// this script: <pluginRoot>/hooks/check-plugin-version.mjs sits next to
|
|
135
|
-
// .claude-plugin/plugin.json (Claude), .codex-plugin/plugin.json (Codex),
|
|
136
|
-
// or package.json.
|
|
137
|
-
export function resolveInstalledVersion(pluginRoot) {
|
|
138
|
-
const manifests = [
|
|
139
|
-
join(pluginRoot, '.claude-plugin', 'plugin.json'),
|
|
140
|
-
join(pluginRoot, '.codex-plugin', 'plugin.json'),
|
|
141
|
-
join(pluginRoot, 'package.json'),
|
|
142
|
-
];
|
|
143
|
-
for (const manifestPath of manifests) {
|
|
144
|
-
let raw;
|
|
145
|
-
try {
|
|
146
|
-
raw = readFileSync(manifestPath, 'utf8');
|
|
147
|
-
} catch {
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
const parsed = tryParseJson(raw);
|
|
151
|
-
if (parsed && parseSemver(parsed.version)) return parsed.version.trim();
|
|
152
|
-
}
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function cachePath(harness, home) {
|
|
157
|
-
return join(home, '.polygraph', 'logs', `plugin-version-check-${harness}.json`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function readCache(harness, home) {
|
|
161
|
-
try {
|
|
162
|
-
return tryParseJson(readFileSync(cachePath(harness, home), 'utf8'));
|
|
163
|
-
} catch {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// A cache entry is only trusted when it is recent, was recorded for the
|
|
169
|
-
// currently installed version (updating the plugin invalidates it), and holds
|
|
170
|
-
// either a parseable latest version or null (a negatively-cached failed
|
|
171
|
-
// fetch, so an offline machine does not re-stall on every session start).
|
|
172
|
-
export function isCacheFresh(cache, installed, now) {
|
|
173
|
-
return Boolean(
|
|
174
|
-
cache &&
|
|
175
|
-
Number.isFinite(cache.checkedAt) &&
|
|
176
|
-
now - cache.checkedAt >= 0 &&
|
|
177
|
-
now - cache.checkedAt < CACHE_MAX_AGE_MS &&
|
|
178
|
-
cache.installed === installed &&
|
|
179
|
-
(cache.latest === null || parseSemver(cache.latest))
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function writeCache(harness, home, entry) {
|
|
184
|
-
const path = cachePath(harness, home);
|
|
185
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
186
|
-
const tmpPath = `${path}.tmp-${process.pid}`;
|
|
187
|
-
writeFileSync(tmpPath, JSON.stringify(entry) + '\n');
|
|
188
|
-
renameSync(tmpPath, path);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function fetchLatestVersion(packageName, fetchImpl) {
|
|
192
|
-
const registry = (process.env.npm_config_registry?.trim() || DEFAULT_REGISTRY)
|
|
193
|
-
.replace(/\/+$/, '');
|
|
194
|
-
const url = `${registry}/-/package/${packageName.replace('/', '%2f')}/dist-tags`;
|
|
195
|
-
const response = await fetchImpl(url, {
|
|
196
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
197
|
-
});
|
|
198
|
-
if (!response.ok) throw new Error(`registry responded ${response.status}`);
|
|
199
|
-
const distTags = await response.json();
|
|
200
|
-
return typeof distTags?.latest === 'string' ? distTags.latest : null;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export function buildOutdatedMessage(harness, installed, latest) {
|
|
204
|
-
return (
|
|
205
|
-
`The Polygraph plugin is outdated: ${installed} installed, ${latest} latest. ` +
|
|
206
|
-
'Stale plugin versions cause incorrect Polygraph behavior. ' +
|
|
207
|
-
`Tell the user to update it now: ${REMEDIATION_BY_HARNESS[harness]} ` +
|
|
208
|
-
'(or re-run `polygraph config`), then restart the agent session.'
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Check whether the installed plugin is outdated.
|
|
214
|
-
*
|
|
215
|
-
* @param {object} opts
|
|
216
|
-
* @param {string} opts.harness 'claude' | 'codex'
|
|
217
|
-
* @param {string} opts.pluginRoot Directory containing the plugin manifest.
|
|
218
|
-
* @param {string} [opts.home] Override HOME for testing.
|
|
219
|
-
* @param {Function} [opts.fetchImpl] Override fetch for testing.
|
|
220
|
-
* @param {number} [opts.now] Override the clock for testing.
|
|
221
|
-
* @returns {Promise<string|null>} The message to emit, or null to stay silent.
|
|
222
|
-
*/
|
|
223
|
-
export async function checkPluginVersion({
|
|
224
|
-
harness,
|
|
225
|
-
pluginRoot,
|
|
226
|
-
home = process.env.HOME?.trim() || homedir(),
|
|
227
|
-
fetchImpl = fetch,
|
|
228
|
-
now = Date.now(),
|
|
229
|
-
}) {
|
|
230
|
-
const packageName = PACKAGE_BY_HARNESS[harness];
|
|
231
|
-
if (!packageName) return null;
|
|
232
|
-
|
|
233
|
-
const installed = resolveInstalledVersion(pluginRoot);
|
|
234
|
-
if (!installed) return null;
|
|
235
|
-
|
|
236
|
-
let latest;
|
|
237
|
-
const cache = readCache(harness, home);
|
|
238
|
-
if (isCacheFresh(cache, installed, now)) {
|
|
239
|
-
if (cache.latest === null) return null;
|
|
240
|
-
latest = cache.latest;
|
|
241
|
-
} else {
|
|
242
|
-
try {
|
|
243
|
-
latest = await fetchLatestVersion(packageName, fetchImpl);
|
|
244
|
-
} catch (error) {
|
|
245
|
-
// Negative cache: remember the failed fetch so an offline machine
|
|
246
|
-
// does not re-stall for the fetch timeout on every session start.
|
|
247
|
-
writeCache(harness, home, { checkedAt: now, installed, latest: null });
|
|
248
|
-
throw error;
|
|
249
|
-
}
|
|
250
|
-
if (!parseSemver(latest)) {
|
|
251
|
-
writeCache(harness, home, { checkedAt: now, installed, latest: null });
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
|
-
writeCache(harness, home, { checkedAt: now, installed, latest });
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (compareSemver(installed, latest) === -1) {
|
|
258
|
-
return buildOutdatedMessage(harness, installed, latest);
|
|
259
|
-
}
|
|
260
|
-
return null;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export async function main() {
|
|
264
|
-
const harness = process.argv[2];
|
|
265
|
-
try {
|
|
266
|
-
const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
267
|
-
const message = await checkPluginVersion({ harness, pluginRoot });
|
|
268
|
-
if (message) process.stdout.write(message + '\n');
|
|
269
|
-
} catch (error) {
|
|
270
|
-
// Offline or broken registry must never block or pollute the session,
|
|
271
|
-
// but record it so failures are not invisible.
|
|
272
|
-
logHookFailure(`${harness || 'unknown'}:check-plugin-version`, error);
|
|
273
|
-
}
|
|
274
|
-
process.exitCode = 0;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Run only when executed directly as a hook, not when imported (e.g. by tests).
|
|
278
|
-
// realpathSync both sides so the check holds when the plugin lives under a
|
|
279
|
-
// symlinked path (e.g. macOS /tmp -> /private/tmp).
|
|
280
|
-
function isMainModule() {
|
|
281
|
-
if (!process.argv[1]) return false;
|
|
282
|
-
try {
|
|
283
|
-
return (
|
|
284
|
-
realpathSync(process.argv[1]) ===
|
|
285
|
-
realpathSync(fileURLToPath(import.meta.url))
|
|
286
|
-
);
|
|
287
|
-
} catch {
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (isMainModule()) {
|
|
293
|
-
main();
|
|
294
|
-
}
|