pi-t3-bridge 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/pstack/brief.ts +33 -9
- package/src/pstack/child.ts +53 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Change log
|
|
2
2
|
|
|
3
|
+
## 0.8.2, 6 November 2026
|
|
4
|
+
|
|
5
|
+
Version 0.8.2 closes a hole in the child sandbox. A delegated child whose brief missed a section or a header field ran with no sandbox and no limits, because the bridge treated it as the user's own session. Such a child could rewrite the parent's git hooks. Now every tool in that child is blocked, and the block lists what the brief is missing, so the parent can resend a complete one. A top-level session is never blocked, even when its first message starts with `# Task:`.
|
|
6
|
+
|
|
3
7
|
## 0.8.1, 6 November 2026
|
|
4
8
|
|
|
5
9
|
Version 0.8.1 lets a delegated child run `pnpm install` and `git commit` in a pnpm and lefthook repo, under `bwrap` on Linux and `sandbox-exec` on macOS. Each child gets its own `XDG_RUNTIME_DIR` in the temp folder, where pnpm 12 takes its store lock. The sandbox can now write to the store that `pnpm store path` names for the workdir, and the child uses that store, so it shares the parent's store and does not build a second one in the workdir.
|
package/README.md
CHANGED
|
@@ -230,7 +230,7 @@ To choose your own models, run the `setup-pstack` skill, which writes a model sh
|
|
|
230
230
|
|
|
231
231
|
### Limits for delegated children
|
|
232
232
|
|
|
233
|
-
When the first message of a session is a complete pstack brief, the extension treats the session as a delegated child and holds it to the limits in the brief. The child can't lift the limits through anything it writes.
|
|
233
|
+
When the first message of a session is a complete pstack brief, the extension treats the session as a delegated child and holds it to the limits in the brief. The child can't lift the limits through anything it writes. A T3-delegated child whose brief has a `# Task:` heading but misses a section or a header field gets every tool blocked, with a list of what is missing, so it never runs without the sandbox.
|
|
234
234
|
|
|
235
235
|
```mermaid
|
|
236
236
|
sequenceDiagram
|
package/package.json
CHANGED
package/src/pstack/brief.ts
CHANGED
|
@@ -253,10 +253,9 @@ const dispatchable = (brief: Brief): boolean =>
|
|
|
253
253
|
brief.diskMb !== undefined &&
|
|
254
254
|
SECTIONS.every((section) => brief.sections.has(section));
|
|
255
255
|
|
|
256
|
-
|
|
257
|
-
export const sessionBrief = (
|
|
256
|
+
const firstMessage = (
|
|
258
257
|
ctx: ExtensionContext
|
|
259
|
-
): { readonly
|
|
258
|
+
): { readonly at: number; readonly text: string } | undefined => {
|
|
260
259
|
const first = ctx.sessionManager
|
|
261
260
|
.getBranch()
|
|
262
261
|
.find((entry) => entry.type === "message" && entry.message.role === "user");
|
|
@@ -267,13 +266,38 @@ export const sessionBrief = (
|
|
|
267
266
|
|
|
268
267
|
const { content } = first.message;
|
|
269
268
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
269
|
+
return {
|
|
270
|
+
at: Date.parse(first.timestamp),
|
|
271
|
+
text: isString(content)
|
|
272
|
+
? content
|
|
273
|
+
: content
|
|
274
|
+
.map((part) => (part.type === "text" ? part.text : ""))
|
|
275
|
+
.join("\n"),
|
|
276
|
+
};
|
|
277
|
+
};
|
|
273
278
|
|
|
274
|
-
|
|
279
|
+
// The brief that started this session, when it is a delegated child.
|
|
280
|
+
export const sessionBrief = (
|
|
281
|
+
ctx: ExtensionContext
|
|
282
|
+
): { readonly brief: Brief; readonly startedAt: number } | undefined => {
|
|
283
|
+
const first = firstMessage(ctx);
|
|
284
|
+
const brief = first === undefined ? undefined : parseBrief(first.text);
|
|
285
|
+
|
|
286
|
+
return first === undefined || brief === undefined || !dispatchable(brief)
|
|
287
|
+
? undefined
|
|
288
|
+
: { brief, startedAt: first.at };
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// What is wrong with a brief that has a `# Task:` heading but is not complete
|
|
292
|
+
// enough to hold a child to. Undefined when there is no such brief.
|
|
293
|
+
export const incompleteBrief = (
|
|
294
|
+
ctx: ExtensionContext,
|
|
295
|
+
rules: BriefRules
|
|
296
|
+
): string[] | undefined => {
|
|
297
|
+
const first = firstMessage(ctx);
|
|
298
|
+
const brief = first === undefined ? undefined : parseBrief(first.text);
|
|
275
299
|
|
|
276
|
-
return brief === undefined ||
|
|
300
|
+
return first === undefined || brief === undefined || dispatchable(brief)
|
|
277
301
|
? undefined
|
|
278
|
-
:
|
|
302
|
+
: briefProblems(first.text, rules);
|
|
279
303
|
};
|
package/src/pstack/child.ts
CHANGED
|
@@ -22,9 +22,9 @@ import {
|
|
|
22
22
|
|
|
23
23
|
import { bashTimeoutSeconds } from "../bridge/bash-timeout.ts";
|
|
24
24
|
import { appendLedger } from "../bridge/state.ts";
|
|
25
|
-
import { ownThreadId } from "../bridge/thread.ts";
|
|
26
|
-
import { sessionBrief } from "./brief.ts";
|
|
27
|
-
import type { Brief } from "./brief.ts";
|
|
25
|
+
import { isDelegatedChild, ownThreadId } from "../bridge/thread.ts";
|
|
26
|
+
import { incompleteBrief, sessionBrief } from "./brief.ts";
|
|
27
|
+
import type { Brief, BriefRules } from "./brief.ts";
|
|
28
28
|
import { policyPath } from "./resources.ts";
|
|
29
29
|
import {
|
|
30
30
|
chooseSandbox,
|
|
@@ -122,10 +122,22 @@ const isExecFailure = compile(
|
|
|
122
122
|
)
|
|
123
123
|
).check;
|
|
124
124
|
|
|
125
|
+
const isLimits = compile(obj({ files: num(), minutes: num() })).check;
|
|
126
|
+
|
|
125
127
|
const isPolicy = compile(
|
|
126
128
|
obj(
|
|
127
129
|
{
|
|
128
|
-
briefs: opt(
|
|
130
|
+
briefs: opt(
|
|
131
|
+
obj(
|
|
132
|
+
{
|
|
133
|
+
noWrites: opt(arr(str())),
|
|
134
|
+
redBeforeGreen: opt(arr(str())),
|
|
135
|
+
sizes: opt(obj({}, { open: true })),
|
|
136
|
+
},
|
|
137
|
+
{ open: true }
|
|
138
|
+
)
|
|
139
|
+
),
|
|
140
|
+
jobs: opt(obj({}, { open: true })),
|
|
129
141
|
},
|
|
130
142
|
{ open: true }
|
|
131
143
|
)
|
|
@@ -139,10 +151,27 @@ const isPathInput = compile(obj({ path: str() }, { open: true })).check;
|
|
|
139
151
|
|
|
140
152
|
const isNumber = compile(num()).check;
|
|
141
153
|
|
|
142
|
-
const
|
|
154
|
+
const readPolicy = () => {
|
|
143
155
|
const policy: unknown = JSON.parse(readFileSync(policyPath(), "utf-8"));
|
|
144
156
|
|
|
145
|
-
return isPolicy(policy) ?
|
|
157
|
+
return isPolicy(policy) ? policy : {};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const readRedBeforeGreen = (): readonly string[] =>
|
|
161
|
+
readPolicy().briefs?.redBeforeGreen ?? [];
|
|
162
|
+
|
|
163
|
+
const readBriefRules = (): BriefRules => {
|
|
164
|
+
const { briefs, jobs } = readPolicy();
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
jobs: Object.keys(jobs ?? {}),
|
|
168
|
+
noWrites: briefs?.noWrites ?? [],
|
|
169
|
+
sizes: Object.fromEntries(
|
|
170
|
+
Object.entries(briefs?.sizes ?? {}).flatMap(([size, limits]) =>
|
|
171
|
+
isLimits(limits) ? [[size, limits] as const] : []
|
|
172
|
+
)
|
|
173
|
+
),
|
|
174
|
+
};
|
|
146
175
|
};
|
|
147
176
|
|
|
148
177
|
const changedFiles = async (cwd: string): Promise<string[]> => {
|
|
@@ -227,6 +256,7 @@ interface ChildState {
|
|
|
227
256
|
diskCheck: Promise<void>;
|
|
228
257
|
diskOver: boolean;
|
|
229
258
|
diskQueued: boolean;
|
|
259
|
+
delegated: boolean | undefined;
|
|
230
260
|
gitDir: string | undefined;
|
|
231
261
|
greenAtStart: boolean;
|
|
232
262
|
ready: boolean;
|
|
@@ -237,6 +267,7 @@ export const registerChild = (pi: ExtensionAPI): void => {
|
|
|
237
267
|
const state: ChildState = {
|
|
238
268
|
baselineKb: undefined,
|
|
239
269
|
before: undefined,
|
|
270
|
+
delegated: undefined,
|
|
240
271
|
diskCheck: Promise.resolve(),
|
|
241
272
|
diskOver: false,
|
|
242
273
|
diskQueued: false,
|
|
@@ -331,7 +362,22 @@ export const registerChild = (pi: ExtensionAPI): void => {
|
|
|
331
362
|
const started = sessionBrief(ctx);
|
|
332
363
|
|
|
333
364
|
if (started === undefined) {
|
|
334
|
-
|
|
365
|
+
// A delegated child whose brief is incomplete would otherwise run with
|
|
366
|
+
// no sandbox and no limits, as if it were the user's own session.
|
|
367
|
+
const problems = incompleteBrief(ctx, readBriefRules());
|
|
368
|
+
|
|
369
|
+
if (problems === undefined) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
state.delegated ??= await isDelegatedChild();
|
|
374
|
+
|
|
375
|
+
return state.delegated
|
|
376
|
+
? {
|
|
377
|
+
block: true,
|
|
378
|
+
reason: `This delegated task's brief is incomplete, so it can't run in the sandbox and every tool is blocked. Use no more tools. Report STATUS: BLOCKED (incomplete brief) with these problems, so the parent can resend a complete brief:\n- ${problems.join("\n- ")}`,
|
|
379
|
+
}
|
|
380
|
+
: undefined;
|
|
335
381
|
}
|
|
336
382
|
|
|
337
383
|
const { brief, startedAt } = started;
|