@bridge4dev/runner 0.60.0 → 0.62.0

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/dist/journal.js CHANGED
@@ -21,6 +21,23 @@ export class SessionJournal {
21
21
  lastStatus = null;
22
22
  /** The agent's conversation tip, and the provider session it lives in. */
23
23
  lastAnchor = null;
24
+ /**
25
+ * Every card this journal has published, and whether it is still open —
26
+ * in the order they were asked (#392).
27
+ *
28
+ * The persistent twin of the supervisor's `running.openQuestions`: that set
29
+ * dies with the process, and a process that dies ungracefully never gets to
30
+ * `shutdown()`, which is the only place it withdraws them. This one is
31
+ * rebuilt from the file, so the next process can close what the last one
32
+ * left open — the card in the browser does not know the runner restarted.
33
+ *
34
+ * Closed cards are remembered too, not only dropped: «this card was closed
35
+ * by somebody» is the fact that stops a SECOND tombstone. A queued answer
36
+ * that reaches the next process after a restore has already closed the card
37
+ * must rescue the words and say nothing more about the card — and the only
38
+ * witness that the card was closed is this file.
39
+ */
40
+ asks = new Map();
24
41
  constructor(sessionId, dir = journalDir()) {
25
42
  this.sessionId = sessionId;
26
43
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -50,6 +67,16 @@ export class SessionJournal {
50
67
  });
51
68
  if (parsed.seq >= this.nextSeq)
52
69
  this.nextSeq = parsed.seq + 1;
70
+ // The event lines carry the same fact as `ask_open`/`ask_closed`, and
71
+ // replaying both is harmless: the set is a set.
72
+ this.trackAsk(parsed.eventType, parsed.payload, false);
73
+ }
74
+ else if (parsed.kind === 'ask_open') {
75
+ if (this.asks.get(parsed.askId) !== 'closed')
76
+ this.asks.set(parsed.askId, 'open');
77
+ }
78
+ else if (parsed.kind === 'ask_closed') {
79
+ this.asks.set(parsed.askId, 'closed');
53
80
  }
