@bridge4dev/runner 0.61.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/README.md +12 -6
- package/dist/adapters/claude.js +11 -6
- package/dist/adapters/codex.js +80 -15
- package/dist/adapters/types.d.ts +50 -0
- package/dist/adapters/types.js +59 -0
- package/dist/index.js +12 -0
- package/dist/policy.d.ts +27 -0
- package/dist/policy.js +38 -5
- package/dist/protocol.d.ts +55 -0
- package/dist/protocol.js +15 -0
- package/dist/supervisor.js +14 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,12 +49,18 @@ it. If you would rather run it in the foreground, or under your own supervisor,
|
|
|
49
49
|
|
|
50
50
|
## What it does on your machine
|
|
51
51
|
|
|
52
|
-
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
- Runs a session either **in the project folder itself** — the default: the branch it is
|
|
53
|
+
already on, nothing to apply afterwards — or in a **git worktree on a session branch of
|
|
54
|
+
its own**, which DevBridge asks for when the folder is busy or a branch was named. Which
|
|
55
|
+
one a session gets is decided per session, in the dashboard.
|
|
56
|
+
- Enforces a local policy layer. Part of it the dashboard **cannot** raise: secret files and
|
|
57
|
+
keys are never read, nothing is written into any repository's `.git`, and `sudo`, service
|
|
58
|
+
control, docker and the firewall are refused in every trust level. Part of it is the bound
|
|
59
|
+
project's own setting, chosen by a manager in DevBridge: whether the agent may `git push`
|
|
60
|
+
and to which branches, and whether it may read and change files **outside the project
|
|
61
|
+
folder** — other projects on the same machine included. Out of the box the agent pushes
|
|
62
|
+
nothing and stays in its folder. Plus secret masking in everything it streams, and its own
|
|
63
|
+
ceiling on how many sessions may run at once.
|
|
58
64
|
- Streams the session journal with sequence numbers and acknowledgements, so a restart of
|
|
59
65
|
either side resumes instead of losing work.
|
|
60
66
|
- Never uploads your source. Diffs and file views are requested per file, and files
|
package/dist/adapters/claude.js
CHANGED
|
@@ -8,7 +8,7 @@ import { mcpConfigPath } from '../paths.js';
|
|
|
8
8
|
import { lowerPriority } from '../process-priority.js';
|
|
9
9
|
import { cageSpawn, noteSessionAgentPid, memoryDeathSentence, releaseSessionScope, sessionMemoryEnv, sessionMemoryPromptLine, } from '../session-cage.js';
|
|
10
10
|
import { evaluateToolUse, maskSecrets, maskString, } from '../policy.js';
|
|
11
|
-
import { availableModes, cardDescription, DIRECT_BRANCH_RULE, MODE_REFUSED_TEXT, MODE_WITHDRAWN_TEXT, policyContextFor, DEVBRIDGE_MCP_SERVER_NAME, } from './types.js';
|
|
11
|
+
import { availableModes, cardDescription, DIRECT_BRANCH_RULE, folderRuleFor, MODE_REFUSED_TEXT, MODE_WITHDRAWN_TEXT, policyContextFor, DEVBRIDGE_MCP_SERVER_NAME, } from './types.js';
|
|
12
12
|
import { percentFromUtilization, RATE_WINDOW_MINUTES, rateWindowKey } from './rate-limits.js';
|
|
13
13
|
import { applyUsagePercentages, lastUsageRows, readUsageRows } from './claude-usage.js';
|
|
14
14
|
import { assertClaudeInstalled, claudeExecutableOption, sessionClaudePath, } from '../agent-binary.js';
|
|
@@ -166,14 +166,19 @@ const MODE_TO_PERMISSION = {
|
|
|
166
166
|
* no line: the agent plans around a restriction that is not there, or walks
|
|
167
167
|
* into one it was told did not exist.
|
|
168
168
|
*/
|
|
169
|
-
function systemAppendFor(spec) {
|
|
169
|
+
function systemAppendFor(spec, mode) {
|
|
170
170
|
const pushBanned = spec.gitPolicy?.agentPushBan !== false;
|
|
171
|
+
// #418: which folder line this session gets, if any — see `folderRuleFor`.
|
|
172
|
+
const folderRule = folderRuleFor(spec, mode);
|
|
171
173
|
const guarded = spec.gitPolicy?.agentProtectedBranches ?? ['main', 'master'];
|
|
172
174
|
const memoryLine = sessionMemoryPromptLine(spec.sessionId);
|
|
173
175
|
return [
|
|
174
176
|
'You are running inside a DevBridge dev session, controlled from the DevBridge dashboard.',
|
|
175
177
|
'Rules:',
|
|
176
|
-
|
|
178
|
+
// #418. The three cases live in `folderRuleFor` — one copy for both
|
|
179
|
+
// adapters, and the Codex adapter asks the same function whether a mode
|
|
180
|
+
// switch would change what the agent was told.
|
|
181
|
+
...(folderRule === null ? [] : [folderRule]),
|
|
177
182
|
// Session 13: pushing was refused outright by layer 1 in every trust mode,
|
|
178
183
|
// and saying so here saved the agent a turn spent discovering it. Session
|
|
179
184
|
// 18 makes it the project's decision — so the sentence has to follow the
|
|
@@ -203,8 +208,8 @@ function systemAppendFor(spec) {
|
|
|
203
208
|
].join('\n');
|
|
204
209
|
}
|
|
205
210
|
/** DevBridge's own rules, then whatever this workspace adds (session 13). */
|
|
206
|
-
function composeSystemAppend(spec) {
|
|
207
|
-
const base = systemAppendFor(spec);
|
|
211
|
+
function composeSystemAppend(spec, mode) {
|
|
212
|
+
const base = systemAppendFor(spec, mode);
|
|
208
213
|
return spec.workspaceContext ? `${base}\n\n${spec.workspaceContext}` : base;
|
|
209
214
|
}
|
|
210
215
|
/**
|
|
@@ -585,7 +590,7 @@ class ClaudeSession {
|
|
|
585
590
|
systemPrompt: {
|
|
586
591
|
type: 'preset',
|
|
587
592
|
preset: 'claude_code',
|
|
588
|
-
append: composeSystemAppend(spec),
|
|
593
|
+
append: composeSystemAppend(spec, this.mode),
|
|
589
594
|
},
|
|
590
595
|
canUseTool: (toolName, input, opts) => this.onCanUseTool(toolName, input, opts),
|
|
591
596
|
// Ticket #113: a running subagent forks its own conversation every ~30s
|
package/dist/adapters/codex.js
CHANGED
|
@@ -6,7 +6,7 @@ import { RUNNER_VERSION } from '../version.js';
|
|
|
6
6
|
import { repairCodexAuth } from './codex-home.js';
|
|
7
7
|
import { AppServerClient, asRecord, num, RpcError, RpcTimeoutError, str, } from './codex-protocol.js';
|
|
8
8
|
import { truncate } from './claude.js';
|
|
9
|
-
import { availableModes, cardDescription, DIRECT_BRANCH_RULE, MODE_REFUSED_TEXT, MODE_WITHDRAWN_TEXT, policyContextFor, DEVBRIDGE_MCP_SERVER_NAME, } from './types.js';
|
|
9
|
+
import { availableModes, cardDescription, DIRECT_BRANCH_RULE, folderRuleFor, MODE_REFUSED_TEXT, MODE_WITHDRAWN_TEXT, policyContextFor, DEVBRIDGE_MCP_SERVER_NAME, } from './types.js';
|
|
10
10
|
import { clampPercent, rateWindowKeyFromMinutes } from './rate-limits.js';
|
|
11
11
|
import { answerSummary, invalidationMessage, mirrorOptions, newAskId, MAX_OPTIONS, MAX_QUESTIONS, OPTION_TEXT_LIMIT, QUESTION_TEXT_LIMIT, } from './questions.js';
|
|
12
12
|
// Codex adapter over `codex app-server` (stage C). The normalized AgentEvent
|
|
@@ -60,6 +60,37 @@ const MODE_POLICY = {
|
|
|
60
60
|
// exactly like Claude's bypassPermissions. The dashboard says so.
|
|
61
61
|
full: { approvalPolicy: 'never', sandbox: 'danger-full-access', plan: false },
|
|
62
62
|
};
|
|
63
|
+
/**
|
|
64
|
+
* Where a `workspace-write` turn may write (#418).
|
|
65
|
+
*
|
|
66
|
+
* Codex confines writes in the KERNEL, not in our policy: under `auto` the
|
|
67
|
+
* sandbox is `workspace-write` with `writableRoots: [cwd]`, so a write outside
|
|
68
|
+
* fails inside the CLI, comes back as a «retry without sandbox?» approval, and
|
|
69
|
+
* only then reaches layer 1. With the project's permission on, that round trip
|
|
70
|
+
* is exactly the refusal the ticket exists to remove — so the ROOT is widened.
|
|
71
|
+
*
|
|
72
|
+
* **The sandbox is widened, never dropped**, and that is the whole safety of
|
|
73
|
+
* this function. `danger-full-access` looks like the simpler answer and is the
|
|
74
|
+
* wrong one: for Codex the kernel sandbox refusing something IS the only
|
|
75
|
+
* channel that reaches `evaluateToolUse` (the adapter calls it from
|
|
76
|
+
* `onApproval` and nowhere else). Remove the sandbox and nothing ever
|
|
77
|
+
* escalates, so `sudo`, docker, the firewall, secret paths, any repository's
|
|
78
|
+
* `.git`, the project's prompt file and the project's own git policy would all
|
|
79
|
+
* stop being enforced for Codex — the very list the setting promises to keep.
|
|
80
|
+
* Keeping `workspaceWrite` keeps that channel, and keeps `networkAccess: false`
|
|
81
|
+
* with it, so the permission moves one boundary and no other.
|
|
82
|
+
*
|
|
83
|
+
* **STRICT does not widen.** There a step outside must still raise a card, and
|
|
84
|
+
* the only way to raise one for Codex is to let the sandbox refuse it first.
|
|
85
|
+
*
|
|
86
|
+
* Read on every turn rather than captured at launch, because the setting can be
|
|
87
|
+
* switched while the session runs.
|
|
88
|
+
*/
|
|
89
|
+
function writableRootsFor(spec) {
|
|
90
|
+
if (spec.trustMode === 'STRICT')
|
|
91
|
+
return [spec.cwd];
|
|
92
|
+
return spec.gitPolicy?.agentAllowOutsideFolder === true ? ['/'] : [spec.cwd];
|
|
93
|
+
}
|
|
63
94
|
/**
|
|
64
95
|
* DevBridge's own rules — composed per session since session 18, and kept
|
|
65
96
|
* deliberately in step with `systemAppendFor` in the Claude adapter.
|
|
@@ -69,14 +100,19 @@ const MODE_POLICY = {
|
|
|
69
100
|
* `workMode: DIRECT` (the default since session 16), and the push sentence is
|
|
70
101
|
* only true when the project has «Принудительно запретить push» switched on.
|
|
71
102
|
*/
|
|
72
|
-
function systemAppendFor(spec) {
|
|
103
|
+
function systemAppendFor(spec, mode) {
|
|
73
104
|
const pushBanned = spec.gitPolicy?.agentPushBan !== false;
|
|
105
|
+
// #418: which folder line this session gets, if any — see `folderRuleFor`.
|
|
106
|
+
const folderRule = folderRuleFor(spec, mode);
|
|
74
107
|
const guarded = spec.gitPolicy?.agentProtectedBranches ?? ['main', 'master'];
|
|
75
108
|
const memoryLine = sessionMemoryPromptLine(spec.sessionId);
|
|
76
109
|
return [
|
|
77
110
|
'You are running inside a DevBridge dev session, controlled from the DevBridge dashboard.',
|
|
78
111
|
'Rules:',
|
|
79
|
-
|
|
112
|
+
// #418. The three cases live in `folderRuleFor` — one copy for both
|
|
113
|
+
// adapters, and the Codex adapter asks the same function whether a mode
|
|
114
|
+
// switch would change what the agent was told.
|
|
115
|
+
...(folderRule === null ? [] : [folderRule]),
|
|
80
116
|
pushBanned
|
|
81
117
|
? '- Commit your work in the current branch with clear messages. You cannot push: `git push` is blocked for this project. A human presses «Push» and «Apply» in DevBridge when the branch is ready.'
|
|
82
118
|
: guarded.length > 0
|
|
@@ -96,8 +132,8 @@ function systemAppendFor(spec) {
|
|
|
96
132
|
].join('\n');
|
|
97
133
|
}
|
|
98
134
|
/** DevBridge's own rules, then whatever this workspace adds (session 13). */
|
|
99
|
-
function composeSystemAppend(spec) {
|
|
100
|
-
const base = systemAppendFor(spec);
|
|
135
|
+
function composeSystemAppend(spec, mode) {
|
|
136
|
+
const base = systemAppendFor(spec, mode);
|
|
101
137
|
return spec.workspaceContext ? `${base}\n\n${spec.workspaceContext}` : base;
|
|
102
138
|
}
|
|
103
139
|
// Allowlist, not denylist: whatever secrets live in the daemon's environment
|
|
@@ -534,8 +570,14 @@ class CodexSession {
|
|
|
534
570
|
return {
|
|
535
571
|
cwd: this.spec.cwd,
|
|
536
572
|
approvalPolicy: policy.approvalPolicy,
|
|
573
|
+
// #418 does NOT change this one, and that is deliberate. The thread-level
|
|
574
|
+
// channel takes the CLI-style MODE string, which has no way to say «this
|
|
575
|
+
// root»; the mode itself is unchanged (`workspace-write` stays
|
|
576
|
+
// `workspace-write`), and the roots travel with every turn below. Both
|
|
577
|
+
// channels therefore still describe the same sandbox, which is what R10
|
|
578
|
+
// of the plan asks for — there is no second shape to keep in step.
|
|
537
579
|
sandbox: policy.sandbox,
|
|
538
|
-
developerInstructions: composeSystemAppend(this.spec),
|
|
580
|
+
developerInstructions: composeSystemAppend(this.spec, this.mode),
|
|
539
581
|
...(this.model ? { model: this.model } : {}),
|
|
540
582
|
...(this.spec.mcp ? { config: this.mcpOverlay() } : {}),
|
|
541
583
|
};
|
|
@@ -582,7 +624,8 @@ class CodexSession {
|
|
|
582
624
|
// [] is accepted, null is rejected outright.
|
|
583
625
|
input: [{ type: 'text', text, text_elements: [] }],
|
|
584
626
|
approvalPolicy: policy.approvalPolicy,
|
|
585
|
-
|
|
627
|
+
// #418: the roots live here and only here — see `writableRootsFor`.
|
|
628
|
+
sandboxPolicy: sandboxPolicyFor(policy.sandbox, this.spec),
|
|
586
629
|
...(this.model ? { model: this.model } : {}),
|
|
587
630
|
// "Override the reasoning effort for this turn and subsequent turns" —
|
|
588
631
|
// the same sticky-override channel the model uses (there is still no
|
|
@@ -793,12 +836,29 @@ class CodexSession {
|
|
|
793
836
|
this.refreshCapabilities();
|
|
794
837
|
}
|
|
795
838
|
/**
|
|
796
|
-
* Codex needs no new process for
|
|
797
|
-
*
|
|
798
|
-
*
|
|
839
|
+
* Codex needs no new process for its POLICY: `approvalPolicy` and the sandbox
|
|
840
|
+
* travel with the next `turn/start`, so the change is in force from the next
|
|
841
|
+
* turn whatever it is (ticket #156).
|
|
842
|
+
*
|
|
843
|
+
* Its RULES are the other half, and they cannot follow (#418). DevBridge's
|
|
844
|
+
* own instructions reach Codex ONLY as the thread's `developerInstructions`,
|
|
845
|
+
* fixed when the thread opens: this protocol version has no `settings/update`,
|
|
846
|
+
* and putting the text on the turn instead is the duplicated-prompt defect of
|
|
847
|
+
* ticket #179. So a line that has to DISAPPEAR in «Unrestricted» — where
|
|
848
|
+
* layer 1 is never consulted and the sentence would be the only thing left
|
|
849
|
+
* enforcing it — can only disappear with a new thread.
|
|
850
|
+
*
|
|
851
|
+
* Asked precisely rather than by hard-coding the `full` boundary: would the
|
|
852
|
+
* folder line actually change? With «may work outside the project folder» ON
|
|
853
|
+
* the answer is never — the sentence is then the same in every mode — so
|
|
854
|
+
* those projects keep Codex's free, instant mode switching. Only a session
|
|
855
|
+
* that would be told something different pays for a new process, and it pays
|
|
856
|
+
* exactly what a Claude session has always paid for the same move.
|
|
799
857
|
*/
|
|
800
|
-
modeSwitchNeedsRelaunch(
|
|
801
|
-
|
|
858
|
+
modeSwitchNeedsRelaunch(mode) {
|
|
859
|
+
if (!availableModes(this.spec.trustMode).includes(mode))
|
|
860
|
+
return false;
|
|
861
|
+
return folderRuleFor(this.spec, mode) !== folderRuleFor(this.spec, this.mode);
|
|
802
862
|
}
|
|
803
863
|
async setMode(mode) {
|
|
804
864
|
if (!availableModes(this.spec.trustMode).includes(mode)) {
|
|
@@ -2255,9 +2315,10 @@ class CodexSession {
|
|
|
2255
2315
|
}
|
|
2256
2316
|
class ResumeFailed extends Error {
|
|
2257
2317
|
}
|
|
2258
|
-
function sandboxPolicyFor(mode,
|
|
2318
|
+
function sandboxPolicyFor(mode, spec) {
|
|
2259
2319
|
// turn/start takes the structured SandboxPolicy, while thread/start takes the
|
|
2260
|
-
// CLI-style SandboxMode string. Same intent, two shapes
|
|
2320
|
+
// CLI-style SandboxMode string. Same intent, two shapes — and only this one
|
|
2321
|
+
// can name the writable roots, which is why #418 lives here.
|
|
2261
2322
|
switch (mode) {
|
|
2262
2323
|
case 'read-only':
|
|
2263
2324
|
return { type: 'readOnly', networkAccess: false };
|
|
@@ -2266,7 +2327,11 @@ function sandboxPolicyFor(mode, cwd) {
|
|
|
2266
2327
|
default:
|
|
2267
2328
|
return {
|
|
2268
2329
|
type: 'workspaceWrite',
|
|
2269
|
-
writableRoots:
|
|
2330
|
+
writableRoots: writableRootsFor(spec),
|
|
2331
|
+
// Unchanged by #418, on purpose: the permission is about the file
|
|
2332
|
+
// system, and letting the network out with it would be a second
|
|
2333
|
+
// boundary nobody asked to move. A network command still fails in the
|
|
2334
|
+
// sandbox, still escalates, and still ends at layer 1.
|
|
2270
2335
|
networkAccess: false,
|
|
2271
2336
|
excludeTmpdirEnvVar: false,
|
|
2272
2337
|
excludeSlashTmp: false,
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -322,6 +322,56 @@ export declare function cardDescription(verdict: PolicyDecision, own: string | u
|
|
|
322
322
|
* into a card.
|
|
323
323
|
*/
|
|
324
324
|
export declare const DIRECT_BRANCH_RULE = "- This session works directly in the project folder, which other sessions and people share. Stay on the current branch: do not `git checkout <branch>` or `git switch` here. To put a file back use `git checkout -- <path>` or `git restore <path>`.";
|
|
325
|
+
/**
|
|
326
|
+
* «Stay in this folder» — the line layer 1 used to back up (#418).
|
|
327
|
+
*
|
|
328
|
+
* Here, beside `DIRECT_BRANCH_RULE` and for the same reason: the two
|
|
329
|
+
* `systemAppendFor` texts are hand-synced copies of each other, and a rule that
|
|
330
|
+
* has to appear in one of them and not the other is the kind of thing that
|
|
331
|
+
* drifts silently. Exported so the tests can name the exact string rather than
|
|
332
|
+
* a substring of it.
|
|
333
|
+
*
|
|
334
|
+
* NOT written when the session is «Unrestricted» — there the CLI never asks
|
|
335
|
+
* layer 1 anything, so this sentence was the only thing left enforcing it, and
|
|
336
|
+
* a rule with nothing underneath is a rule the agent refuses itself by — nor
|
|
337
|
+
* when the project has switched «may work outside the project folder» on, where
|
|
338
|
+
* `outsideFolderRule` takes its place.
|
|
339
|
+
*/
|
|
340
|
+
export declare const WORKING_DIRECTORY_RULE = "- Work ONLY inside the current working directory.";
|
|
341
|
+
/**
|
|
342
|
+
* What replaces the line above when the project allows work outside the folder
|
|
343
|
+
* (#418).
|
|
344
|
+
*
|
|
345
|
+
* It says three things and no more: where the agent is, that it may go
|
|
346
|
+
* elsewhere when the task needs it, and what it gives up by doing so — nothing
|
|
347
|
+
* outside the folder reaches «Changes», «Apply» or a restore point. That last
|
|
348
|
+
* half is the part the agent cannot discover for itself and the part a person
|
|
349
|
+
* would otherwise find out from a diff that is missing a file.
|
|
350
|
+
*/
|
|
351
|
+
export declare function outsideFolderRule(cwd: string): string;
|
|
352
|
+
/**
|
|
353
|
+
* Which of the two lines above this session gets, or neither (#418).
|
|
354
|
+
*
|
|
355
|
+
* One function rather than the same three-way choice written out in both
|
|
356
|
+
* `systemAppendFor` texts — and it has a second caller that made it worth
|
|
357
|
+
* extracting: the Codex adapter asks it whether a MODE SWITCH would change what
|
|
358
|
+
* the agent was told, because Codex's developer instructions are fixed when the
|
|
359
|
+
* thread opens and cannot be edited afterwards.
|
|
360
|
+
*
|
|
361
|
+
* The three cases, in order:
|
|
362
|
+
* - the project allows work outside the folder → say where the folder is and
|
|
363
|
+
* what leaving it costs, in EVERY mode (the permission does not depend on
|
|
364
|
+
* the mode, so neither does the sentence);
|
|
365
|
+
* - «Unrestricted» → `null`, nothing at all. Layer 1 is never consulted there,
|
|
366
|
+
* so this line would be the only thing enforcing the rule: the agent refuses
|
|
367
|
+
* itself by a sentence nothing backs up, which is the complaint #418 came
|
|
368
|
+
* from;
|
|
369
|
+
* - otherwise → the rule as it has always been, with layer 1 behind it.
|
|
370
|
+
*
|
|
371
|
+
* The mode passed in must be the one the session is actually IN, not the one it
|
|
372
|
+
* was asked for: a `full` refused on a STRICT workspace keeps the line.
|
|
373
|
+
*/
|
|
374
|
+
export declare function folderRuleFor(spec: SessionSpec, mode: AgentMode | undefined): string | null;
|
|
325
375
|
/**
|
|
326
376
|
* One question inside an agent's question call (session 12).
|
|
327
377
|
*
|
package/dist/adapters/types.js
CHANGED
|
@@ -100,6 +100,65 @@ export function cardDescription(verdict, own) {
|
|
|
100
100
|
* into a card.
|
|
101
101
|
*/
|
|
102
102
|
export const DIRECT_BRANCH_RULE = '- This session works directly in the project folder, which other sessions and people share. Stay on the current branch: do not `git checkout <branch>` or `git switch` here. To put a file back use `git checkout -- <path>` or `git restore <path>`.';
|
|
103
|
+
/**
|
|
104
|
+
* «Stay in this folder» — the line layer 1 used to back up (#418).
|
|
105
|
+
*
|
|
106
|
+
* Here, beside `DIRECT_BRANCH_RULE` and for the same reason: the two
|
|
107
|
+
* `systemAppendFor` texts are hand-synced copies of each other, and a rule that
|
|
108
|
+
* has to appear in one of them and not the other is the kind of thing that
|
|
109
|
+
* drifts silently. Exported so the tests can name the exact string rather than
|
|
110
|
+
* a substring of it.
|
|
111
|
+
*
|
|
112
|
+
* NOT written when the session is «Unrestricted» — there the CLI never asks
|
|
113
|
+
* layer 1 anything, so this sentence was the only thing left enforcing it, and
|
|
114
|
+
* a rule with nothing underneath is a rule the agent refuses itself by — nor
|
|
115
|
+
* when the project has switched «may work outside the project folder» on, where
|
|
116
|
+
* `outsideFolderRule` takes its place.
|
|
117
|
+
*/
|
|
118
|
+
export const WORKING_DIRECTORY_RULE = '- Work ONLY inside the current working directory.';
|
|
119
|
+
/**
|
|
120
|
+
* What replaces the line above when the project allows work outside the folder
|
|
121
|
+
* (#418).
|
|
122
|
+
*
|
|
123
|
+
* It says three things and no more: where the agent is, that it may go
|
|
124
|
+
* elsewhere when the task needs it, and what it gives up by doing so — nothing
|
|
125
|
+
* outside the folder reaches «Changes», «Apply» or a restore point. That last
|
|
126
|
+
* half is the part the agent cannot discover for itself and the part a person
|
|
127
|
+
* would otherwise find out from a diff that is missing a file.
|
|
128
|
+
*/
|
|
129
|
+
export function outsideFolderRule(cwd) {
|
|
130
|
+
return (`- Your working directory is ${cwd}; you may read and change files elsewhere on this server ` +
|
|
131
|
+
`when the task needs it. Only what is inside ${cwd} shows up in the DevBridge diff and restore points.`);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Which of the two lines above this session gets, or neither (#418).
|
|
135
|
+
*
|
|
136
|
+
* One function rather than the same three-way choice written out in both
|
|
137
|
+
* `systemAppendFor` texts — and it has a second caller that made it worth
|
|
138
|
+
* extracting: the Codex adapter asks it whether a MODE SWITCH would change what
|
|
139
|
+
* the agent was told, because Codex's developer instructions are fixed when the
|
|
140
|
+
* thread opens and cannot be edited afterwards.
|
|
141
|
+
*
|
|
142
|
+
* The three cases, in order:
|
|
143
|
+
* - the project allows work outside the folder → say where the folder is and
|
|
144
|
+
* what leaving it costs, in EVERY mode (the permission does not depend on
|
|
145
|
+
* the mode, so neither does the sentence);
|
|
146
|
+
* - «Unrestricted» → `null`, nothing at all. Layer 1 is never consulted there,
|
|
147
|
+
* so this line would be the only thing enforcing the rule: the agent refuses
|
|
148
|
+
* itself by a sentence nothing backs up, which is the complaint #418 came
|
|
149
|
+
* from;
|
|
150
|
+
* - otherwise → the rule as it has always been, with layer 1 behind it.
|
|
151
|
+
*
|
|
152
|
+
* The mode passed in must be the one the session is actually IN, not the one it
|
|
153
|
+
* was asked for: a `full` refused on a STRICT workspace keeps the line.
|
|
154
|
+
*/
|
|
155
|
+
export function folderRuleFor(spec, mode) {
|
|
156
|
+
// `=== true` — silence keeps the agent in, exactly as `policy.ts` resolves the
|
|
157
|
+
// same field.
|
|
158
|
+
if (spec.gitPolicy?.agentAllowOutsideFolder === true)
|
|
159
|
+
return outsideFolderRule(spec.cwd);
|
|
160
|
+
return mode === 'full' ? null : WORKING_DIRECTORY_RULE;
|
|
161
|
+
}
|
|
103
162
|
/**
|
|
104
163
|
* The name our MCP server is registered under inside an agent session.
|
|
105
164
|
*
|
package/dist/index.js
CHANGED
|
@@ -381,6 +381,18 @@ function runnerCapabilities(apiUrlOverride) {
|
|
|
381
381
|
* been handed a worse problem.
|
|
382
382
|
*/
|
|
383
383
|
agentGitPolicy: true,
|
|
384
|
+
/**
|
|
385
|
+
* #418: «the agent may work outside the project folder» is a setting of the
|
|
386
|
+
* project, and this runner honours it.
|
|
387
|
+
*
|
|
388
|
+
* Its own flag rather than a second meaning for `agentGitPolicy`: the two
|
|
389
|
+
* ship a release apart, and a server on 0.61.x announces the git policy
|
|
390
|
+
* perfectly honestly while ignoring this field completely. Announced so the
|
|
391
|
+
* settings card can say so — warn, not refuse, for the same reason as the
|
|
392
|
+
* git policy: a manager must be able to record the decision before the last
|
|
393
|
+
* machine in the fleet is up to date.
|
|
394
|
+
*/
|
|
395
|
+
agentOutsideFolder: true,
|
|
384
396
|
/**
|
|
385
397
|
* Session 16: git is git.
|
|
386
398
|
*
|
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) &&
|
|
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 (
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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
|
}
|
package/dist/protocol.d.ts
CHANGED
|
@@ -113,6 +113,18 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
113
113
|
agentProtectedBranches: z.ZodCatch<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
|
|
114
114
|
agentAllowForcePush: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
|
|
115
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>>;
|
|
116
128
|
budgetUsd: z.ZodNullable<z.ZodNumber>;
|
|
117
129
|
budgetMinutes: z.ZodNullable<z.ZodNumber>;
|
|
118
130
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -128,6 +140,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
128
140
|
agentProtectedBranches?: string[] | undefined;
|
|
129
141
|
agentAllowForcePush?: boolean | undefined;
|
|
130
142
|
agentAllowDestructiveGit?: boolean | undefined;
|
|
143
|
+
agentAllowOutsideFolder?: boolean | undefined;
|
|
131
144
|
}, {
|
|
132
145
|
path: string;
|
|
133
146
|
id: string;
|
|
@@ -141,6 +154,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
141
154
|
agentProtectedBranches?: unknown;
|
|
142
155
|
agentAllowForcePush?: unknown;
|
|
143
156
|
agentAllowDestructiveGit?: unknown;
|
|
157
|
+
agentAllowOutsideFolder?: unknown;
|
|
144
158
|
}>;
|
|
145
159
|
/**
|
|
146
160
|
* «Run this one without the project's agent prompt.» Absent means no — both
|
|
@@ -248,6 +262,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
248
262
|
agentProtectedBranches?: string[] | undefined;
|
|
249
263
|
agentAllowForcePush?: boolean | undefined;
|
|
250
264
|
agentAllowDestructiveGit?: boolean | undefined;
|
|
265
|
+
agentAllowOutsideFolder?: boolean | undefined;
|
|
251
266
|
};
|
|
252
267
|
tickets: {
|
|
253
268
|
number: number;
|
|
@@ -290,6 +305,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
290
305
|
agentProtectedBranches?: unknown;
|
|
291
306
|
agentAllowForcePush?: unknown;
|
|
292
307
|
agentAllowDestructiveGit?: unknown;
|
|
308
|
+
agentAllowOutsideFolder?: unknown;
|
|
293
309
|
};
|
|
294
310
|
tickets: {
|
|
295
311
|
number: number;
|
|
@@ -547,6 +563,18 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
547
563
|
agentProtectedBranches: z.ZodCatch<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
|
|
548
564
|
agentAllowForcePush: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
|
|
549
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>>;
|
|
550
578
|
budgetUsd: z.ZodNullable<z.ZodNumber>;
|
|
551
579
|
budgetMinutes: z.ZodNullable<z.ZodNumber>;
|
|
552
580
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -562,6 +590,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
562
590
|
agentProtectedBranches?: string[] | undefined;
|
|
563
591
|
agentAllowForcePush?: boolean | undefined;
|
|
564
592
|
agentAllowDestructiveGit?: boolean | undefined;
|
|
593
|
+
agentAllowOutsideFolder?: boolean | undefined;
|
|
565
594
|
}, {
|
|
566
595
|
path: string;
|
|
567
596
|
id: string;
|
|
@@ -575,6 +604,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
575
604
|
agentProtectedBranches?: unknown;
|
|
576
605
|
agentAllowForcePush?: unknown;
|
|
577
606
|
agentAllowDestructiveGit?: unknown;
|
|
607
|
+
agentAllowOutsideFolder?: unknown;
|
|
578
608
|
}>;
|
|
579
609
|
/**
|
|
580
610
|
* «Run this one without the project's agent prompt.» Absent means no — both
|
|
@@ -682,6 +712,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
682
712
|
agentProtectedBranches?: string[] | undefined;
|
|
683
713
|
agentAllowForcePush?: boolean | undefined;
|
|
684
714
|
agentAllowDestructiveGit?: boolean | undefined;
|
|
715
|
+
agentAllowOutsideFolder?: boolean | undefined;
|
|
685
716
|
};
|
|
686
717
|
tickets: {
|
|
687
718
|
number: number;
|
|
@@ -724,6 +755,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
724
755
|
agentProtectedBranches?: unknown;
|
|
725
756
|
agentAllowForcePush?: unknown;
|
|
726
757
|
agentAllowDestructiveGit?: unknown;
|
|
758
|
+
agentAllowOutsideFolder?: unknown;
|
|
727
759
|
};
|
|
728
760
|
tickets: {
|
|
729
761
|
number: number;
|
|
@@ -779,6 +811,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
779
811
|
agentProtectedBranches?: string[] | undefined;
|
|
780
812
|
agentAllowForcePush?: boolean | undefined;
|
|
781
813
|
agentAllowDestructiveGit?: boolean | undefined;
|
|
814
|
+
agentAllowOutsideFolder?: boolean | undefined;
|
|
782
815
|
};
|
|
783
816
|
tickets: {
|
|
784
817
|
number: number;
|
|
@@ -827,6 +860,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
827
860
|
agentProtectedBranches?: unknown;
|
|
828
861
|
agentAllowForcePush?: unknown;
|
|
829
862
|
agentAllowDestructiveGit?: unknown;
|
|
863
|
+
agentAllowOutsideFolder?: unknown;
|
|
830
864
|
};
|
|
831
865
|
tickets: {
|
|
832
866
|
number: number;
|
|
@@ -998,6 +1032,18 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
998
1032
|
agentProtectedBranches: z.ZodCatch<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
|
|
999
1033
|
agentAllowForcePush: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
|
|
1000
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>>;
|
|
1001
1047
|
budgetUsd: z.ZodNullable<z.ZodNumber>;
|
|
1002
1048
|
budgetMinutes: z.ZodNullable<z.ZodNumber>;
|
|
1003
1049
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -1013,6 +1059,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1013
1059
|
agentProtectedBranches?: string[] | undefined;
|
|
1014
1060
|
agentAllowForcePush?: boolean | undefined;
|
|
1015
1061
|
agentAllowDestructiveGit?: boolean | undefined;
|
|
1062
|
+
agentAllowOutsideFolder?: boolean | undefined;
|
|
1016
1063
|
}, {
|
|
1017
1064
|
path: string;
|
|
1018
1065
|
id: string;
|
|
@@ -1026,6 +1073,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1026
1073
|
agentProtectedBranches?: unknown;
|
|
1027
1074
|
agentAllowForcePush?: unknown;
|
|
1028
1075
|
agentAllowDestructiveGit?: unknown;
|
|
1076
|
+
agentAllowOutsideFolder?: unknown;
|
|
1029
1077
|
}>;
|
|
1030
1078
|
/**
|
|
1031
1079
|
* «Run this one without the project's agent prompt.» Absent means no — both
|
|
@@ -1133,6 +1181,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1133
1181
|
agentProtectedBranches?: string[] | undefined;
|
|
1134
1182
|
agentAllowForcePush?: boolean | undefined;
|
|
1135
1183
|
agentAllowDestructiveGit?: boolean | undefined;
|
|
1184
|
+
agentAllowOutsideFolder?: boolean | undefined;
|
|
1136
1185
|
};
|
|
1137
1186
|
tickets: {
|
|
1138
1187
|
number: number;
|
|
@@ -1175,6 +1224,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1175
1224
|
agentProtectedBranches?: unknown;
|
|
1176
1225
|
agentAllowForcePush?: unknown;
|
|
1177
1226
|
agentAllowDestructiveGit?: unknown;
|
|
1227
|
+
agentAllowOutsideFolder?: unknown;
|
|
1178
1228
|
};
|
|
1179
1229
|
tickets: {
|
|
1180
1230
|
number: number;
|
|
@@ -1230,6 +1280,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1230
1280
|
agentProtectedBranches?: string[] | undefined;
|
|
1231
1281
|
agentAllowForcePush?: boolean | undefined;
|
|
1232
1282
|
agentAllowDestructiveGit?: boolean | undefined;
|
|
1283
|
+
agentAllowOutsideFolder?: boolean | undefined;
|
|
1233
1284
|
};
|
|
1234
1285
|
tickets: {
|
|
1235
1286
|
number: number;
|
|
@@ -1275,6 +1326,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1275
1326
|
agentProtectedBranches?: unknown;
|
|
1276
1327
|
agentAllowForcePush?: unknown;
|
|
1277
1328
|
agentAllowDestructiveGit?: unknown;
|
|
1329
|
+
agentAllowOutsideFolder?: unknown;
|
|
1278
1330
|
};
|
|
1279
1331
|
tickets: {
|
|
1280
1332
|
number: number;
|
|
@@ -1457,6 +1509,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1457
1509
|
agentProtectedBranches: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
1458
1510
|
agentAllowForcePush: z.ZodOptional<z.ZodBoolean>;
|
|
1459
1511
|
agentAllowDestructiveGit: z.ZodOptional<z.ZodBoolean>;
|
|
1512
|
+
agentAllowOutsideFolder: z.ZodOptional<z.ZodBoolean>;
|
|
1460
1513
|
}, "strip", z.ZodTypeAny, {
|
|
1461
1514
|
type: "workspace_settings";
|
|
1462
1515
|
workspaceId: string;
|
|
@@ -1466,6 +1519,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1466
1519
|
agentProtectedBranches?: string[] | undefined;
|
|
1467
1520
|
agentAllowForcePush?: boolean | undefined;
|
|
1468
1521
|
agentAllowDestructiveGit?: boolean | undefined;
|
|
1522
|
+
agentAllowOutsideFolder?: boolean | undefined;
|
|
1469
1523
|
}, {
|
|
1470
1524
|
type: "workspace_settings";
|
|
1471
1525
|
workspaceId: string;
|
|
@@ -1475,6 +1529,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
1475
1529
|
agentProtectedBranches?: string[] | undefined;
|
|
1476
1530
|
agentAllowForcePush?: boolean | undefined;
|
|
1477
1531
|
agentAllowDestructiveGit?: boolean | undefined;
|
|
1532
|
+
agentAllowOutsideFolder?: boolean | undefined;
|
|
1478
1533
|
}>, z.ZodObject<{
|
|
1479
1534
|
type: z.ZodLiteral<"session_settings">;
|
|
1480
1535
|
sessionId: z.ZodString;
|
package/dist/protocol.js
CHANGED
|
@@ -151,6 +151,18 @@ export const SessionDescriptorSchema = z.object({
|
|
|
151
151
|
.catch(undefined),
|
|
152
152
|
agentAllowForcePush: z.boolean().optional().catch(undefined),
|
|
153
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),
|
|
154
166
|
budgetUsd: z.number().nullable(),
|
|
155
167
|
budgetMinutes: z.number().nullable(),
|
|
156
168
|
}),
|
|
@@ -391,6 +403,9 @@ export const GatewayFrameSchema = z.discriminatedUnion('type', [
|
|
|
391
403
|
.optional(),
|
|
392
404
|
agentAllowForcePush: z.boolean().optional(),
|
|
393
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(),
|
|
394
409
|
}),
|
|
395
410
|
z.object({
|
|
396
411
|
type: z.literal('session_settings'),
|
package/dist/supervisor.js
CHANGED
|
@@ -67,6 +67,12 @@ function gitPolicyOf(descriptor) {
|
|
|
67
67
|
...(w.agentAllowDestructiveGit !== undefined
|
|
68
68
|
? { agentAllowDestructiveGit: w.agentAllowDestructiveGit }
|
|
69
69
|
: {}),
|
|
70
|
+
// #418 — «may the agent work outside the project folder». Not a git
|
|
71
|
+
// question; it rides in this object because the object is the pipe (plan
|
|
72
|
+
// R8, and the comment on `AgentGitPolicy` in `policy.ts`).
|
|
73
|
+
...(w.agentAllowOutsideFolder !== undefined
|
|
74
|
+
? { agentAllowOutsideFolder: w.agentAllowOutsideFolder }
|
|
75
|
+
: {}),
|
|
70
76
|
};
|
|
71
77
|
}
|
|
72
78
|
function freshLevels() {
|
|
@@ -1759,7 +1765,11 @@ export class Supervisor {
|
|
|
1759
1765
|
const policyChanged = frame.agentPushBan !== undefined ||
|
|
1760
1766
|
frame.agentProtectedBranches !== undefined ||
|
|
1761
1767
|
frame.agentAllowForcePush !== undefined ||
|
|
1762
|
-
frame.agentAllowDestructiveGit !== undefined
|
|
1768
|
+
frame.agentAllowDestructiveGit !== undefined ||
|
|
1769
|
+
// #418 travels in the same frame and the same object: it is read on
|
|
1770
|
+
// every file tool call, so taking the permission away has to reach
|
|
1771
|
+
// a running agent without stopping it.
|
|
1772
|
+
frame.agentAllowOutsideFolder !== undefined;
|
|
1763
1773
|
if (policyChanged) {
|
|
1764
1774
|
const next = {
|
|
1765
1775
|
...(frame.agentPushBan !== undefined ? { agentPushBan: frame.agentPushBan } : {}),
|
|
@@ -1772,6 +1782,9 @@ export class Supervisor {
|
|
|
1772
1782
|
...(frame.agentAllowDestructiveGit !== undefined
|
|
1773
1783
|
? { agentAllowDestructiveGit: frame.agentAllowDestructiveGit }
|
|
1774
1784
|
: {}),
|
|
1785
|
+
...(frame.agentAllowOutsideFolder !== undefined
|
|
1786
|
+
? { agentAllowOutsideFolder: frame.agentAllowOutsideFolder }
|
|
1787
|
+
: {}),
|
|
1775
1788
|
};
|
|
1776
1789
|
Object.assign(running.descriptor.workspace, next);
|
|
1777
1790
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const RUNNER_VERSION = "0.
|
|
1
|
+
export declare const RUNNER_VERSION = "0.62.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/package.json
CHANGED