@preapexis/piexis 0.2.2 → 0.2.5
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/README.md +85 -131
- package/apps/server/src/index.ts +97 -0
- package/apps/server/src/session-manager.ts +95 -0
- package/apps/server/src/workspace.ts +39 -0
- package/{tsconfig.json → apps/server/tsconfig.json} +3 -3
- package/apps/web/dist/assets/index-D4n3_lAw.js +10 -0
- package/apps/web/dist/assets/index-igiK0FPY.css +1 -0
- package/apps/web/dist/index.html +3 -0
- package/bin/piexis-web.mjs +24 -0
- package/extensions/agents.ts +205 -0
- package/extensions/ask-question.ts +54 -95
- package/extensions/recommended-tools.ts +62 -0
- package/extensions/ui.ts +204 -58
- package/extensions/webui.ts +45 -0
- package/lib/agent-config.ts +55 -0
- package/lib/agents.ts +208 -0
- package/lib/question-model.ts +98 -0
- package/lib/question-ui.ts +214 -0
- package/lib/recommended-tools.ts +119 -0
- package/lib/subagent-runner.ts +146 -0
- package/lib/ui-tips.json +41 -0
- package/package.json +41 -14
- package/packages/pi-integration/src/rpc-client.ts +99 -0
- package/packages/shared/src/index.ts +27 -0
- package/prompts/init.md +10 -2
- package/prompts/summary.md +7 -6
- package/schemas/agent.schema.json +48 -0
- package/skills/{claude-api/LICENSE.txt → caveman/LICENSE} +2 -2
- package/skills/caveman/LICENSE-MIT +21 -0
- package/skills/caveman/NOTICE +10 -0
- package/skills/caveman/SKILL.md +89 -0
- package/skills/find-skills/SKILL.md +141 -0
- package/skills/ponytail/LICENSE +21 -0
- package/skills/ponytail/NOTICE +9 -0
- package/skills/ponytail/SKILL.md +52 -0
- package/themes/midnight.json +81 -0
- package/.github/workflows/publish.yml +0 -35
- package/AGENTS.md +0 -58
- package/eval/README.md +0 -72
- package/eval/permission-cases.json +0 -28
- package/eval/permissions.mjs +0 -103
- package/extensions/background-tasks.ts +0 -433
- package/extensions/background-worker.ts +0 -50
- package/extensions/glob.ts +0 -32
- package/extensions/mode.ts +0 -397
- package/lib/bash-approvals.ts +0 -113
- package/lib/bash-policy.ts +0 -164
- package/lib/mode-policy.ts +0 -174
- package/lib/path-policy.ts +0 -182
- package/lib/permission-classifier.ts +0 -128
- package/lib/permission-config.ts +0 -91
- package/lib/permission-denials.ts +0 -32
- package/lib/permission-intent.ts +0 -128
- package/lib/permission-taxonomy.ts +0 -15
- package/lib/task-broker.ts +0 -65
- package/lib/task-checkout.ts +0 -39
- package/lib/task-git.ts +0 -44
- package/lib/task-patch.ts +0 -106
- package/lib/task-permissions.ts +0 -51
- package/lib/task-report.ts +0 -62
- package/lib/task-worktree-policy.ts +0 -56
- package/scripts/release.mjs +0 -32
- package/skills/claude-api/SKILL.md +0 -572
- package/skills/claude-api/csharp/claude-api/README.md +0 -362
- package/skills/claude-api/csharp/claude-api/batches.md +0 -14
- package/skills/claude-api/csharp/claude-api/files-api.md +0 -25
- package/skills/claude-api/csharp/claude-api/streaming.md +0 -28
- package/skills/claude-api/csharp/claude-api/tool-use.md +0 -164
- package/skills/claude-api/curl/examples.md +0 -256
- package/skills/claude-api/curl/managed-agents.md +0 -371
- package/skills/claude-api/go/claude-api/README.md +0 -188
- package/skills/claude-api/go/claude-api/files-api.md +0 -23
- package/skills/claude-api/go/claude-api/streaming.md +0 -43
- package/skills/claude-api/go/claude-api/tool-use.md +0 -220
- package/skills/claude-api/go/managed-agents/README.md +0 -564
- package/skills/claude-api/java/claude-api/README.md +0 -238
- package/skills/claude-api/java/claude-api/files-api.md +0 -27
- package/skills/claude-api/java/claude-api/streaming.md +0 -24
- package/skills/claude-api/java/claude-api/tool-use.md +0 -228
- package/skills/claude-api/java/managed-agents/README.md +0 -443
- package/skills/claude-api/php/claude-api/README.md +0 -174
- package/skills/claude-api/php/claude-api/batches.md +0 -15
- package/skills/claude-api/php/claude-api/files-api.md +0 -13
- package/skills/claude-api/php/claude-api/streaming.md +0 -27
- package/skills/claude-api/php/claude-api/tool-use.md +0 -253
- package/skills/claude-api/php/managed-agents/README.md +0 -443
- package/skills/claude-api/python/claude-api/README.md +0 -565
- package/skills/claude-api/python/claude-api/batches.md +0 -198
- package/skills/claude-api/python/claude-api/files-api.md +0 -170
- package/skills/claude-api/python/claude-api/sdk-upgrade.md +0 -286
- package/skills/claude-api/python/claude-api/streaming.md +0 -179
- package/skills/claude-api/python/claude-api/tool-use.md +0 -627
- package/skills/claude-api/python/managed-agents/README.md +0 -335
- package/skills/claude-api/ruby/claude-api/README.md +0 -134
- package/skills/claude-api/ruby/claude-api/streaming.md +0 -16
- package/skills/claude-api/ruby/claude-api/tool-use.md +0 -41
- package/skills/claude-api/ruby/managed-agents/README.md +0 -394
- package/skills/claude-api/shared/admin-api.md +0 -179
- package/skills/claude-api/shared/agent-design.md +0 -101
- package/skills/claude-api/shared/anthropic-cli.md +0 -269
- package/skills/claude-api/shared/claude-platform-on-aws.md +0 -59
- package/skills/claude-api/shared/cost-optimization.md +0 -233
- package/skills/claude-api/shared/error-codes.md +0 -262
- package/skills/claude-api/shared/live-sources.md +0 -166
- package/skills/claude-api/shared/managed-agents-api-reference.md +0 -455
- package/skills/claude-api/shared/managed-agents-client-patterns.md +0 -220
- package/skills/claude-api/shared/managed-agents-core.md +0 -359
- package/skills/claude-api/shared/managed-agents-environments.md +0 -226
- package/skills/claude-api/shared/managed-agents-events.md +0 -290
- package/skills/claude-api/shared/managed-agents-memory.md +0 -201
- package/skills/claude-api/shared/managed-agents-multiagent.md +0 -260
- package/skills/claude-api/shared/managed-agents-onboarding.md +0 -82
- package/skills/claude-api/shared/managed-agents-outcomes.md +0 -108
- package/skills/claude-api/shared/managed-agents-overview.md +0 -77
- package/skills/claude-api/shared/managed-agents-scheduled-deployments.md +0 -156
- package/skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +0 -282
- package/skills/claude-api/shared/managed-agents-tools.md +0 -504
- package/skills/claude-api/shared/managed-agents-webhooks.md +0 -143
- package/skills/claude-api/shared/model-migration.md +0 -1872
- package/skills/claude-api/shared/models.md +0 -142
- package/skills/claude-api/shared/platform-availability.md +0 -53
- package/skills/claude-api/shared/prompt-audit.md +0 -223
- package/skills/claude-api/shared/prompt-caching.md +0 -292
- package/skills/claude-api/shared/token-counting.md +0 -56
- package/skills/claude-api/shared/tool-use-concepts.md +0 -526
- package/skills/claude-api/typescript/claude-api/README.md +0 -405
- package/skills/claude-api/typescript/claude-api/batches.md +0 -106
- package/skills/claude-api/typescript/claude-api/files-api.md +0 -98
- package/skills/claude-api/typescript/claude-api/streaming.md +0 -178
- package/skills/claude-api/typescript/claude-api/tool-use.md +0 -586
- package/skills/claude-api/typescript/managed-agents/README.md +0 -359
- package/skills-lock.json +0 -359
- package/tests/auto-permissions.test.mjs +0 -315
- package/tests/background-tasks.test.mjs +0 -27
- package/tests/background-worker.test.mjs +0 -60
- package/tests/bash-permissions.test.mjs +0 -276
- package/tests/glob.test.mjs +0 -31
- package/tests/mode.test.mjs +0 -62
- package/tests/permission-evaluation.test.mjs +0 -70
- package/tests/permission-foundation.test.mjs +0 -194
- package/tests/permission-policy.test.mjs +0 -158
- package/tests/runtime/README.md +0 -51
- package/tests/runtime/harness.mjs +0 -190
- package/tests/runtime/permissions.test.mjs +0 -192
- package/tests/runtime/tui.test.mjs +0 -60
- package/tests/skills-routing.test.mjs +0 -51
- package/tests/task-integration.test.mjs +0 -324
- package/tests/task-permissions.test.mjs +0 -178
- package/tests/task-worktree-policy.test.mjs +0 -53
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdir, mkdtemp, readdir, symlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
|
|
7
|
-
import modeExtension from "../extensions/mode.ts";
|
|
8
|
-
import { classifierAction } from "../lib/permission-classifier.ts";
|
|
9
|
-
import { evaluateAutoPolicy } from "../lib/mode-policy.ts";
|
|
10
|
-
import { evaluatePathPolicy, resolveWorkspaceRoot } from "../lib/path-policy.ts";
|
|
11
|
-
import { readProjectPermissionConfig } from "../lib/permission-config.ts";
|
|
12
|
-
|
|
13
|
-
async function fixture() {
|
|
14
|
-
const root = await mkdtemp(join(tmpdir(), "piexis-policy-"));
|
|
15
|
-
const project = join(root, "project");
|
|
16
|
-
const outside = join(root, "outside");
|
|
17
|
-
await Promise.all([mkdir(join(project, "nested"), { recursive: true }), mkdir(outside)]);
|
|
18
|
-
await writeFile(join(project, ".git"), "fixture");
|
|
19
|
-
await writeFile(join(project, "file with spaces.txt"), "ok");
|
|
20
|
-
await writeFile(join(project, "üñïçødé.txt"), "ok");
|
|
21
|
-
await writeFile(join(outside, "secret"), "no");
|
|
22
|
-
await symlink(outside, join(project, "outside-link"));
|
|
23
|
-
await symlink(join(outside, "missing"), join(project, "broken-link"));
|
|
24
|
-
return { project, outside, nested: join(project, "nested") };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
test("path policy canonicalizes nested project cwd and rejects lexical boundary escapes", async () => {
|
|
28
|
-
const { project, outside, nested } = await fixture();
|
|
29
|
-
const root = await resolveWorkspaceRoot(nested);
|
|
30
|
-
assert.equal(root, project);
|
|
31
|
-
for (const path of ["file with spaces.txt", "üñïçødé.txt", "nested/../file with spaces.txt", "./file with spaces.txt"]) {
|
|
32
|
-
assert.equal((await evaluatePathPolicy("read", { path }, root))?.decision, "allow", path);
|
|
33
|
-
}
|
|
34
|
-
for (const path of ["../outside/secret", "../../", outside, "outside-link/secret"]) {
|
|
35
|
-
const result = await evaluatePathPolicy("read", { path }, root);
|
|
36
|
-
assert.equal(result?.decision, "review", path);
|
|
37
|
-
assert.equal(result?.reason, "outside-project");
|
|
38
|
-
}
|
|
39
|
-
assert.deepEqual(await evaluatePathPolicy("read", { path: "broken-link" }, root), {
|
|
40
|
-
decision: "deny", reason: "unresolvable-path"
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("sensitive paths require review even when lexically project-local", async () => {
|
|
45
|
-
const { project } = await fixture();
|
|
46
|
-
await writeFile(join(project, ".env"), "TOKEN=not-for-policy");
|
|
47
|
-
const result = await evaluatePathPolicy("read", { path: ".env" }, project);
|
|
48
|
-
assert.equal(result?.decision, "review");
|
|
49
|
-
assert.equal(result?.reason, "sensitive-path");
|
|
50
|
-
assert.equal((await evaluatePathPolicy("read", { path: "~/.ssh/id_ed25519" }, project))?.decision, "review");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("Auto deterministic policy allows ordinary local reads/edits and denies malformed or unsupported requests", async () => {
|
|
54
|
-
const { project } = await fixture();
|
|
55
|
-
assert.equal((await evaluateAutoPolicy("read", { path: "README.md" }, project)).decision, "allow");
|
|
56
|
-
assert.equal((await evaluateAutoPolicy("write", { path: "new.ts", content: "fixture" }, project)).decision, "allow");
|
|
57
|
-
assert.equal((await evaluateAutoPolicy("edit", { path: "new.ts", edits: [{ oldText: "a", newText: "b" }] }, project)).decision, "allow");
|
|
58
|
-
assert.equal((await evaluateAutoPolicy("write", { path: "../outside/new.ts", content: "fixture" }, project)).decision, "review");
|
|
59
|
-
assert.deepEqual(await evaluateAutoPolicy("read", { path: "../outside" }, project), {
|
|
60
|
-
decision: "review", source: "policy", reason: "outside-project",
|
|
61
|
-
pathPolicy: await evaluatePathPolicy("read", { path: "../outside" }, project)
|
|
62
|
-
});
|
|
63
|
-
assert.equal((await evaluateAutoPolicy("read", { path: ".env" }, project)).decision, "deny");
|
|
64
|
-
assert.equal((await evaluateAutoPolicy("bash", { command: "pwd" }, project)).decision, "allow");
|
|
65
|
-
assert.equal((await evaluateAutoPolicy("bash", { command: "git diff --stat" }, project)).decision, "review");
|
|
66
|
-
assert.equal((await evaluateAutoPolicy("bash", null, project)).decision, "deny");
|
|
67
|
-
assert.equal((await evaluateAutoPolicy("unknown", {}, project)).decision, "deny");
|
|
68
|
-
assert.equal((await evaluateAutoPolicy("read", { path: 12 }, project)).reason, "malformed-path");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("Auto does not send deterministic sensitive-path denials to the classifier", async () => {
|
|
72
|
-
const cwd = await mkdtemp(join(tmpdir(), "piexis-auto-"));
|
|
73
|
-
await mkdir(join(cwd, ".pi"));
|
|
74
|
-
await writeFile(join(cwd, ".pi", "permission-modes.json"), JSON.stringify({
|
|
75
|
-
version: 1, initialized: true,
|
|
76
|
-
classifier: { provider: "test", model: "classifier", timeoutMs: 1000 }
|
|
77
|
-
}));
|
|
78
|
-
const handlers = new Map();
|
|
79
|
-
let classifierCalls = 0;
|
|
80
|
-
modeExtension({
|
|
81
|
-
registerFlag() {}, registerTool() {}, registerCommand() {}, registerShortcut() {},
|
|
82
|
-
on(name, handler) { handlers.set(name, handler); }, appendEntry() {},
|
|
83
|
-
getFlag(name) { return name === "permission-mode" ? "auto" : false; }, events: { emit() {} }
|
|
84
|
-
});
|
|
85
|
-
const ctx = {
|
|
86
|
-
cwd, hasUI: false, mode: "json", model: undefined,
|
|
87
|
-
modelRegistry: {
|
|
88
|
-
find() { return { provider: "test", id: "classifier" }; },
|
|
89
|
-
hasConfiguredAuth() { return true; },
|
|
90
|
-
async complete() { classifierCalls++; throw new Error("must not run"); }
|
|
91
|
-
},
|
|
92
|
-
sessionManager: { getBranch() { return []; } }
|
|
93
|
-
};
|
|
94
|
-
await handlers.get("session_start")({ reason: "startup" }, ctx);
|
|
95
|
-
const outcome = await handlers.get("tool_call")({ toolName: "read", input: { path: ".env" } }, ctx);
|
|
96
|
-
assert.match(outcome.reason, /sensitive-path/);
|
|
97
|
-
assert.equal(classifierCalls, 0);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("classifier payload excludes file contents and conversation data", () => {
|
|
101
|
-
assert.deepEqual(classifierAction("write", { path: ".env", content: "API_KEY=secret", environment: { TOKEN: "secret" } }), {
|
|
102
|
-
tool: "write", input: { path: ".env" }
|
|
103
|
-
});
|
|
104
|
-
assert.deepEqual(classifierAction("edit", { path: "src/a.ts", oldText: "secret", newText: "other secret" }), {
|
|
105
|
-
tool: "edit", input: { path: "src/a.ts" }
|
|
106
|
-
});
|
|
107
|
-
assert.deepEqual(classifierAction("bash", { command: "git status", env: { TOKEN: "secret" } }), {
|
|
108
|
-
tool: "bash", input: { command: "git status" }
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("missing project configuration uses defaults without creating .pi", async () => {
|
|
113
|
-
const cwd = await mkdtemp(join(tmpdir(), "piexis-config-"));
|
|
114
|
-
assert.deepEqual(await readProjectPermissionConfig(cwd), { version: 1, initialized: true });
|
|
115
|
-
await assert.rejects(readdir(join(cwd, ".pi")));
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("session approval labels make whole-tool Bash access explicit", async () => {
|
|
119
|
-
const cwd = await mkdtemp(join(tmpdir(), "piexis-mode-"));
|
|
120
|
-
const handlers = new Map();
|
|
121
|
-
modeExtension({
|
|
122
|
-
registerFlag() {}, registerTool() {}, registerCommand() {}, registerShortcut() {},
|
|
123
|
-
on(name, handler) { handlers.set(name, handler); }, appendEntry() {}, getFlag() {}, events: { emit() {} }
|
|
124
|
-
});
|
|
125
|
-
let options = [];
|
|
126
|
-
const outcome = await handlers.get("tool_call")({ toolName: "bash", input: { command: "git status" } }, {
|
|
127
|
-
cwd, hasUI: true,
|
|
128
|
-
ui: { select: async (_title, nextOptions) => { options = nextOptions; return "Deny"; } }
|
|
129
|
-
});
|
|
130
|
-
assert.deepEqual(options, [
|
|
131
|
-
"Allow once: bash",
|
|
132
|
-
"Allow ALL bash actions for this session",
|
|
133
|
-
"Deny"
|
|
134
|
-
]);
|
|
135
|
-
assert.equal(outcome.block, true);
|
|
136
|
-
assert.match(outcome.reason, /Blocked by user/);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("Bash metacharacters, command substitution, and assignments do not receive reusable grants", async () => {
|
|
140
|
-
const cwd = await mkdtemp(join(tmpdir(), "piexis-mode-"));
|
|
141
|
-
const handlers = new Map();
|
|
142
|
-
modeExtension({
|
|
143
|
-
registerFlag() {}, registerTool() {}, registerCommand() {}, registerShortcut() {},
|
|
144
|
-
on(name, handler) { handlers.set(name, handler); }, appendEntry() {}, getFlag() {}, events: { emit() {} }
|
|
145
|
-
});
|
|
146
|
-
for (const command of [
|
|
147
|
-
"git status && rm -rf something", "git status; another-command", "sh -c \"echo x\"",
|
|
148
|
-
"bash -c \"echo x\"", "$(whoami)", "`whoami`", "VAR=value command", "command > file", "command | other-command"
|
|
149
|
-
]) {
|
|
150
|
-
let options = [];
|
|
151
|
-
await handlers.get("tool_call")({ toolName: "bash", input: { command } }, {
|
|
152
|
-
cwd, hasUI: true,
|
|
153
|
-
ui: { select: async (_title, nextOptions) => { options = nextOptions; return "Allow once: bash"; } }
|
|
154
|
-
});
|
|
155
|
-
assert.equal(options.some((option) => option.startsWith("Allow executable")), false, command);
|
|
156
|
-
assert.equal(options.some((option) => option.includes("in this directory")), false, command);
|
|
157
|
-
}
|
|
158
|
-
});
|
package/tests/runtime/README.md
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# Local Pi runtime validation
|
|
2
|
-
|
|
3
|
-
```sh
|
|
4
|
-
npm run test:runtime
|
|
5
|
-
```
|
|
6
|
-
|
|
7
|
-
This is separate from the fast `npm test` suite. By default it launches the **declared packaged Pi CLI entrypoint** from the resolved peer dependency, loads every extension declared in the package manifest, and exercises actual SDK tools and subprocess workers. Git and the installed Pi peer dependencies are required. The PTY test additionally requires Linux's `script` and `stty`; it is explicitly skipped on other operating systems.
|
|
8
|
-
|
|
9
|
-
To check a different installed Node-based Pi CLI, override its path (symlinks are resolved):
|
|
10
|
-
|
|
11
|
-
```sh
|
|
12
|
-
PIEXIS_TEST_PI_CLI="$(command -v pi)" npm run test:runtime
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
This POSIX-shell command tests the global CLI instead of assuming its version matches the project's peer dependency. The override is used only by this test harness.
|
|
16
|
-
|
|
17
|
-
## What runs
|
|
18
|
-
|
|
19
|
-
A deterministic, OpenAI-compatible HTTP fixture listens only on `127.0.0.1`. Both agent and reviewer requests use that endpoint and a dummy key. The fixture emits scripted tool calls and reviewer replies; **there is no real model inference, paid provider access, or measurement of classifier judgment**.
|
|
20
|
-
|
|
21
|
-
Children use a fresh agent directory, temporary home and Git repository, an environment allowlist rather than inherited credential/proxy variables, and `--offline`. User/project context discovery, project trust, skills, prompts, and themes are disabled. Package extensions are explicitly loaded from the manifest; automatic package installation/discovery is not tested. Existing credentials and project settings are not modified.
|
|
22
|
-
|
|
23
|
-
Actual operations include writing fixture files, harmless `printf` commands, creating worktrees, inspecting/applying patches, and deleting validated fixture worktrees. Child shutdown is bounded, and cleanup validates recorded worktree ownership before removing retained task directories. This is test-state isolation, not an OS security boundary.
|
|
24
|
-
|
|
25
|
-
## Covered contracts
|
|
26
|
-
|
|
27
|
-
- Manual RPC denial and one-time approval affect actual filesystem writes; Plan blocks writes.
|
|
28
|
-
- Accept Edits, Don't Ask, and Bypass preserve their launch-time behavior. A busy agent cannot enter Plan; RPC command acceptance alone does not mean the mode changed.
|
|
29
|
-
- Auto reaches the real model registry and HTTP transport; malformed replies and reviewer timeouts fail closed.
|
|
30
|
-
- Real worker processes use the parent broker, perform permitted edits/Bash, and produce actual execution observations. Replacement text is not forwarded in review requests.
|
|
31
|
-
- Without Bash opt-in, workers expose no Bash tool and record no command execution, even when the scripted worker falsely claims that tests passed.
|
|
32
|
-
- Missing worker wrappers do not silently enable unrestricted built-in tools.
|
|
33
|
-
- Actual diff inspection, confirmation, patch application, and cleanup work through RPC.
|
|
34
|
-
- Missing headless approval UI denies execution; repeated denials stop after the configured default threshold and emit the diagnostic/session entry.
|
|
35
|
-
- Session replacement invalidates pending worker approval, stops a real worker, and retains interrupted state. A stale approval response cannot write a file.
|
|
36
|
-
- Closing RPC stdin shuts down an active worker cleanly.
|
|
37
|
-
- A real pseudo-terminal exercises the bundled UI, Alt+M cycling, `/plan`, an approval dialog, and `/permissions`, checking that one-time approval leaves no session grant.
|
|
38
|
-
|
|
39
|
-
The PTY check uses actual terminal input/output but is not a full terminal emulator or a visual/accessibility audit. It answers cursor-position queries and inspects rendered strings plus filesystem effects. Windows/macOS behavior, other terminal families, all AskQuestion interactions, skills/prompts/themes, arbitrary descendant-process cancellation, and live-provider model accuracy remain unvalidated.
|
|
40
|
-
|
|
41
|
-
## Recorded local run
|
|
42
|
-
|
|
43
|
-
On **2026-09-27 UTC**:
|
|
44
|
-
|
|
45
|
-
- Linux; Node **24.17.0**; Git **2.47.3**.
|
|
46
|
-
- Project peer dependency's packaged CLI: **Pi 0.85.1**, **14/14 passed**.
|
|
47
|
-
- Global installed packaged CLI: **Pi 0.87.1**, **14/14 passed**.
|
|
48
|
-
- Both runs included the PTY check; none skipped. These are the same 14 contracts checked against two runtime versions, not 28 independent contracts.
|
|
49
|
-
- No production permission behavior was changed for these checks. Harness assumptions about ESM package resolution, CLI entrypoint/version selection, mode labels, RPC extension-command error delivery, and the initial approval-menu selection were corrected during test development.
|
|
50
|
-
|
|
51
|
-
These results apply to these installed runtimes and controlled fixtures, not all Pi versions or all workloads. Run the suite again after SDK or extension changes. See [the classifier evaluation guide](../../eval/README.md) for the separate, explicitly authorized live-classifier evaluation procedure.
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { createServer } from "node:http";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import { mkdtemp, mkdir, writeFile, readFile, readdir, realpath, rm } from "node:fs/promises";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { taskGit } from "../../lib/task-git.ts";
|
|
8
|
-
import { validateTaskWorktree } from "../../lib/task-worktree-policy.ts";
|
|
9
|
-
|
|
10
|
-
const sdkRoot = new URL("../", import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
11
|
-
const sdkManifest = JSON.parse(await readFile(new URL("package.json", sdkRoot), "utf8"));
|
|
12
|
-
const declaredBin = typeof sdkManifest.bin === "string" ? sdkManifest.bin : sdkManifest.bin.pi;
|
|
13
|
-
export const cli = await realpath(process.env.PIEXIS_TEST_PI_CLI ?? fileURLToPath(new URL(declaredBin, sdkRoot)));
|
|
14
|
-
const project = fileURLToPath(new URL("../../", import.meta.url));
|
|
15
|
-
const text = (content) => typeof content === "string" ? content : (content ?? []).map((part) => part.text ?? "").join("\n");
|
|
16
|
-
export async function waitFor(check, diagnostic = () => "", timeout = 15000) {
|
|
17
|
-
const deadline = Date.now() + timeout;
|
|
18
|
-
while (Date.now() < deadline) {
|
|
19
|
-
const value = await check();
|
|
20
|
-
if (value) return value;
|
|
21
|
-
await new Promise((done) => setTimeout(done, 20));
|
|
22
|
-
}
|
|
23
|
-
throw new Error(`Runtime fixture timed out: ${diagnostic()}`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function fixture(t, mode = "default", extra = []) {
|
|
27
|
-
const root = await mkdtemp(join(tmpdir(), "piexis-runtime-"));
|
|
28
|
-
const cwd = join(root, "repo");
|
|
29
|
-
const agent = join(root, "agent");
|
|
30
|
-
const children = [];
|
|
31
|
-
let server;
|
|
32
|
-
t.after(cleanup);
|
|
33
|
-
await mkdir(cwd);
|
|
34
|
-
await mkdir(agent);
|
|
35
|
-
await mkdir(join(root, "home"));
|
|
36
|
-
await taskGit(["init"], cwd);
|
|
37
|
-
await taskGit(["config", "user.name", "Runtime fixture"], cwd);
|
|
38
|
-
await taskGit(["config", "user.email", "runtime@example.invalid"], cwd);
|
|
39
|
-
await writeFile(join(cwd, "base.txt"), "fixture\n");
|
|
40
|
-
await taskGit(["add", "."], cwd);
|
|
41
|
-
await taskGit(["commit", "-m", "fixture"], cwd);
|
|
42
|
-
await mkdir(join(cwd, ".pi"));
|
|
43
|
-
await writeFile(join(cwd, ".pi", "permission-modes.json"), JSON.stringify({ version: 1, initialized: true, classifier: { provider: "runtime-fixture", model: "reviewer", timeoutMs: 1000 } }));
|
|
44
|
-
const requests = [];
|
|
45
|
-
const errors = [];
|
|
46
|
-
let reviewBehavior = "allow";
|
|
47
|
-
server = createServer(async (req, res) => {
|
|
48
|
-
try {
|
|
49
|
-
if (req.url !== "/v1/chat/completions" || req.headers.authorization !== "Bearer fixture-not-a-real-key") throw new Error("Unexpected local provider request.");
|
|
50
|
-
let body = "";
|
|
51
|
-
for await (const chunk of req) { body += chunk; if (body.length > 2_000_000) throw new Error("Oversized fixture request"); }
|
|
52
|
-
const input = JSON.parse(body);
|
|
53
|
-
requests.push(input);
|
|
54
|
-
if (requests.length > 80) throw new Error("Fixture request budget exceeded.");
|
|
55
|
-
const lastUser = [...input.messages].reverse().find((message) => message.role === "user");
|
|
56
|
-
const prompt = text(lastUser?.content);
|
|
57
|
-
const last = input.messages.at(-1);
|
|
58
|
-
let content = "Fixture complete.";
|
|
59
|
-
let call;
|
|
60
|
-
if (input.model === "reviewer") {
|
|
61
|
-
if (reviewBehavior === "hold") return;
|
|
62
|
-
if (reviewBehavior === "malformed") content = "not JSON";
|
|
63
|
-
else {
|
|
64
|
-
const payload = JSON.parse(prompt);
|
|
65
|
-
content = JSON.stringify({ decision: "allow", category: "ordinary-development", reason: "Deterministic local transport fixture, not model judgment.", authorizedBy: [payload.context.intent.currentRequestId] });
|
|
66
|
-
}
|
|
67
|
-
} else if (prompt.includes("RUNTIME_HOLD")) return;
|
|
68
|
-
else if ((last?.role !== "tool" || prompt.includes("RUNTIME_REPEAT")) && prompt.includes("RUNTIME_WRITE")) call = { name: "write", arguments: { path: prompt.includes("PLAN_MUST_BLOCK") ? "plan-only.txt" : "made.txt", content: "actual tool executed\n" } };
|
|
69
|
-
else if (last?.role !== "tool" && prompt.includes("RUNTIME_BASH")) call = { name: "bash", arguments: { command: "printf 'runtime-bash-check\\n'" } };
|
|
70
|
-
else if (prompt.includes("RUNTIME_WORKER")) {
|
|
71
|
-
const toolResults = input.messages.filter((message) => message.role === "tool").length;
|
|
72
|
-
if (toolResults === 0) call = { name: "worker_write", arguments: { path: "worker.txt", content: "actual worker edit\n" } };
|
|
73
|
-
else if (toolResults === 1) call = { name: "worker_bash", arguments: { command: "printf 'runtime-worker-check\\n'" } };
|
|
74
|
-
else content = "Fixture complete. I ran npm test and all tests passed."; // Deliberately unverified worker prose.
|
|
75
|
-
}
|
|
76
|
-
const toolCalls = call ? [{ id: `fixture-${requests.length}`, type: "function", function: { name: call.name, arguments: JSON.stringify(call.arguments) } }] : undefined;
|
|
77
|
-
const message = { role: "assistant", content: toolCalls ? null : content, ...(toolCalls ? { tool_calls: toolCalls } : {}) };
|
|
78
|
-
if (!input.stream) {
|
|
79
|
-
res.setHeader("Content-Type", "application/json");
|
|
80
|
-
res.end(JSON.stringify({ id: "fixture", object: "chat.completion", model: input.model, choices: [{ index: 0, message, finish_reason: toolCalls ? "tool_calls" : "stop" }], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } }));
|
|
81
|
-
} else {
|
|
82
|
-
res.setHeader("Content-Type", "text/event-stream");
|
|
83
|
-
const delta = toolCalls ? { role: "assistant", tool_calls: toolCalls.map((value, index) => ({ index, ...value })) } : { role: "assistant", content };
|
|
84
|
-
for (const choice of [{ index: 0, delta, finish_reason: null }, { index: 0, delta: {}, finish_reason: toolCalls ? "tool_calls" : "stop" }])
|
|
85
|
-
res.write(`data: ${JSON.stringify({ id: "fixture", object: "chat.completion.chunk", model: input.model, choices: [choice] })}\n\n`);
|
|
86
|
-
res.end("data: [DONE]\n\n");
|
|
87
|
-
}
|
|
88
|
-
} catch (error) { errors.push(error.message); res.writeHead(400); res.end("Local fixture rejected request."); }
|
|
89
|
-
});
|
|
90
|
-
await new Promise((done, reject) => {
|
|
91
|
-
server.once("error", reject);
|
|
92
|
-
server.listen(0, "127.0.0.1", done);
|
|
93
|
-
});
|
|
94
|
-
await writeFile(join(agent, "models.json"), JSON.stringify({ providers: { "runtime-fixture": {
|
|
95
|
-
api: "openai-completions", baseUrl: `http://127.0.0.1:${server.address().port}/v1`, apiKey: "fixture-not-a-real-key",
|
|
96
|
-
models: ["agent", "reviewer"].map((id) => ({ id, reasoning: false, input: ["text"], contextWindow: 128000, maxTokens: 1024 }))
|
|
97
|
-
} } }));
|
|
98
|
-
await writeFile(join(agent, "settings.json"), JSON.stringify({ retry: { enabled: false }, compaction: { enabled: false }, quietStartup: true }));
|
|
99
|
-
const env = { PATH: process.env.PATH, HOME: join(root, "home"), USERPROFILE: join(root, "home"),
|
|
100
|
-
PI_CODING_AGENT_DIR: agent, PI_OFFLINE: "1", TERM: "xterm-256color", LANG: "C.UTF-8",
|
|
101
|
-
...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}) };
|
|
102
|
-
const manifest = JSON.parse(await readFile(join(project, "package.json"), "utf8"));
|
|
103
|
-
const extensions = manifest.pi.extensions.flatMap((path) => ["-e", resolve(project, path)]);
|
|
104
|
-
const args = [cli, "--offline", "--no-session", "--no-approve", "--no-context-files", "--no-extensions", "--no-skills", "--no-prompt-templates", "--no-themes",
|
|
105
|
-
"--model", "runtime-fixture/agent", "--permission-mode", mode,
|
|
106
|
-
...extensions, ...extra];
|
|
107
|
-
async function metadata() {
|
|
108
|
-
const directory = join(agent, "piexis", "tasks");
|
|
109
|
-
const files = await readdir(directory).catch(() => []);
|
|
110
|
-
if (!files.some((name) => name.endsWith(".json"))) return [];
|
|
111
|
-
const saved = JSON.parse(await readFile(join(directory, files.find((name) => name.endsWith(".json"))), "utf8"));
|
|
112
|
-
return saved.tasks;
|
|
113
|
-
}
|
|
114
|
-
function start(command = process.execPath, childArgs = [...args, "--mode", "rpc"]) {
|
|
115
|
-
const child = spawn(command, childArgs, { cwd, env, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
|
|
116
|
-
children.push(child);
|
|
117
|
-
child.on("error", (error) => errors.push(error.message));
|
|
118
|
-
child.stdin.on("error", () => {});
|
|
119
|
-
return child;
|
|
120
|
-
}
|
|
121
|
-
async function cleanup() {
|
|
122
|
-
for (const child of children) {
|
|
123
|
-
child.stdin.end();
|
|
124
|
-
await waitFor(() => child.exitCode !== null || child.signalCode !== null, () => "shutdown", 5000).catch(() => {});
|
|
125
|
-
try { if (process.platform !== "win32") process.kill(-child.pid, "SIGKILL"); else child.kill("SIGKILL"); } catch { /* Owned process group already exited. */ }
|
|
126
|
-
}
|
|
127
|
-
if (server?.listening) {
|
|
128
|
-
server.closeAllConnections();
|
|
129
|
-
await new Promise((done) => server.close(done));
|
|
130
|
-
}
|
|
131
|
-
// Remove only fixture-owned worktrees with matching ownership markers/Git roots.
|
|
132
|
-
for (const task of await metadata()) {
|
|
133
|
-
if (task.repository === cwd && task.worktree) {
|
|
134
|
-
const validated = await validateTaskWorktree(task);
|
|
135
|
-
await rm(validated.parent, { recursive: true, force: true });
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
await rm(root, { recursive: true, force: true });
|
|
139
|
-
}
|
|
140
|
-
return { root, cwd, agent, args, env, requests, errors, metadata, start,
|
|
141
|
-
set reviewBehavior(value) { reviewBehavior = value; } };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export async function jsonRun(h, message) {
|
|
145
|
-
const child = h.start(process.execPath, [...h.args, "--mode", "json", "--", message]);
|
|
146
|
-
let stdout = "";
|
|
147
|
-
let stderr = "";
|
|
148
|
-
child.stdout.setEncoding("utf8");
|
|
149
|
-
child.stderr.setEncoding("utf8");
|
|
150
|
-
child.stdout.on("data", (chunk) => { stdout = (stdout + chunk).slice(-1_000_000); });
|
|
151
|
-
child.stderr.on("data", (chunk) => { stderr = (stderr + chunk).slice(-20000); });
|
|
152
|
-
child.stdin.end();
|
|
153
|
-
await waitFor(() => child.exitCode !== null || child.signalCode !== null, () => `${stderr}\n${stdout.slice(-5000)}`);
|
|
154
|
-
return { events: stdout.split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line)), stderr, exitCode: child.exitCode };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export async function rpc(h) {
|
|
158
|
-
const child = h.start();
|
|
159
|
-
const events = [];
|
|
160
|
-
let stderr = "";
|
|
161
|
-
let buffer = "";
|
|
162
|
-
let next = 0;
|
|
163
|
-
child.stdout.setEncoding("utf8");
|
|
164
|
-
child.stderr.setEncoding("utf8");
|
|
165
|
-
child.stderr.on("data", (text) => { stderr = (stderr + text).slice(-20000); });
|
|
166
|
-
child.stdout.on("data", (text) => {
|
|
167
|
-
buffer += text;
|
|
168
|
-
let newline;
|
|
169
|
-
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
170
|
-
const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1);
|
|
171
|
-
try { events.push(JSON.parse(line)); } catch { events.push({ type: "invalid-json", line }); }
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
const send = (value) => child.stdin.write(`${JSON.stringify(value)}\n`);
|
|
175
|
-
const diagnostic = () => JSON.stringify({ stderr, events: events.slice(-8), serverErrors: h.errors });
|
|
176
|
-
async function command(type, values = {}) {
|
|
177
|
-
const id = `rpc-${next++}`;
|
|
178
|
-
send({ id, type, ...values });
|
|
179
|
-
const response = await waitFor(() => events.find((event) => event.type === "response" && event.id === id), diagnostic);
|
|
180
|
-
if (!response.success) throw new Error(response.error);
|
|
181
|
-
return response.data;
|
|
182
|
-
}
|
|
183
|
-
await command("get_state");
|
|
184
|
-
await command("set_auto_retry", { enabled: false });
|
|
185
|
-
return { child, events, send, command, diagnostic,
|
|
186
|
-
wait: (predicate, start = 0) => waitFor(() => events.slice(start).find(predicate), diagnostic),
|
|
187
|
-
async prompt(message) { const start = events.length; await command("prompt", { message }); return start; },
|
|
188
|
-
async settled(start) { return waitFor(() => events.slice(start).find((event) => event.type === "agent_settled"), diagnostic); }
|
|
189
|
-
};
|
|
190
|
-
}
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import { readFile, stat } from "node:fs/promises";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { fixture, rpc, jsonRun, waitFor } from "./harness.mjs";
|
|
6
|
-
|
|
7
|
-
const toolEnd = (name) => (event) => event.type === "tool_execution_end" && event.toolName === name;
|
|
8
|
-
|
|
9
|
-
test("installed Pi RPC: Manual denial/approval and Plan restriction affect actual writes", { timeout: 45000 }, async (t) => {
|
|
10
|
-
const h = await fixture(t);
|
|
11
|
-
const client = await rpc(h);
|
|
12
|
-
let start = await client.prompt("RUNTIME_WRITE create made.txt");
|
|
13
|
-
let dialog = await client.wait((event) => event.type === "extension_ui_request" && event.method === "select", start);
|
|
14
|
-
client.send({ type: "extension_ui_response", id: dialog.id, value: "Deny" });
|
|
15
|
-
assert.equal((await client.wait(toolEnd("write"), start)).isError, true);
|
|
16
|
-
await client.settled(start);
|
|
17
|
-
await assert.rejects(stat(join(h.cwd, "made.txt")), { code: "ENOENT" });
|
|
18
|
-
start = await client.prompt("RUNTIME_WRITE create made.txt with approval");
|
|
19
|
-
dialog = await client.wait((event) => event.type === "extension_ui_request" && event.method === "select", start);
|
|
20
|
-
client.send({ type: "extension_ui_response", id: dialog.id, value: dialog.options.find((option) => option.startsWith("Allow once:")) });
|
|
21
|
-
assert.equal((await client.wait(toolEnd("write"), start)).isError, false);
|
|
22
|
-
await client.settled(start);
|
|
23
|
-
assert.equal(await readFile(join(h.cwd, "made.txt"), "utf8"), "actual tool executed\n");
|
|
24
|
-
await client.prompt("/plan");
|
|
25
|
-
start = await client.prompt("RUNTIME_WRITE PLAN_MUST_BLOCK do not create plan-only.txt");
|
|
26
|
-
assert.equal((await client.wait(toolEnd("write"), start)).isError, true);
|
|
27
|
-
await client.settled(start);
|
|
28
|
-
await assert.rejects(stat(join(h.cwd, "plan-only.txt")), { code: "ENOENT" });
|
|
29
|
-
assert.equal(client.events.some((event) => event.type === "extension_error" || event.type === "invalid-json"), false, client.diagnostic());
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("installed Pi RPC: /plan cannot change modes while the agent is busy", { timeout: 30000 }, async (t) => {
|
|
33
|
-
const h = await fixture(t);
|
|
34
|
-
const client = await rpc(h);
|
|
35
|
-
await client.prompt("RUNTIME_HOLD keep the agent busy");
|
|
36
|
-
await waitFor(() => h.requests.some((request) => request.model === "agent"), client.diagnostic);
|
|
37
|
-
assert.equal((await client.command("get_state")).isStreaming, true);
|
|
38
|
-
const changing = await client.prompt("/plan");
|
|
39
|
-
// RPC acknowledges command handling; extension command errors arrive as events.
|
|
40
|
-
await client.wait((event) => /idle/.test(String(event.error ?? event.message ?? "")), changing);
|
|
41
|
-
await client.command("abort");
|
|
42
|
-
const start = await client.prompt("RUNTIME_WRITE confirm Manual mode was retained");
|
|
43
|
-
const dialog = await client.wait((event) => event.method === "select", start);
|
|
44
|
-
client.send({ type: "extension_ui_response", id: dialog.id, value: "Deny" });
|
|
45
|
-
await client.settled(start);
|
|
46
|
-
await assert.rejects(stat(join(h.cwd, "made.txt")), { code: "ENOENT" });
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
for (const mode of ["acceptEdits", "bypassPermissions", "dontAsk"]) {
|
|
50
|
-
test(`installed Pi JSON: ${mode} retains its launch-time policy`, { timeout: 30000 }, async (t) => {
|
|
51
|
-
const h = await fixture(t, mode);
|
|
52
|
-
const tool = mode === "bypassPermissions" ? "bash" : "write";
|
|
53
|
-
const result = await jsonRun(h, tool === "bash" ? "RUNTIME_BASH print the runtime check" : "RUNTIME_WRITE create made.txt");
|
|
54
|
-
const completed = result.events.find(toolEnd(tool));
|
|
55
|
-
assert.ok(completed, JSON.stringify(result));
|
|
56
|
-
assert.equal(completed.isError, mode === "dontAsk");
|
|
57
|
-
if (mode === "dontAsk") {
|
|
58
|
-
assert.match(JSON.stringify(completed.result), /not-pre-approved/);
|
|
59
|
-
await assert.rejects(stat(join(h.cwd, "made.txt")), { code: "ENOENT" });
|
|
60
|
-
} else if (mode === "acceptEdits") assert.equal(await readFile(join(h.cwd, "made.txt"), "utf8"), "actual tool executed\n");
|
|
61
|
-
else assert.match(JSON.stringify(completed.result), /runtime-bash-check/);
|
|
62
|
-
assert.equal(result.events.some((event) => event.method === "select"), false);
|
|
63
|
-
assert.equal(h.requests.some((request) => request.model === "reviewer"), false);
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
test("installed Pi RPC: Auto reviewer uses real registry/transport and malformed responses fail closed", { timeout: 45000 }, async (t) => {
|
|
68
|
-
const h = await fixture(t, "auto");
|
|
69
|
-
const client = await rpc(h);
|
|
70
|
-
let start = await client.prompt("RUNTIME_BASH print the requested runtime check");
|
|
71
|
-
const allowed = await client.wait(toolEnd("bash"), start);
|
|
72
|
-
assert.equal(allowed.isError, false, JSON.stringify(allowed));
|
|
73
|
-
await client.settled(start);
|
|
74
|
-
assert.ok(h.requests.some((request) => request.model === "reviewer"));
|
|
75
|
-
assert.equal(client.events.slice(start).some((event) => event.method === "select"), false);
|
|
76
|
-
h.reviewBehavior = "malformed";
|
|
77
|
-
start = await client.prompt("RUNTIME_BASH malformed reviewer must block this");
|
|
78
|
-
assert.equal((await client.wait(toolEnd("bash"), start)).isError, true);
|
|
79
|
-
await client.settled(start);
|
|
80
|
-
h.reviewBehavior = "hold";
|
|
81
|
-
start = await client.prompt("RUNTIME_BASH reviewer timeout must block this");
|
|
82
|
-
const timeout = await client.wait(toolEnd("bash"), start);
|
|
83
|
-
assert.equal(timeout.isError, true);
|
|
84
|
-
assert.match(JSON.stringify(timeout.result), /timed out/);
|
|
85
|
-
await client.settled(start);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("installed Pi RPC and real worker: broker approvals, Bash observations, diff/apply/cleanup", { timeout: 60000 }, async (t) => {
|
|
89
|
-
const h = await fixture(t, "auto", ["--background-bash"]);
|
|
90
|
-
const client = await rpc(h);
|
|
91
|
-
await client.prompt("/task RUNTIME_WORKER write worker.txt then print the runtime worker check");
|
|
92
|
-
const task = await waitFor(async () => (await h.metadata()).find((task) => ["completed", "failed", "cancelled"].includes(task.status)), client.diagnostic, 25000);
|
|
93
|
-
assert.equal(task.status, "completed", JSON.stringify(task));
|
|
94
|
-
assert.equal(await readFile(join(task.worktree, "worker.txt"), "utf8"), "actual worker edit\n");
|
|
95
|
-
assert.equal(task.commands.length, 1, JSON.stringify(task));
|
|
96
|
-
assert.equal(task.commands[0].outcome, "completed");
|
|
97
|
-
const reviews = h.requests.filter((request) => request.model === "reviewer");
|
|
98
|
-
assert.ok(reviews.length >= 3); // Task creation, delegated write, delegated Bash.
|
|
99
|
-
assert.ok(reviews.every((request) => !JSON.stringify(request).includes("actual worker edit")));
|
|
100
|
-
assert.match(task.commands[0].command, /runtime-worker-check/);
|
|
101
|
-
await assert.rejects(stat(join(h.cwd, "worker.txt")), { code: "ENOENT" });
|
|
102
|
-
let start = await client.prompt(`/task diff ${task.id}`);
|
|
103
|
-
await client.wait((event) => event.method === "notify" && event.message?.includes("actual worker edit"), start);
|
|
104
|
-
start = client.events.length;
|
|
105
|
-
const applying = client.prompt(`/task apply ${task.id}`);
|
|
106
|
-
const confirmation = await client.wait((event) => event.method === "confirm", start);
|
|
107
|
-
assert.match(confirmation.message, /worker.txt/);
|
|
108
|
-
client.send({ type: "extension_ui_response", id: confirmation.id, confirmed: true });
|
|
109
|
-
await applying;
|
|
110
|
-
assert.equal(await readFile(join(h.cwd, "worker.txt"), "utf8"), "actual worker edit\n");
|
|
111
|
-
start = client.events.length;
|
|
112
|
-
const cleaning = client.prompt(`/task clean ${task.id}`);
|
|
113
|
-
const cleanup = await client.wait((event) => event.method === "confirm", start);
|
|
114
|
-
client.send({ type: "extension_ui_response", id: cleanup.id, confirmed: true });
|
|
115
|
-
await cleaning;
|
|
116
|
-
await assert.rejects(stat(task.worktree), { code: "ENOENT" });
|
|
117
|
-
assert.equal(h.errors.length, 0);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("installed Pi worker without Bash opt-in cannot run a command or claim observed verification", { timeout: 45000 }, async (t) => {
|
|
121
|
-
const h = await fixture(t, "auto");
|
|
122
|
-
const client = await rpc(h);
|
|
123
|
-
await client.prompt("/task RUNTIME_WORKER write worker.txt; Bash is not enabled");
|
|
124
|
-
const task = await waitFor(async () => (await h.metadata()).find((task) => ["completed", "failed", "cancelled"].includes(task.status)), client.diagnostic, 25000);
|
|
125
|
-
assert.equal(task.status, "completed", JSON.stringify(task));
|
|
126
|
-
assert.equal(await readFile(join(task.worktree, "worker.txt"), "utf8"), "actual worker edit\n");
|
|
127
|
-
assert.deepEqual(task.commands, []);
|
|
128
|
-
assert.match(task.output, /I ran npm test and all tests passed/);
|
|
129
|
-
const workerRequests = h.requests.filter((request) => request.model === "agent");
|
|
130
|
-
assert.ok(workerRequests.length >= 2);
|
|
131
|
-
assert.ok(workerRequests.every((request) => !request.tools.some((tool) => ["bash", "worker_bash"].includes(tool.function.name))));
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("installed Pi missing worker wrappers never fall back to unrestricted built-in writes", { timeout: 30000 }, async (t) => {
|
|
135
|
-
const h = await fixture(t, "bypassPermissions", ["--no-builtin-tools", "--tools", "worker_write"]);
|
|
136
|
-
const result = await jsonRun(h, "RUNTIME_WRITE attempt a built-in write with the worker guard absent");
|
|
137
|
-
await assert.rejects(stat(join(h.cwd, "made.txt")), { code: "ENOENT" });
|
|
138
|
-
assert.ok(result.exitCode !== 0 || result.events.some((event) => toolEnd("write")(event) && event.isError), JSON.stringify(result));
|
|
139
|
-
assert.ok(h.requests.every((request) => !(request.tools ?? []).some((tool) => tool.function.name === "write")));
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("installed Pi JSON: missing approval UI denies and repeated denials abort without writing", { timeout: 30000 }, async (t) => {
|
|
143
|
-
const h = await fixture(t);
|
|
144
|
-
const result = await jsonRun(h, "RUNTIME_WRITE RUNTIME_REPEAT try the same write repeatedly");
|
|
145
|
-
await assert.rejects(stat(join(h.cwd, "made.txt")), { code: "ENOENT" });
|
|
146
|
-
const writes = result.events.filter(toolEnd("write"));
|
|
147
|
-
assert.equal(writes.length, 3);
|
|
148
|
-
assert.ok(writes.every((event) => event.isError));
|
|
149
|
-
assert.match(result.stderr, /permission_blocked: denial limit reached/);
|
|
150
|
-
assert.ok(result.events.some((event) => event.type === "entry_appended" && event.entry?.customType === "piexis-permission-stop"));
|
|
151
|
-
assert.equal(result.events.some((event) => event.method === "select"), false);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("installed Pi RPC: a stale worker approval cannot survive session replacement", { timeout: 45000 }, async (t) => {
|
|
155
|
-
const h = await fixture(t);
|
|
156
|
-
const client = await rpc(h);
|
|
157
|
-
const start = await client.prompt("/task RUNTIME_WORKER write worker.txt");
|
|
158
|
-
const dialog = await client.wait((event) => event.method === "select", start);
|
|
159
|
-
const task = (await h.metadata()).find((task) => task.status === "running");
|
|
160
|
-
assert.ok(task?.pid);
|
|
161
|
-
await client.command("new_session");
|
|
162
|
-
client.send({ type: "extension_ui_response", id: dialog.id, value: dialog.options.find((option) => option.startsWith("Allow once:")) });
|
|
163
|
-
await assert.rejects(stat(join(task.worktree, "worker.txt")), { code: "ENOENT" });
|
|
164
|
-
assert.equal((await h.metadata()).find((item) => item.id === task.id).status, "interrupted");
|
|
165
|
-
assert.throws(() => process.kill(task.pid, 0), { code: "ESRCH" });
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test("installed Pi RPC: orderly stdin EOF shuts down a real active worker", { timeout: 45000 }, async (t) => {
|
|
169
|
-
const h = await fixture(t);
|
|
170
|
-
const client = await rpc(h);
|
|
171
|
-
await client.prompt("/task RUNTIME_HOLD wait without executing tools");
|
|
172
|
-
const task = await waitFor(async () => (await h.metadata()).find((task) => task.status === "running" && task.pid), client.diagnostic);
|
|
173
|
-
await waitFor(() => h.requests.some((request) => request.model === "agent"), client.diagnostic);
|
|
174
|
-
client.child.stdin.end();
|
|
175
|
-
await waitFor(() => client.child.exitCode !== null, client.diagnostic);
|
|
176
|
-
assert.equal(client.child.exitCode, 0, client.diagnostic());
|
|
177
|
-
assert.throws(() => process.kill(task.pid, 0), { code: "ESRCH" });
|
|
178
|
-
assert.equal((await h.metadata()).find((item) => item.id === task.id).status, "interrupted");
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("installed Pi: session replacement stops a real waiting worker and retains interrupted state", { timeout: 45000 }, async (t) => {
|
|
182
|
-
const h = await fixture(t);
|
|
183
|
-
const client = await rpc(h);
|
|
184
|
-
await client.prompt("/task RUNTIME_HOLD wait without executing tools");
|
|
185
|
-
const task = await waitFor(async () => (await h.metadata()).find((task) => task.status === "running" && task.pid), client.diagnostic);
|
|
186
|
-
assert.doesNotThrow(() => process.kill(task.pid, 0));
|
|
187
|
-
await waitFor(() => h.requests.some((request) => request.model === "agent" && JSON.stringify(request.messages).includes("RUNTIME_HOLD")), client.diagnostic);
|
|
188
|
-
await client.command("new_session");
|
|
189
|
-
await waitFor(() => { try { process.kill(task.pid, 0); return false; } catch (error) { return error.code === "ESRCH"; } }, client.diagnostic);
|
|
190
|
-
assert.equal((await h.metadata()).find((item) => item.id === task.id).status, "interrupted");
|
|
191
|
-
assert.ok(await stat(task.worktree));
|
|
192
|
-
});
|