54
81
  else if (parsed.kind === 'ack') {
55
82
  this.unackedBySeq.delete(parsed.seq);
@@ -107,6 +134,11 @@ export class SessionJournal {
107
134
  if (this.lastAnchor) {
108
135
  lines.push({ kind: 'anchor', ...this.lastAnchor });
109
136
  }
137
+ // AFTER the event lines: an unacked `question` re-written above would
138
+ // otherwise reopen, on the next replay, a card these lines say is closed.
139
+ for (const [askId, state] of this.asks) {
140
+ lines.push({ kind: state === 'open' ? 'ask_open' : 'ask_closed', askId });
141
+ }
110
142
  if (this.lastStatus) {
111
143
  lines.push({
112
144
  kind: 'status',
@@ -182,8 +214,58 @@ export class SessionJournal {
182
214
  const event = { seq: this.nextSeq++, eventType, payload };
183
215
  this.write({ kind: 'event', ...event, ts: new Date().toISOString() });
184
216
  this.unackedBySeq.set(event.seq, event);
217
+ this.trackAsk(eventType, payload, true);
185
218
  return event;
186
219
  }
220
+ /**
221
+ * Keep `openAsks` in step with the cards going out through `append` (#392).
222
+ *
223
+ * Here and not in the supervisor, because `append` is the one door every
224
+ * event takes: a card that was published was published through it, and so
225
+ * was every resolution — the adapter's own, `withdrawOpenQuestions`, the
226
+ * `not_open` miss. A set kept beside the callers would need every one of
227
+ * them to remember it, which is exactly how the in-memory set came to be
228
+ * empty at the moment it was needed.
229
+ *
230
+ * A resolution for a card this journal never published writes nothing:
231
+ * there is nothing to close, and remembering strangers would only grow the
232
+ * file.
233
+ */
234
+ trackAsk(eventType, payload, persist) {
235
+ if (eventType !== 'question' && eventType !== 'question_resolved')
236
+ return;
237
+ const askId = payload['askId'];
238
+ if (typeof askId !== 'string' || !askId)
239
+ return;
240
+ const state = this.asks.get(askId);
241
+ if (eventType === 'question') {
242
+ // A card is asked once; a replayed event line for one already closed
243
+ // (compaction keeps unacked events) must not reopen it.
244
+ if (state !== undefined)
245
+ return;
246
+ this.asks.set(askId, 'open');
247
+ if (persist)
248
+ this.write({ kind: 'ask_open', askId });
249
+ return;
250
+ }
251
+ if (state !== 'open')
252
+ return;
253
+ this.asks.set(askId, 'closed');
254
+ if (persist)
255
+ this.write({ kind: 'ask_closed', askId });
256
+ }
257
+ /** The cards still waiting on a person, oldest first (#392). */
258
+ openAskIds() {
259
+ return [...this.asks].filter(([, state]) => state === 'open').map(([askId]) => askId);
260
+ }
261
+ /**
262
+ * What this journal knows about one card (#392): `open`, `closed`, or
263
+ * nothing at all — a card asked by a runner from before these lines existed,
264
+ * or one this state directory never saw.
265
+ */
266
+ askState(askId) {
267
+ return this.asks.get(askId);
268
+ }
187
269
  /**
188
270
  * Never reuse a seq the API already stored: after a runner state-dir wipe the
189
271
  * local counter restarts at 1 and every replayed event would collide with an
package/dist/policy.d.ts CHANGED
@@ -40,6 +40,33 @@ export interface AgentGitPolicy {
40
40
  agentAllowForcePush?: boolean;
41
41
  /** `git reset --hard` and `git clean`. `undefined` and `false` mean no. */
42
42
  agentAllowDestructiveGit?: boolean;
43
+ /**
44
+ * #418: may the agent read and write OUTSIDE the folder this session works
45
+ * in — other projects on the same machine included?
46
+ *
47
+ * READ AS `=== true`. `undefined` means an API too old to send the field, or
48
+ * a value the protocol schema threw away, and there the answer must be what
49
+ * it has always been: confined to the folder. The same direction as
50
+ * `agentAllowForcePush` two fields up, the opposite of `agentPushBan` at the
51
+ * top — one names a permission, the other names a ban, and silence refuses
52
+ * in both readings (gotcha 193).
53
+ *
54
+ * **Not a git setting, and it lives in a git-named object on purpose.** The
55
+ * whole delivery pipe for the project's per-agent policy already exists —
56
+ * descriptor → `gitPolicyOf` → `SessionSpec` → `policyContextFor` →
57
+ * `PolicyContext`, with `workspace_settings` for a live change — and it moves
58
+ * this object WHOLE at every hop. A field of its own beside it would have
59
+ * been a second branch in the supervisor, a second one in the live frame and
60
+ * a second chance to forget one of them (plan R8). The name is the price;
61
+ * this comment is the receipt.
62
+ *
63
+ * What it does NOT lift: `isSecretPath`, `isGitInternalPath`,
64
+ * `ctx.agentPromptFile`, `DENIED_COMMAND_PATTERNS`, `SECRET_COMMAND_PATTERNS`,
65
+ * the git policy above, the auto-commit refusal and the heap ceiling. It
66
+ * lifts exactly one rule — «this path is not inside the folder» — and leaves
67
+ * every other reason to refuse standing.
68
+ */
69
+ agentAllowOutsideFolder?: boolean;
43
70
  }
44
71
  export interface PolicyContext extends AgentGitPolicy {
45
72
  trustMode: TrustMode;
package/dist/policy.js CHANGED
@@ -11,6 +11,8 @@ function resolveGitPolicy(ctx) {
11
11
  protectedBranches: ctx.agentProtectedBranches ?? DEFAULT_PROTECTED_BRANCHES,
12
12
  allowForcePush: ctx.agentAllowForcePush === true,
13
13
  allowDestructiveGit: ctx.agentAllowDestructiveGit === true,
14
+ // `=== true`: silence keeps the agent in its folder (#418).
15
+ allowOutsideFolder: ctx.agentAllowOutsideFolder === true,
14
16
  };
15
17
  }
16
18
  // ─── Secret masking (plan §8.7) ──────────────────────────────────────
@@ -1309,6 +1311,18 @@ export function evaluateToolUse(toolName, input, ctx) {
1309
1311
  if (READ_TOOLS.has(toolName) || WRITE_TOOLS.has(toolName)) {
1310
1312
  const rawPath = String(input['file_path'] ?? input['path'] ?? input['notebook_path'] ?? '');
1311
1313
  const resolved = rawPath ? normalize(rawPath, ctx.worktreePath) : ctx.worktreePath;
1314
+ /**
1315
+ * #418: this project answered «yes» to «may the agent work outside the
1316
+ * project folder», so «not inside the folder» stops being a reason on its
1317
+ * own. Resolved through `resolveGitPolicy` rather than read off `ctx` here,
1318
+ * because that function is the one place in this file where an absent field
1319
+ * is given its safe meaning — and «unknown» here has to mean «stay in».
1320
+ *
1321
+ * It moves exactly one rule out of the way. Everything checked above and
1322
+ * below this line — secret paths, `.git` internals, the session's own
1323
+ * prompt file — is checked in the same order and refuses the same things.
1324
+ */
1325
+ const outsideAllowed = resolveGitPolicy(ctx).allowOutsideFolder;
1312
1326
  if (isSecretPath(resolved)) {
1313
1327
  return { decision: 'deny', reason: 'protected secret path' };
1314
1328
  }
@@ -1328,7 +1342,9 @@ export function evaluateToolUse(toolName, input, ctx) {
1328
1342
  reason: 'writing inside .git is not allowed — a hook or a config entry is code git runs on its own, past every rule here',
1329
1343
  };
1330
1344
  }
1331
- if (WRITE_TOOLS.has(toolName) && !isInsideWorktree(resolved, ctx.worktreePath)) {
1345
+ if (WRITE_TOOLS.has(toolName) &&
1346
+ !outsideAllowed &&
1347
+ !isInsideWorktree(resolved, ctx.worktreePath)) {
1332
1348
  return { decision: 'deny', reason: 'writes outside the session worktree are not allowed' };
1333
1349
  }
1334
1350
  // The project's own prompt file — the same rule as `.git` above, for the
@@ -1344,10 +1360,27 @@ export function evaluateToolUse(toolName, input, ctx) {
1344
1360
  }
1345
1361
  if (trust === 'STRICT')
1346
1362
  return { decision: 'ask', reason: 'strict mode' };
1347
- if (READ_TOOLS.has(toolName) && !isInsideWorktree(resolved, ctx.worktreePath)) {
1348
- return trust === 'AUTO'
1349
- ? { decision: 'allow', reason: 'auto mode' }
1350
- : { decision: 'ask', reason: 'read outside the worktree' };
1363
+ if (!isInsideWorktree(resolved, ctx.worktreePath)) {
1364
+ /**
1365
+ * #418. Below STRICT — which asked one line up and goes on asking, which
1366
+ * is the whole of «Strict still asks» — a path outside the folder is now
1367
+ * an ORDINARY path for a project that allowed it: no card, no refusal,
1368
+ * and the same answer for a read and for a write.
1369
+ *
1370
+ * Its own reason string rather than falling through to «inside worktree»
1371
+ * below: that sentence goes to the log, and about a file in another
1372
+ * folder it would simply be false.
1373
+ */
1374
+ if (outsideAllowed) {
1375
+ return { decision: 'allow', reason: 'outside the project folder, allowed by this project' };
1376
+ }
1377
+ // Reads only: a write outside was refused above unless the project
1378
+ // allowed it, so nothing else reaches this line.
1379
+ if (READ_TOOLS.has(toolName)) {
1380
+ return trust === 'AUTO'
1381
+ ? { decision: 'allow', reason: 'auto mode' }
1382
+ : { decision: 'ask', reason: 'read outside the worktree' };
1383
+ }
1351
1384
  }
1352
1385
  return { decision: 'allow', reason: 'inside worktree' };
1353
1386
  }
@@ -32,6 +32,20 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
32
32
  * behaviour every runner had before this release.
33
33
  */
34
34
  pausedUntil: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
35
+ /**
36
+ * Subagents the API still believes to be alive in this session (#393).
37
+ *
38
+ * The number is the dead process's last word, kept by the API: a runner
39
+ * restart takes every subagent with it, and the new process seeds its own
40
+ * count at zero — so the descriptor is the only place the number survives
41
+ * long enough to be written down. The API sends it only while it still
42
+ * believes it (its own trust window on the count's age); absent or zero
43
+ * means «nothing to say», and a restore says nothing.
44
+ *
45
+ * `.catch(undefined)` like its neighbours: one malformed value must cost
46
+ * its own session a line at most, never the frame (QA-100 MAJOR-1).
47
+ */
48
+ backgroundTasks: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
35
49
  /**
36
50
  * The newest published version of THIS session's agent — Р13, §4.8.
37
51
  *
@@ -99,6 +113,18 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
99
113
  agentProtectedBranches: z.ZodCatch<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
100
114
  agentAllowForcePush: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
101
115
  agentAllowDestructiveGit: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
116
+ /**
117
+ * #418: may the agent read and write OUTSIDE the project folder?
118
+ *
119
+ * `.optional().catch(undefined)` for the QA-100 MAJOR-1 reason above, and
120
+ * safe here for the same reason as `agentPushBan` — though the polarity
121
+ * runs the other way. `policy.ts` resolves this one with `=== true`, so a
122
+ * value this schema throws away leaves the session confined to its folder:
123
+ * strictly more restricted, never less. Rubbish in this field therefore
124
+ * costs a project its permission, which is the direction a dropped value is
125
+ * allowed to fail in.
126
+ */
127
+ agentAllowOutsideFolder: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
102
128
  budgetUsd: z.ZodNullable<z.ZodNumber>;
103
129
  budgetMinutes: z.ZodNullable<z.ZodNumber>;
104
130
  }, "strip", z.ZodTypeAny, {
@@ -114,6 +140,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
114
140
  agentProtectedBranches?: string[] | undefined;
115
141
  agentAllowForcePush?: boolean | undefined;
116
142
  agentAllowDestructiveGit?: boolean | undefined;
143
+ agentAllowOutsideFolder?: boolean | undefined;
117
144
  }, {
118
145
  path: string;
119
146
  id: string;
@@ -127,6 +154,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
127
154
  agentProtectedBranches?: unknown;
128
155
  agentAllowForcePush?: unknown;
129
156
  agentAllowDestructiveGit?: unknown;
157
+ agentAllowOutsideFolder?: unknown;
130
158
  }>;
131
159
  /**
132
160
  * «Run this one without the project's agent prompt.» Absent means no — both
@@ -234,6 +262,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
234
262
  agentProtectedBranches?: string[] | undefined;
235
263
  agentAllowForcePush?: boolean | undefined;
236
264
  agentAllowDestructiveGit?: boolean | undefined;
265
+ agentAllowOutsideFolder?: boolean | undefined;
237
266
  };
238
267
  tickets: {
239
268
  number: number;
@@ -246,6 +275,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
246
275
  } | undefined;
247
276
  workMode?: "DIRECT" | "BRANCH" | undefined;
248
277
  pausedUntil?: string | null | undefined;
278
+ backgroundTasks?: number | undefined;
249
279
  agentLatestVersion?: string | undefined;
250
280
  skipAgentPrompt?: boolean | undefined;
251
281
  branchHint?: string | undefined;
@@ -275,6 +305,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
275
305
  agentProtectedBranches?: unknown;
276
306
  agentAllowForcePush?: unknown;
277
307
  agentAllowDestructiveGit?: unknown;
308
+ agentAllowOutsideFolder?: unknown;
278
309
  };
279
310
  tickets: {
280
311
  number: number;
@@ -295,6 +326,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
295
326
  activeMsBase?: number | undefined;
296
327
  extraBudgetMinutes?: number | null | undefined;
297
328
  pausedUntil?: unknown;
329
+ backgroundTasks?: unknown;
298
330
  agentLatestVersion?: unknown;
299
331
  skipAgentPrompt?: unknown;
300
332
  branchHint?: unknown;
@@ -450,6 +482,20 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
450
482
  * behaviour every runner had before this release.
451
483
  */
452
484
  pausedUntil: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
485
+ /**
486
+ * Subagents the API still believes to be alive in this session (#393).
487
+ *
488
+ * The number is the dead process's last word, kept by the API: a runner
489
+ * restart takes every subagent with it, and the new process seeds its own
490
+ * count at zero — so the descriptor is the only place the number survives
491
+ * long enough to be written down. The API sends it only while it still
492
+ * believes it (its own trust window on the count's age); absent or zero
493
+ * means «nothing to say», and a restore says nothing.
494
+ *
495
+ * `.catch(undefined)` like its neighbours: one malformed value must cost
496
+ * its own session a line at most, never the frame (QA-100 MAJOR-1).
497
+ */
498
+ backgroundTasks: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
453
499
  /**
454
500
  * The newest published version of THIS session's agent — Р13, §4.8.
455
501
  *
@@ -517,6 +563,18 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
517
563
  agentProtectedBranches: z.ZodCatch<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
518
564
  agentAllowForcePush: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
519
565
  agentAllowDestructiveGit: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
566
+ /**
567
+ * #418: may the agent read and write OUTSIDE the project folder?
568
+ *
569
+ * `.optional().catch(undefined)` for the QA-100 MAJOR-1 reason above, and
570
+ * safe here for the same reason as `agentPushBan` — though the polarity
571
+ * runs the other way. `policy.ts` resolves this one with `=== true`, so a
572
+ * value this schema throws away leaves the session confined to its folder:
573
+ * strictly more restricted, never less. Rubbish in this field therefore
574
+ * costs a project its permission, which is the direction a dropped value is
575
+ * allowed to fail in.
576
+ */
577
+ agentAllowOutsideFolder: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
520
578
  budgetUsd: z.ZodNullable<z.ZodNumber>;
521
579
  budgetMinutes: z.ZodNullable<z.ZodNumber>;
522
580
  }, "strip", z.ZodTypeAny, {
@@ -532,6 +590,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
532
590
  agentProtectedBranches?: string[] | undefined;
533
591
  agentAllowForcePush?: boolean | undefined;
534
592
  agentAllowDestructiveGit?: boolean | undefined;
593
+ agentAllowOutsideFolder?: boolean | undefined;
535
594
  }, {
536
595
  path: string;
537
596
  id: string;
@@ -545,6 +604,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
545
604
  agentProtectedBranches?: unknown;
546
605
  agentAllowForcePush?: unknown;
547
606
  agentAllowDestructiveGit?: unknown;
607
+ agentAllowOutsideFolder?: unknown;
548
608
  }>;
549
609
  /**
550
610
  * «Run this one without the project's agent prompt.» Absent means no — both
@@ -652,6 +712,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
652
712
  agentProtectedBranches?: string[] | undefined;
653
713
  agentAllowForcePush?: boolean | undefined;
654
714
  agentAllowDestructiveGit?: boolean | undefined;
715
+ agentAllowOutsideFolder?: boolean | undefined;
655
716
  };
656
717
  tickets: {
657
718
  number: number;
@@ -664,6 +725,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
664
725
  } | undefined;
665
726
  workMode?: "DIRECT" | "BRANCH" | undefined;
666
727
  pausedUntil?: string | null | undefined;
728
+ backgroundTasks?: number | undefined;
667
729
  agentLatestVersion?: string | undefined;
668
730
  skipAgentPrompt?: boolean | undefined;
669
731
  branchHint?: string | undefined;
@@ -693,6 +755,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
693
755
  agentProtectedBranches?: unknown;
694
756
  agentAllowForcePush?: unknown;
695
757
  agentAllowDestructiveGit?: unknown;
758
+ agentAllowOutsideFolder?: unknown;
696
759
  };
697
760
  tickets: {
698
761
  number: number;
@@ -713,6 +776,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
713
776
  activeMsBase?: number | undefined;
714
777
  extraBudgetMinutes?: number | null | undefined;
715
778
  pausedUntil?: unknown;
779
+ backgroundTasks?: unknown;
716
780
  agentLatestVersion?: unknown;
717
781
  skipAgentPrompt?: unknown;
718
782
  branchHint?: unknown;
@@ -747,6 +811,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
747
811
  agentProtectedBranches?: string[] | undefined;
748
812
  agentAllowForcePush?: boolean | undefined;
749
813
  agentAllowDestructiveGit?: boolean | undefined;
814
+ agentAllowOutsideFolder?: boolean | undefined;
750
815
  };
751
816
  tickets: {
752
817
  number: number;
@@ -759,6 +824,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
759
824
  } | undefined;
760
825
  workMode?: "DIRECT" | "BRANCH" | undefined;
761
826
  pausedUntil?: string | null | undefined;
827
+ backgroundTasks?: number | undefined;
762
828
  agentLatestVersion?: string | undefined;
763
829
  skipAgentPrompt?: boolean | undefined;
764
830
  branchHint?: string | undefined;
@@ -794,6 +860,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
794
860
  agentProtectedBranches?: unknown;
795
861
  agentAllowForcePush?: unknown;
796
862
  agentAllowDestructiveGit?: unknown;
863
+ agentAllowOutsideFolder?: unknown;
797
864
  };
798
865
  tickets: {
799
866
  number: number;
@@ -814,6 +881,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
814
881
  activeMsBase?: number | undefined;
815
882
  extraBudgetMinutes?: number | null | undefined;
816
883
  pausedUntil?: unknown;
884
+ backgroundTasks?: unknown;
817
885
  agentLatestVersion?: unknown;
818
886
  skipAgentPrompt?: unknown;
819
887
  branchHint?: unknown;
@@ -883,6 +951,20 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
883
951
  * behaviour every runner had before this release.
884
952
  */
885
953
  pausedUntil: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
954
+ /**
955
+ * Subagents the API still believes to be alive in this session (#393).
956
+ *
957
+ * The number is the dead process's last word, kept by the API: a runner
958
+ * restart takes every subagent with it, and the new process seeds its own
959
+ * count at zero — so the descriptor is the only place the number survives
960
+ * long enough to be written down. The API sends it only while it still
961
+ * believes it (its own trust window on the count's age); absent or zero
962
+ * means «nothing to say», and a restore says nothing.
963
+ *
964
+ * `.catch(undefined)` like its neighbours: one malformed value must cost
965
+ * its own session a line at most, never the frame (QA-100 MAJOR-1).
966
+ */
967
+ backgroundTasks: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
886
968
  /**
887
969
  * The newest published version of THIS session's agent — Р13, §4.8.
888
970
  *
@@ -950,6 +1032,18 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
950
1032
  agentProtectedBranches: z.ZodCatch<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
951
1033
  agentAllowForcePush: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
952
1034
  agentAllowDestructiveGit: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
1035
+ /**
1036
+ * #418: may the agent read and write OUTSIDE the project folder?
1037
+ *
1038
+ * `.optional().catch(undefined)` for the QA-100 MAJOR-1 reason above, and
1039
+ * safe here for the same reason as `agentPushBan` — though the polarity
1040
+ * runs the other way. `policy.ts` resolves this one with `=== true`, so a
1041
+ * value this schema throws away leaves the session confined to its folder:
1042
+ * strictly more restricted, never less. Rubbish in this field therefore
1043
+ * costs a project its permission, which is the direction a dropped value is
1044
+ * allowed to fail in.
1045
+ */
1046
+ agentAllowOutsideFolder: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
953
1047
  budgetUsd: z.ZodNullable<z.ZodNumber>;
954
1048
  budgetMinutes: z.ZodNullable<z.ZodNumber>;
955
1049
  }, "strip", z.ZodTypeAny, {
@@ -965,6 +1059,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
965
1059
  agentProtectedBranches?: string[] | undefined;
966
1060
  agentAllowForcePush?: boolean | undefined;
967
1061
  agentAllowDestructiveGit?: boolean | undefined;
1062
+ agentAllowOutsideFolder?: boolean | undefined;
968
1063
  }, {
969
1064
  path: string;
970
1065
  id: string;
@@ -978,6 +1073,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
978
1073
  agentProtectedBranches?: unknown;
979
1074
  agentAllowForcePush?: unknown;
980
1075
  agentAllowDestructiveGit?: unknown;
1076
+ agentAllowOutsideFolder?: unknown;
981
1077
  }>;
982
1078
  /**
983
1079
  * «Run this one without the project's agent prompt.» Absent means no — both
@@ -1085,6 +1181,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1085
1181
  agentProtectedBranches?: string[] | undefined;
1086
1182
  agentAllowForcePush?: boolean | undefined;
1087
1183
  agentAllowDestructiveGit?: boolean | undefined;
1184
+ agentAllowOutsideFolder?: boolean | undefined;
1088
1185
  };
1089
1186
  tickets: {
1090
1187
  number: number;
@@ -1097,6 +1194,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1097
1194
  } | undefined;
1098
1195
  workMode?: "DIRECT" | "BRANCH" | undefined;
1099
1196
  pausedUntil?: string | null | undefined;
1197
+ backgroundTasks?: number | undefined;
1100
1198
  agentLatestVersion?: string | undefined;
1101
1199
  skipAgentPrompt?: boolean | undefined;
1102
1200
  branchHint?: string | undefined;
@@ -1126,6 +1224,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1126
1224
  agentProtectedBranches?: unknown;
1127
1225
  agentAllowForcePush?: unknown;
1128
1226
  agentAllowDestructiveGit?: unknown;
1227
+ agentAllowOutsideFolder?: unknown;
1129
1228
  };
1130
1229
  tickets: {
1131
1230
  number: number;
@@ -1146,6 +1245,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1146
1245
  activeMsBase?: number | undefined;
1147
1246
  extraBudgetMinutes?: number | null | undefined;
1148
1247
  pausedUntil?: unknown;
1248
+ backgroundTasks?: unknown;
1149
1249
  agentLatestVersion?: unknown;
1150
1250
  skipAgentPrompt?: unknown;
1151
1251
  branchHint?: unknown;
@@ -1180,6 +1280,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1180
1280
  agentProtectedBranches?: string[] | undefined;
1181
1281
  agentAllowForcePush?: boolean | undefined;
1182
1282
  agentAllowDestructiveGit?: boolean | undefined;
1283
+ agentAllowOutsideFolder?: boolean | undefined;
1183
1284
  };
1184
1285
  tickets: {
1185
1286
  number: number;
@@ -1192,6 +1293,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1192
1293
  } | undefined;
1193
1294
  workMode?: "DIRECT" | "BRANCH" | undefined;
1194
1295
  pausedUntil?: string | null | undefined;
1296
+ backgroundTasks?: number | undefined;
1195
1297
  agentLatestVersion?: string | undefined;
1196
1298
  skipAgentPrompt?: boolean | undefined;
1197
1299
  branchHint?: string | undefined;
@@ -1224,6 +1326,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1224
1326
  agentProtectedBranches?: unknown;
1225
1327
  agentAllowForcePush?: unknown;
1226
1328
  agentAllowDestructiveGit?: unknown;
1329
+ agentAllowOutsideFolder?: unknown;
1227
1330
  };
1228
1331
  tickets: {
1229
1332
  number: number;
@@ -1244,6 +1347,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1244
1347
  activeMsBase?: number | undefined;
1245
1348
  extraBudgetMinutes?: number | null | undefined;
1246
1349
  pausedUntil?: unknown;
1350
+ backgroundTasks?: unknown;
1247
1351
  agentLatestVersion?: unknown;
1248
1352
  skipAgentPrompt?: unknown;
1249
1353
  branchHint?: unknown;
@@ -1405,6 +1509,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1405
1509
  agentProtectedBranches: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1406
1510
  agentAllowForcePush: z.ZodOptional<z.ZodBoolean>;
1407
1511
  agentAllowDestructiveGit: z.ZodOptional<z.ZodBoolean>;
1512
+ agentAllowOutsideFolder: z.ZodOptional<z.ZodBoolean>;
1408
1513
  }, "strip", z.ZodTypeAny, {
1409
1514
  type: "workspace_settings";
1410
1515
  workspaceId: string;
@@ -1414,6 +1519,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1414
1519
  agentProtectedBranches?: string[] | undefined;
1415
1520
  agentAllowForcePush?: boolean | undefined;
1416
1521
  agentAllowDestructiveGit?: boolean | undefined;
1522
+ agentAllowOutsideFolder?: boolean | undefined;
1417
1523
  }, {
1418
1524
  type: "workspace_settings";
1419
1525
  workspaceId: string;
@@ -1423,6 +1529,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
1423
1529
  agentProtectedBranches?: string[] | undefined;
1424
1530
  agentAllowForcePush?: boolean | undefined;
1425
1531
  agentAllowDestructiveGit?: boolean | undefined;
1532
+ agentAllowOutsideFolder?: boolean | undefined;
1426
1533
  }>, z.ZodObject<{
1427
1534
  type: z.ZodLiteral<"session_settings">;
1428
1535
  sessionId: z.ZodString;
package/dist/protocol.js CHANGED
@@ -59,6 +59,20 @@ export const SessionDescriptorSchema = z.object({
59
59
  * behaviour every runner had before this release.
60
60
  */
61
61
  pausedUntil: z.string().max(40).nullable().optional().catch(undefined),
62
+ /**
63
+ * Subagents the API still believes to be alive in this session (#393).
64
+ *
65
+ * The number is the dead process's last word, kept by the API: a runner
66
+ * restart takes every subagent with it, and the new process seeds its own
67
+ * count at zero — so the descriptor is the only place the number survives
68
+ * long enough to be written down. The API sends it only while it still
69
+ * believes it (its own trust window on the count's age); absent or zero
70
+ * means «nothing to say», and a restore says nothing.
71
+ *
72
+ * `.catch(undefined)` like its neighbours: one malformed value must cost
73
+ * its own session a line at most, never the frame (QA-100 MAJOR-1).
74
+ */
75
+ backgroundTasks: z.number().int().min(0).optional().catch(undefined),
62
76
  /**
63
77
  * The newest published version of THIS session's agent — Р13, §4.8.
64
78
  *
@@ -137,6 +151,18 @@ export const SessionDescriptorSchema = z.object({
137
151
  .catch(undefined),
138
152
  agentAllowForcePush: z.boolean().optional().catch(undefined),
139
153
  agentAllowDestructiveGit: z.boolean().optional().catch(undefined),
154
+ /**
155
+ * #418: may the agent read and write OUTSIDE the project folder?
156
+ *
157
+ * `.optional().catch(undefined)` for the QA-100 MAJOR-1 reason above, and
158
+ * safe here for the same reason as `agentPushBan` — though the polarity
159
+ * runs the other way. `policy.ts` resolves this one with `=== true`, so a
160
+ * value this schema throws away leaves the session confined to its folder:
161
+ * strictly more restricted, never less. Rubbish in this field therefore
162
+ * costs a project its permission, which is the direction a dropped value is
163
+ * allowed to fail in.
164
+ */
165
+ agentAllowOutsideFolder: z.boolean().optional().catch(undefined),
140
166
  budgetUsd: z.number().nullable(),
141
167
  budgetMinutes: z.number().nullable(),
142
168
  }),
@@ -377,6 +403,9 @@ export const GatewayFrameSchema = z.discriminatedUnion('type', [
377
403
  .optional(),
378
404
  agentAllowForcePush: z.boolean().optional(),
379
405
  agentAllowDestructiveGit: z.boolean().optional(),
406
+ // #418 — read on every file tool call, so it belongs in the live frame with
407
+ // its neighbours: switching it back off must not mean stopping the agent.
408
+ agentAllowOutsideFolder: z.boolean().optional(),
380
409
  }),
381
410
  z.object({
382
411
  type: z.literal('session_settings'),