@sema-agent/server 7.88.2 → 7.90.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/boot/fatal-exit.d.ts +3 -0
- package/dist/boot/fatal-exit.js +5 -0
- package/dist/boot/resolve-spec.js +2 -0
- package/dist/boot/stage-06-runners.js +7 -3
- package/dist/brain.js +32 -19
- package/dist/capabilities/center-plugins.d.ts +10 -0
- package/dist/capabilities/center-plugins.js +21 -5
- package/dist/capabilities/memory-notice.d.ts +7 -1
- package/dist/capabilities/memory-notice.js +6 -6
- package/dist/capabilities/team.d.ts +37 -0
- package/dist/config-catalog.js +7 -19
- package/dist/config-center/apply-effective.js +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +11 -5
- package/dist/hooks/hook-llm.d.ts +25 -0
- package/dist/hooks/hook-llm.js +27 -9
- package/dist/hooks/hook-runner.js +21 -4
- package/dist/http/admission.js +9 -5
- package/dist/http/routes/capabilities.js +2 -2
- package/dist/http/routes/session-sync.js +10 -3
- package/dist/http/routes/sessions.js +16 -14
- package/dist/http/wire-types.d.ts +12 -0
- package/dist/main.js +2 -1
- package/dist/model-route-url.d.ts +27 -3
- package/dist/model-route-url.js +28 -14
- package/dist/plugins/memory-embedder.d.ts +0 -6
- package/dist/plugins/memory-embedder.js +10 -16
- package/dist/plugins/remote-env-adb.d.ts +13 -0
- package/dist/plugins/remote-env-adb.js +42 -9
- package/dist/plugins/remote-env-device.d.ts +39 -3
- package/dist/plugins/remote-env-device.js +107 -31
- package/dist/request-key-closure.js +2 -1
- package/dist/runtime-governance.d.ts +16 -7
- package/dist/runtime-governance.js +27 -7
- package/dist/session-policy-wire.d.ts +103 -0
- package/dist/session-policy-wire.js +66 -0
- package/dist/task-settings.d.ts +12 -2
- package/dist/task-settings.js +7 -5
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/project.d.ts +7 -0
- package/dist/trace/project.js +1 -0
- package/dist/trace/redact.d.ts +110 -5
- package/dist/trace/redact.js +71 -12
- package/package.json +2 -2
|
@@ -99,6 +99,17 @@ const DEVICE_REJECT_TO_REMOTE_ERROR = {
|
|
|
99
99
|
function rejectText(reject) {
|
|
100
100
|
return `[${reject.code}] ${reject.message}`;
|
|
101
101
|
}
|
|
102
|
+
const ENV_TERMINAL = { kind: "env_terminal" };
|
|
103
|
+
const ENV_TERMINAL_MESSAGE = "execution env already destroyed";
|
|
104
|
+
function failureFor(code, message) {
|
|
105
|
+
return { remote: DEVICE_REJECT_TO_REMOTE_ERROR[code], exec: DEVICE_REJECT_TO_EXEC_ERROR[code], file: DEVICE_REJECT_TO_FILE_ERROR[code], message };
|
|
106
|
+
}
|
|
107
|
+
const ADMISSION_DESTROYED = {
|
|
108
|
+
remote: "connect_failed",
|
|
109
|
+
exec: "shell_unavailable",
|
|
110
|
+
file: "unknown",
|
|
111
|
+
message: "execution env already destroyed",
|
|
112
|
+
};
|
|
102
113
|
function assertPosixDevicePlatform(os, deviceId) {
|
|
103
114
|
switch (os) {
|
|
104
115
|
case "darwin":
|
|
@@ -118,6 +129,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
118
129
|
writeFileExclusive;
|
|
119
130
|
writeFileGuarded;
|
|
120
131
|
connected = false;
|
|
132
|
+
connectPromise;
|
|
121
133
|
destroyed = false;
|
|
122
134
|
constructor(cfg) {
|
|
123
135
|
this.cfg = cfg;
|
|
@@ -172,18 +184,61 @@ export class RemoteDeviceExecutionEnv {
|
|
|
172
184
|
};
|
|
173
185
|
}
|
|
174
186
|
async connect(config) {
|
|
175
|
-
if (this.destroyed)
|
|
176
|
-
return { ok: false, error: new RemoteExecutionError("connect_failed", "device env destroyed — create a fresh env instead of reviving") };
|
|
177
187
|
if (config?.abortSignal?.aborted === true)
|
|
178
188
|
return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted") };
|
|
179
|
-
const admitted = await this.
|
|
189
|
+
const admitted = await this.admitShared();
|
|
190
|
+
if (admitted.ok)
|
|
191
|
+
return ok(admitted.value);
|
|
192
|
+
return { ok: false, error: new RemoteExecutionError(admitted.failure.remote, admitted.failure.message) };
|
|
193
|
+
}
|
|
194
|
+
async admitShared() {
|
|
195
|
+
const inflight = this.connectPromise;
|
|
196
|
+
if (inflight !== undefined)
|
|
197
|
+
return inflight;
|
|
198
|
+
const started = this.admit();
|
|
199
|
+
this.connectPromise = started;
|
|
200
|
+
try {
|
|
201
|
+
return await started;
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
if (this.connectPromise === started)
|
|
205
|
+
this.connectPromise = undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async ensureConnected() {
|
|
209
|
+
if (this.destroyed)
|
|
210
|
+
return ADMISSION_DESTROYED;
|
|
211
|
+
if (this.connected)
|
|
212
|
+
return undefined;
|
|
213
|
+
const admitted = await this.admitShared();
|
|
214
|
+
return admitted.ok ? undefined : admitted.failure;
|
|
215
|
+
}
|
|
216
|
+
async admit() {
|
|
217
|
+
if (this.destroyed) {
|
|
218
|
+
return { ok: false, failure: { ...ADMISSION_DESTROYED, message: "device env destroyed — create a fresh env instead of reviving" } };
|
|
219
|
+
}
|
|
220
|
+
let admitted;
|
|
221
|
+
try {
|
|
222
|
+
admitted = await this.cfg.enrollment.admitSession({ rootSessionId: this.cfg.rootSessionId, requester: this.cfg.owner });
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
failure: {
|
|
228
|
+
remote: "connect_failed",
|
|
229
|
+
exec: "target_unavailable",
|
|
230
|
+
file: "transport_lost",
|
|
231
|
+
message: `[device.admission_unavailable] the device admission read failed, so this session's device could not be confirmed (fail-closed): ${err instanceof Error ? err.message : String(err)}`,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
180
235
|
if (!admitted.ok) {
|
|
181
|
-
return { ok: false,
|
|
236
|
+
return { ok: false, failure: failureFor(admitted.reject.code, rejectText(admitted.reject)) };
|
|
182
237
|
}
|
|
183
238
|
if (admitted.value.deviceId !== this.cfg.deviceId) {
|
|
184
239
|
return {
|
|
185
240
|
ok: false,
|
|
186
|
-
|
|
241
|
+
failure: failureFor("device.identity_mismatch", `[${DEVICE_ERROR_CODES.IDENTITY_MISMATCH}] this session is now bound to a different device (${admitted.value.deviceId}) than the one this environment was minted for — refusing to silently re-target; re-initiate the work on the currently bound device (device lane identity, v2 §4.5/§3)`),
|
|
187
242
|
};
|
|
188
243
|
}
|
|
189
244
|
{
|
|
@@ -191,12 +246,20 @@ export class RemoteDeviceExecutionEnv {
|
|
|
191
246
|
if (gained.length > 0) {
|
|
192
247
|
return {
|
|
193
248
|
ok: false,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
249
|
+
failure: {
|
|
250
|
+
remote: "connect_failed",
|
|
251
|
+
exec: "shell_unavailable",
|
|
252
|
+
file: "not_supported",
|
|
253
|
+
message: `[device.capability_drift] this environment was minted while the device's protective write primitives (${gained.join(", ")}) were not observable, ` +
|
|
254
|
+
"so they are absent from its filesystem face — core would read that absence as \"the backend has no such primitive\" and fall back to a NON-atomic write, " +
|
|
255
|
+
"silently re-opening the TOCTOU window guarded writes exist to close (#157). Refusing this connect; the per-task factory will mint a correctly-shaped env on retry.",
|
|
256
|
+
},
|
|
197
257
|
};
|
|
198
258
|
}
|
|
199
259
|
}
|
|
260
|
+
if (this.destroyed) {
|
|
261
|
+
return { ok: false, failure: { ...ADMISSION_DESTROYED, message: "device env destroyed while admitting — create a fresh env instead of reviving" } };
|
|
262
|
+
}
|
|
200
263
|
this.connected = true;
|
|
201
264
|
return ok(this.workspaceHandle());
|
|
202
265
|
}
|
|
@@ -226,24 +289,28 @@ export class RemoteDeviceExecutionEnv {
|
|
|
226
289
|
async cleanup() {
|
|
227
290
|
return this.destroy();
|
|
228
291
|
}
|
|
229
|
-
shellGate() {
|
|
292
|
+
async shellGate() {
|
|
230
293
|
if (this.destroyed)
|
|
231
294
|
return { ok: false, error: new ExecutionError("shell_unavailable", "execution env already destroyed") };
|
|
232
|
-
|
|
233
|
-
|
|
295
|
+
const failure = await this.ensureConnected();
|
|
296
|
+
if (failure)
|
|
297
|
+
return { ok: false, error: new ExecutionError(failure.exec, failure.message) };
|
|
234
298
|
return undefined;
|
|
235
299
|
}
|
|
236
|
-
fsGate(p) {
|
|
300
|
+
async fsGate(p) {
|
|
237
301
|
if (this.destroyed)
|
|
238
302
|
return { ok: false, error: new FileError("unknown", "execution env already destroyed", this.resolve(p)) };
|
|
239
|
-
|
|
240
|
-
|
|
303
|
+
const failure = await this.ensureConnected();
|
|
304
|
+
if (failure)
|
|
305
|
+
return { ok: false, error: new FileError(failure.file, failure.message, this.resolve(p)) };
|
|
241
306
|
return undefined;
|
|
242
307
|
}
|
|
243
308
|
timeoutMsFor(seconds) {
|
|
244
309
|
return seconds != null && Number.isFinite(seconds) && seconds > 0 ? Math.ceil(seconds * 1000) : this.cfg.execTimeoutMs;
|
|
245
310
|
}
|
|
246
311
|
dispatch(kind, args, opts) {
|
|
312
|
+
if (this.destroyed)
|
|
313
|
+
return Promise.resolve(ENV_TERMINAL);
|
|
247
314
|
const input = {
|
|
248
315
|
rootSessionId: this.cfg.rootSessionId,
|
|
249
316
|
requester: this.cfg.owner,
|
|
@@ -262,6 +329,8 @@ export class RemoteDeviceExecutionEnv {
|
|
|
262
329
|
}
|
|
263
330
|
fileOutcome(res, absPath, take) {
|
|
264
331
|
switch (res.kind) {
|
|
332
|
+
case "env_terminal":
|
|
333
|
+
return { ok: false, error: new FileError("unknown", ENV_TERMINAL_MESSAGE, absPath) };
|
|
265
334
|
case "rejected":
|
|
266
335
|
return { ok: false, error: new FileError(DEVICE_REJECT_TO_FILE_ERROR[res.reject.code], rejectText(res.reject), absPath) };
|
|
267
336
|
case "never_started":
|
|
@@ -277,7 +346,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
277
346
|
return { ok: false, error: new FileError("unknown", `device returned a "${got}" outcome for an operation that requires "${want}" — protocol violation`, absPath) };
|
|
278
347
|
}
|
|
279
348
|
async exec(command, options) {
|
|
280
|
-
const gated = this.shellGate();
|
|
349
|
+
const gated = await this.shellGate();
|
|
281
350
|
if (gated)
|
|
282
351
|
return gated;
|
|
283
352
|
let stdout = "";
|
|
@@ -301,6 +370,8 @@ export class RemoteDeviceExecutionEnv {
|
|
|
301
370
|
},
|
|
302
371
|
});
|
|
303
372
|
switch (res.kind) {
|
|
373
|
+
case "env_terminal":
|
|
374
|
+
return { ok: false, error: new ExecutionError("shell_unavailable", ENV_TERMINAL_MESSAGE) };
|
|
304
375
|
case "rejected":
|
|
305
376
|
return { ok: false, error: new ExecutionError(DEVICE_REJECT_TO_EXEC_ERROR[res.reject.code], rejectText(res.reject)) };
|
|
306
377
|
case "never_started":
|
|
@@ -321,8 +392,11 @@ export class RemoteDeviceExecutionEnv {
|
|
|
321
392
|
async *execStream(command, options) {
|
|
322
393
|
if (this.destroyed)
|
|
323
394
|
throw new RemoteExecutionError("connect_failed", "execution env already destroyed");
|
|
324
|
-
|
|
325
|
-
|
|
395
|
+
{
|
|
396
|
+
const failure = await this.ensureConnected();
|
|
397
|
+
if (failure)
|
|
398
|
+
throw new RemoteExecutionError(failure.remote, failure.message);
|
|
399
|
+
}
|
|
326
400
|
const queue = [];
|
|
327
401
|
let queuedBytes = 0;
|
|
328
402
|
let done = false;
|
|
@@ -434,7 +508,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
434
508
|
}
|
|
435
509
|
}
|
|
436
510
|
async exists(p, abortSignal) {
|
|
437
|
-
const gated = this.fsGate(p);
|
|
511
|
+
const gated = await this.fsGate(p);
|
|
438
512
|
if (gated)
|
|
439
513
|
return gated;
|
|
440
514
|
const abs = this.resolve(p);
|
|
@@ -448,7 +522,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
448
522
|
});
|
|
449
523
|
}
|
|
450
524
|
async canonicalPath(p, abortSignal) {
|
|
451
|
-
const gated = this.fsGate(p);
|
|
525
|
+
const gated = await this.fsGate(p);
|
|
452
526
|
if (gated)
|
|
453
527
|
return gated;
|
|
454
528
|
const abs = this.resolve(p);
|
|
@@ -462,7 +536,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
462
536
|
});
|
|
463
537
|
}
|
|
464
538
|
async fileInfo(p, abortSignal) {
|
|
465
|
-
const gated = this.fsGate(p);
|
|
539
|
+
const gated = await this.fsGate(p);
|
|
466
540
|
if (gated)
|
|
467
541
|
return gated;
|
|
468
542
|
const abs = this.resolve(p);
|
|
@@ -476,7 +550,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
476
550
|
});
|
|
477
551
|
}
|
|
478
552
|
async doReadLink(p, abortSignal) {
|
|
479
|
-
const gated = this.fsGate(p);
|
|
553
|
+
const gated = await this.fsGate(p);
|
|
480
554
|
if (gated)
|
|
481
555
|
return gated;
|
|
482
556
|
const abs = this.resolve(p);
|
|
@@ -492,7 +566,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
492
566
|
});
|
|
493
567
|
}
|
|
494
568
|
async readBinaryFile(p, abortSignal) {
|
|
495
|
-
const gated = this.fsGate(p);
|
|
569
|
+
const gated = await this.fsGate(p);
|
|
496
570
|
if (gated)
|
|
497
571
|
return gated;
|
|
498
572
|
const abs = this.resolve(p);
|
|
@@ -508,7 +582,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
508
582
|
return this.fileOutcome(res, abs, (o) => (o.kind === "read" ? ok(new Uint8Array(Buffer.concat(parts))) : this.wrongOutcome("read", o.kind, abs)));
|
|
509
583
|
}
|
|
510
584
|
async readTextFile(p, abortSignal) {
|
|
511
|
-
const gated = this.fsGate(p);
|
|
585
|
+
const gated = await this.fsGate(p);
|
|
512
586
|
if (gated)
|
|
513
587
|
return gated;
|
|
514
588
|
const abs = this.resolve(p);
|
|
@@ -524,7 +598,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
524
598
|
return this.fileOutcome(res, abs, (o) => (o.kind === "read" ? ok(Buffer.concat(parts).toString("utf8")) : this.wrongOutcome("read", o.kind, abs)));
|
|
525
599
|
}
|
|
526
600
|
async readTextLines(p, options) {
|
|
527
|
-
const gated = this.fsGate(p);
|
|
601
|
+
const gated = await this.fsGate(p);
|
|
528
602
|
if (gated)
|
|
529
603
|
return gated;
|
|
530
604
|
const abs = this.resolve(p);
|
|
@@ -547,7 +621,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
547
621
|
});
|
|
548
622
|
}
|
|
549
623
|
async listDir(p, abortSignal) {
|
|
550
|
-
const gated = this.fsGate(p);
|
|
624
|
+
const gated = await this.fsGate(p);
|
|
551
625
|
if (gated)
|
|
552
626
|
return gated;
|
|
553
627
|
const abs = this.resolve(p);
|
|
@@ -567,7 +641,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
567
641
|
return this.doWrite("appendFile", p, content, undefined, abortSignal);
|
|
568
642
|
}
|
|
569
643
|
async doWrite(kind, p, content, _unused, abortSignal) {
|
|
570
|
-
const gated = this.fsGate(p);
|
|
644
|
+
const gated = await this.fsGate(p);
|
|
571
645
|
if (gated)
|
|
572
646
|
return gated;
|
|
573
647
|
const abs = this.resolve(p);
|
|
@@ -590,7 +664,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
590
664
|
});
|
|
591
665
|
}
|
|
592
666
|
async doGuardedWrite(p, content, expect, abortSignal) {
|
|
593
|
-
const gated = this.fsGate(p);
|
|
667
|
+
const gated = await this.fsGate(p);
|
|
594
668
|
if (gated)
|
|
595
669
|
return gated;
|
|
596
670
|
const abs = this.resolve(p);
|
|
@@ -615,7 +689,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
615
689
|
return false;
|
|
616
690
|
}
|
|
617
691
|
async createDir(p, options) {
|
|
618
|
-
const gated = this.fsGate(p);
|
|
692
|
+
const gated = await this.fsGate(p);
|
|
619
693
|
if (gated)
|
|
620
694
|
return gated;
|
|
621
695
|
const abs = this.resolve(p);
|
|
@@ -623,7 +697,7 @@ export class RemoteDeviceExecutionEnv {
|
|
|
623
697
|
return this.fileOutcome(res, abs, (o) => (o.kind === "void" ? ok(undefined) : this.wrongOutcome("void", o.kind, abs)));
|
|
624
698
|
}
|
|
625
699
|
async remove(p, options) {
|
|
626
|
-
const gated = this.fsGate(p);
|
|
700
|
+
const gated = await this.fsGate(p);
|
|
627
701
|
if (gated)
|
|
628
702
|
return gated;
|
|
629
703
|
const abs = this.resolve(p);
|
|
@@ -631,14 +705,14 @@ export class RemoteDeviceExecutionEnv {
|
|
|
631
705
|
return this.fileOutcome(res, abs, (o) => (o.kind === "void" ? ok(undefined) : this.wrongOutcome("void", o.kind, abs)));
|
|
632
706
|
}
|
|
633
707
|
async createTempDir(prefix = "tmp-", abortSignal) {
|
|
634
|
-
const gated = this.fsGate(this.cwd);
|
|
708
|
+
const gated = await this.fsGate(this.cwd);
|
|
635
709
|
if (gated)
|
|
636
710
|
return gated;
|
|
637
711
|
const res = await this.dispatch("createTempDir", { prefix }, { timeoutMs: this.cfg.execTimeoutMs, ...(abortSignal === undefined ? {} : { signal: abortSignal }) });
|
|
638
712
|
return this.fileOutcome(res, this.cwd, (o) => (o.kind === "write" ? ok(o.canonicalPath) : this.wrongOutcome("write", o.kind, this.cwd)));
|
|
639
713
|
}
|
|
640
714
|
async createTempFile(options) {
|
|
641
|
-
const gated = this.fsGate(this.cwd);
|
|
715
|
+
const gated = await this.fsGate(this.cwd);
|
|
642
716
|
if (gated)
|
|
643
717
|
return gated;
|
|
644
718
|
const res = await this.dispatch("createTempFile", { ...(options?.prefix === undefined ? {} : { prefix: options.prefix }), ...(options?.suffix === undefined ? {} : { suffix: options.suffix }) }, { timeoutMs: this.cfg.execTimeoutMs, ...(options?.abortSignal === undefined ? {} : { signal: options.abortSignal }) });
|
|
@@ -666,6 +740,8 @@ function toWireExpectation(e) {
|
|
|
666
740
|
}
|
|
667
741
|
function streamFailure(res) {
|
|
668
742
|
switch (res.kind) {
|
|
743
|
+
case "env_terminal":
|
|
744
|
+
return new RemoteExecutionError("connect_failed", ENV_TERMINAL_MESSAGE);
|
|
669
745
|
case "rejected":
|
|
670
746
|
return new RemoteExecutionError(DEVICE_REJECT_TO_REMOTE_ERROR[res.reject.code], rejectText(res.reject));
|
|
671
747
|
case "never_started":
|
|
@@ -20,7 +20,8 @@ export function keyClosureClause(names, label) {
|
|
|
20
20
|
const TASK_BODY_DECLARED_KEYS = [
|
|
21
21
|
"a2aPeers", "acceptPartialRestore", "additionalDirectories", "additionalReadDirectories", "agents",
|
|
22
22
|
"appendSystemPrompt", "attachmentIds", "attachments", "capabilitiesNeeded", "cascade", "clientContext",
|
|
23
|
-
"compaction", "compactionModel", "council", "cwd", "debate", "deferTools", "deviceId", "enableFork",
|
|
23
|
+
"compaction", "compactionModel", "council", "cwd", "debate", "deferTools", "deviceId", "enableFork",
|
|
24
|
+
"excludeAllTools", "excludeTools",
|
|
24
25
|
"finalVerification", "forwardSubagentEvents", "images", "interactiveTools", "jobId", "limits", "maxCostUsd",
|
|
25
26
|
"maxTokens", "mcpServers", "memory", "memoryCapture", "memoryWrite", "model", "objective", "oneShot", "outputRetries",
|
|
26
27
|
"outputSchema", "permissionMode", "projectId", "promptProfile", "reasoningEffort", "requireExistingSession",
|
|
@@ -24,12 +24,14 @@
|
|
|
24
24
|
* name). Real isolation is the `executionEnv` sandbox. Same caveat core documents on `createCoarseCommandNamePolicy`.
|
|
25
25
|
*/
|
|
26
26
|
import { type TaskSpec, type ToolPolicy } from "@sema-agent/core";
|
|
27
|
+
import { GovernanceConfig } from "@sema-agent/settings-schema";
|
|
27
28
|
import { type GovernanceAskMarks } from "./governance-ask-marks.js";
|
|
28
|
-
/**
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
/** 运维 autonomy 四档的**闭集**(同上)。`config.ts` 的 `parseAutonomy` 也从这里读 —— 四档词表全仓一份。 */
|
|
30
|
+
export declare const AUTONOMY_MODES: ["read-only", "ask", "plan", "auto"];
|
|
31
|
+
/** A per-command governance rule (registry `runtime.commandPolicy[]`) —— **型也从契约派生**:
|
|
32
|
+
* 契约给 `command` 加一条 refine / 给 `decision` 加一个判词,本仓的型当场跟上(或在下面的
|
|
33
|
+
* `RANK` / 穷尽 switch 上编译红),而不是靠人记得把手写联合改一遍。 */
|
|
34
|
+
export type CommandRule = NonNullable<GovernanceConfig["commandPolicy"]>[number];
|
|
33
35
|
/** True when `name` names a DELEGATION tool — the subagent tool(CC-187 canonical `"Agent"`,core
|
|
34
36
|
* `DEFAULT_SUBAGENT_TOOL_NAME`)。5.0.0 [ref]:折叠面退役,RAW 比对 core 单源常量——旧名("Task")
|
|
35
37
|
* 在 core roster 层响亮 miss,不再需要归一;单源常量本身就防了当年 1.202 改名时裸字符串失配的
|
|
@@ -50,8 +52,8 @@ export declare function isDelegationToolName(name: string): boolean;
|
|
|
50
52
|
export declare function stripDelegationTools<T extends {
|
|
51
53
|
name: string;
|
|
52
54
|
}>(tools: readonly T[]): T[];
|
|
53
|
-
/** The operator autonomy mode (registry `
|
|
54
|
-
export type Autonomy = "
|
|
55
|
+
/** The operator autonomy mode (registry `governance.autonomy`) —— 契约派生(见 {@link AUTONOMY_MODES})。 */
|
|
56
|
+
export type Autonomy = NonNullable<GovernanceConfig["autonomy"]>;
|
|
55
57
|
/** 导出给 `test/coarse-shell-tools-mirror.test.ts` 的同源钉用(见那里的顶注)。 */
|
|
56
58
|
export declare const COARSE_SHELL_TOOLS_MIRROR: readonly string[];
|
|
57
59
|
/** Validate `commandPolicy` rules — the shape (array of {command, decision}), the `command` (a bare argv[0] name,
|
|
@@ -81,6 +83,13 @@ export declare function validateCommandRules(rules: CommandRule[]): string[];
|
|
|
81
83
|
* Either way the result only ever ADDS asks/denies — composed onto the baseline via `combinePolicies`
|
|
82
84
|
* (deny-wins) by {@link applyRuntimeGovernance}, so it is tighten-only. (Assumes commands are pre-validated by
|
|
83
85
|
* {@link validateCommandRules}; an un-matchable command here is simply inert, never a parse error.)
|
|
86
|
+
*
|
|
87
|
+
* 🔴 **S-496:词表外的 `decision` 折 `deny`,不折 `ask`。** {@link validateCommandRules} 的头注写下的那条
|
|
88
|
+
* 后果(「域外的 `decision` 穿过本函数的三元链落到 ASK,静默把一条 intended deny 弱化掉」)此前只靠
|
|
89
|
+
* **调用方记得先校验**挡着 —— 而本函数在 `resolve-spec` 里是逐请求现调的,`config.commandPolicy` 的来路
|
|
90
|
+
* (center hot apply / 本地 `config.d/governance.json` / LKG / 测试直塞)不止一条。现在闭集在本函数**内部**
|
|
91
|
+
* 收口:`VALID_DECISIONS` 之外的值一律折最严的 `deny`(fail-closed,[ref] 的运行期 miss 臂,下方自陈),
|
|
92
|
+
* 闭集本身则由契约 zod 派生 ⇒ 契约加一个判词,`RANK` 与桶分配的穷尽 switch 双双**编译红**。
|
|
84
93
|
*/
|
|
85
94
|
export declare function compileCommandPolicy(rules: CommandRule[] | undefined): ToolPolicy | undefined;
|
|
86
95
|
/**
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { combinePolicies, COARSE_SHELL_TOOLS as CORE_COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, DEFAULT_SUBAGENT_TOOL_NAME, parseLeadingCommandName, tightenTaskSpec, } from "@sema-agent/core";
|
|
2
|
-
import { SAFETY_MERGE_SPEC } from "@sema-agent/settings-schema";
|
|
2
|
+
import { GovernanceConfig, SAFETY_MERGE_SPEC } from "@sema-agent/settings-schema";
|
|
3
3
|
import { currentGovernanceAskMarks } from "./governance-ask-marks.js";
|
|
4
4
|
import { operatorAsk } from "./operator-ask.js";
|
|
5
|
+
const COMMAND_RULE_SHAPE = GovernanceConfig.shape.commandPolicy.unwrap().element.shape;
|
|
6
|
+
const COMMAND_DECISIONS = COMMAND_RULE_SHAPE.decision.options;
|
|
7
|
+
export const AUTONOMY_MODES = GovernanceConfig.shape.autonomy.unwrap().options;
|
|
5
8
|
export function isDelegationToolName(name) {
|
|
6
9
|
return name === DEFAULT_SUBAGENT_TOOL_NAME;
|
|
7
10
|
}
|
|
@@ -30,7 +33,9 @@ function createCommandAskListPolicy(askCommands, tools = COARSE_SHELL_TOOLS) {
|
|
|
30
33
|
};
|
|
31
34
|
}
|
|
32
35
|
const VALID_COMMAND_NAME = /^[A-Za-z0-9._+-]+$/;
|
|
33
|
-
|
|
36
|
+
function isCommandDecision(raw) {
|
|
37
|
+
return typeof raw === "string" && COMMAND_DECISIONS.includes(raw);
|
|
38
|
+
}
|
|
34
39
|
export function validateCommandRules(rules) {
|
|
35
40
|
const errors = [];
|
|
36
41
|
if (!Array.isArray(rules))
|
|
@@ -45,8 +50,8 @@ export function validateCommandRules(rules) {
|
|
|
45
50
|
errors.push(`commandPolicy command "${String(command)}" is not a bare argv[0] name — core matches the EXACT command name (no glob/path/operators). ` +
|
|
46
51
|
`Use e.g. "git" not "git*"; to gate sub-commands or pipes, deny the interpreter ("sh"/"bash") + rely on the sandbox.`);
|
|
47
52
|
}
|
|
48
|
-
if (
|
|
49
|
-
errors.push(`commandPolicy command "${String(command)}" has invalid decision "${String(decision)}" — must be one of
|
|
53
|
+
if (!isCommandDecision(decision)) {
|
|
54
|
+
errors.push(`commandPolicy command "${String(command)}" has invalid decision "${String(decision)}" — must be one of ${COMMAND_DECISIONS.join(" | ")}`);
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
57
|
return errors;
|
|
@@ -57,15 +62,30 @@ export function compileCommandPolicy(rules) {
|
|
|
57
62
|
const RANK = { deny: 3, ask: 2, allow: 1 };
|
|
58
63
|
const strictest = new Map();
|
|
59
64
|
for (const r of rules) {
|
|
65
|
+
const decision = isCommandDecision(r.decision) ? r.decision : "deny";
|
|
60
66
|
const prev = strictest.get(r.command);
|
|
61
|
-
if (prev === undefined
|
|
62
|
-
strictest.set(r.command,
|
|
67
|
+
if (RANK[decision] > (prev === undefined ? 0 : RANK[prev]))
|
|
68
|
+
strictest.set(r.command, decision);
|
|
63
69
|
}
|
|
64
70
|
const deny = [];
|
|
65
71
|
const allow = [];
|
|
66
72
|
const ask = [];
|
|
67
73
|
for (const [command, decision] of strictest) {
|
|
68
|
-
(decision
|
|
74
|
+
switch (decision) {
|
|
75
|
+
case "deny":
|
|
76
|
+
deny.push(command);
|
|
77
|
+
break;
|
|
78
|
+
case "allow":
|
|
79
|
+
allow.push(command);
|
|
80
|
+
break;
|
|
81
|
+
case "ask":
|
|
82
|
+
ask.push(command);
|
|
83
|
+
break;
|
|
84
|
+
default: {
|
|
85
|
+
const unhandled = decision;
|
|
86
|
+
throw new Error(`commandPolicy decision "${String(unhandled)}" has no compilation arm`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
69
89
|
}
|
|
70
90
|
const policies = [];
|
|
71
91
|
if (allow.length > 0) {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SessionPermissionRules` 的 **wire 形读者** —— 两条写入腿(`PUT /v1/sessions/:id/policy` 与
|
|
3
|
+
* `POST /v1/sessions/:id/sync/import` 的 `policy` 面)**唯一**的形判据。
|
|
4
|
+
*
|
|
5
|
+
* ── 为什么是一只共享读者(S-496 / [ref],取证 P4 + S-498)───────────────────────────────────────────
|
|
6
|
+
* 改前两条腿各自为政:
|
|
7
|
+
* · PUT 腿在 `http/routes/sessions.ts` 里**手抄**五桶键名(`["toolAllow","toolDeny","allowDirs",
|
|
8
|
+
* "commandAllow","commandDeny"] as const`),core 的 `SessionPermissionRules` 加第六桶 ⇒ 那一桶在
|
|
9
|
+
* PUT 上被**静默丢**、200 回执却说「已生效」;
|
|
10
|
+
* · sync/import 腿(S-498)只判 `Array.isArray(body.policy)` **外层**,逐条记录的 `principal` / `rules`
|
|
11
|
+
* 一个字节都不校验就裸交 `replayPolicyRecords` → `putRules`。坏形走的是**放宽**方向,亲读 core
|
|
12
|
+
* `session-policy-store.js` 两处实现:
|
|
13
|
+
* · `normalizeRules` 对每桶做 `[...rules.toolDeny]` ⇒ 一个**字符串** `"rm"` 被展开成 `["r","m"]`,
|
|
14
|
+
* 本来该拒 `rm` 的 deny 规则变成拒两个不存在的命令名 = 门整条失效;
|
|
15
|
+
* · `allowDirs` 走 `.map(...)` ⇒ 字符串上没有 `.map` ⇒ **TypeError → 500**(路由头注 `:300-302`
|
|
16
|
+
* 自陈「Malformed shape → 400(never a downstream TypeError)」,这一半没守);
|
|
17
|
+
* · `loosenReasons` 的每一臂都从 `prior` 出发,**prior 为空时恒回空集** ⇒ tighten-only 门对一条
|
|
18
|
+
* 全新的会话放行任何内容,坏形因此连「收紧门」这一层都碰不到。
|
|
19
|
+
* ⇒ 类修:一只读者,键集由 `keyof SessionPermissionRules` **派生**(下方 `RULE_FIELD_PRESENCE` 的
|
|
20
|
+
* `satisfies Record<keyof SessionPermissionRules, true>`:core 加桶 ⇒ 缺键**编译红**;core 删桶 ⇒
|
|
21
|
+
* 多键**编译红**),两条腿同调。
|
|
22
|
+
*
|
|
23
|
+
* ── 文案也在这里(codex 一轮追加:两条腿各铸一份句子 = 第二份手抄)───────────────────────────────
|
|
24
|
+
* 初版把 wire 文案留在两个路由各自铸,理由是错误文案冻结门(`test/api-error-text-freeze.test.ts`)锚的是
|
|
25
|
+
* `src/http/` 桶里的 `sendError` 站点。codex 一轮指出那等于**把同一句话写了两遍**(`sessions.ts` 与
|
|
26
|
+
* `session-sync.ts` 各一份),这正是本批要消灭的形。改法照本仓既有成案(S-405 / [ref] / [ref] 三处
|
|
27
|
+
* 「常量锚」):句子收进本文件**单一属主**,冻结职责搬到冻结门的**常量锚格** —— 那一格直接断言这两只
|
|
28
|
+
* 渲染器对每一种判词的输出字节,且对判词闭集穷尽(加一种判词 ⇒ 那一格编译红)。文案一个字节未改。
|
|
29
|
+
*/
|
|
30
|
+
import type { SessionPermissionRules, SessionRulesRecord } from "@sema-agent/core";
|
|
31
|
+
/** core `SessionPermissionRules` 的桶名全集(派生,不是手抄)。 */
|
|
32
|
+
export declare const SESSION_PERMISSION_RULE_FIELDS: readonly (keyof SessionPermissionRules)[];
|
|
33
|
+
/** 一条规则体的形问题。判词而非文案(见文件头注的「文案属主」一段)。 */
|
|
34
|
+
export type SessionPermissionRulesProblem =
|
|
35
|
+
/** 整体不是一只普通对象(`null` / 数组 / 标量 / 缺席)。 */
|
|
36
|
+
{
|
|
37
|
+
kind: "not-an-object";
|
|
38
|
+
}
|
|
39
|
+
/** 某一桶在场但不是 `string[]`(字符串 / 数字 / 混杂数组都在此)。 */
|
|
40
|
+
| {
|
|
41
|
+
kind: "field-not-string-array";
|
|
42
|
+
field: keyof SessionPermissionRules;
|
|
43
|
+
};
|
|
44
|
+
/** {@link readSessionPermissionRules} 的读数:坏形给判词,好形给**已收窄**的规则体(调用方零 cast)。 */
|
|
45
|
+
export type SessionPermissionRulesRead = {
|
|
46
|
+
problem: SessionPermissionRulesProblem;
|
|
47
|
+
rules?: undefined;
|
|
48
|
+
} | {
|
|
49
|
+
problem?: undefined;
|
|
50
|
+
rules: SessionPermissionRules;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* 读一只 wire 上的 `SessionPermissionRules`:形不对给判词,形对给**只含五桶**的规则体。
|
|
54
|
+
*
|
|
55
|
+
* 认识面窄得刻意:**只判形**,不判内容(桶内字符串的 pattern 文法、路径是否可达、tighten-only 是否成立
|
|
56
|
+
* 都不在这里 —— 前两者归 core 的 rule 文法与 `normalizeRules`,后者归 `putRules` 的收紧门)。
|
|
57
|
+
* 未知键**照旧忽略**(不是新的拒因):core 的 `normalizeRules` 本来就只搬这五桶,本读者与它同口径。
|
|
58
|
+
*/
|
|
59
|
+
export declare function readSessionPermissionRules(raw: unknown): SessionPermissionRulesRead;
|
|
60
|
+
/** 一条 `SessionRulesRecord` 的形问题(`policy[]` 的元素级;桶级问题原样转载在 `rules` 位)。 */
|
|
61
|
+
export type SessionRulesRecordProblem =
|
|
62
|
+
/** `policy` 本身不是数组。 */
|
|
63
|
+
{
|
|
64
|
+
kind: "not-an-array";
|
|
65
|
+
}
|
|
66
|
+
/** 第 `index` 条记录不是一只普通对象。 */
|
|
67
|
+
| {
|
|
68
|
+
kind: "record-not-an-object";
|
|
69
|
+
index: number;
|
|
70
|
+
}
|
|
71
|
+
/** 第 `index` 条的 `principal` 在场但不是字符串(**缺席**才是「会话级默认行」的合法写法 —— 两个店的
|
|
72
|
+
* `listBySession` 对无 principal 的行都产 `undefined`,JSON 序列化后即缺席,所以 `null` 不在受理面)。 */
|
|
73
|
+
| {
|
|
74
|
+
kind: "principal-not-a-string";
|
|
75
|
+
index: number;
|
|
76
|
+
}
|
|
77
|
+
/** 第 `index` 条的 `rules` 形不对(转载桶级判词)。 */
|
|
78
|
+
| {
|
|
79
|
+
kind: "rules";
|
|
80
|
+
index: number;
|
|
81
|
+
problem: SessionPermissionRulesProblem;
|
|
82
|
+
};
|
|
83
|
+
/** {@link readSessionRulesRecords} 的读数。 */
|
|
84
|
+
export type SessionRulesRecordsRead = {
|
|
85
|
+
problem: SessionRulesRecordProblem;
|
|
86
|
+
records?: undefined;
|
|
87
|
+
} | {
|
|
88
|
+
problem?: undefined;
|
|
89
|
+
records: SessionRulesRecord[];
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* 读 sync/import 的 `policy` 面(`SessionRulesRecord[]`)。好形返回**已收窄**的记录数组,坏形给判词。
|
|
93
|
+
*
|
|
94
|
+
* `rev`:记录里带的 `rev` 是**导出端的**乐观锁读数,`putRules` 侧一进门就 `stripRev` 掉(`sameRulesContent`
|
|
95
|
+
* 也比的是去 rev 的字节形),所以它在本读者里只是过境位 —— 非数字即当缺席(不是拒因:一个坏 rev 改变不了
|
|
96
|
+
* 任何一次写入的结果,拒它只会把合法同步砖死)。
|
|
97
|
+
*/
|
|
98
|
+
export declare function readSessionRulesRecords(raw: unknown): SessionRulesRecordsRead;
|
|
99
|
+
/** 桶级判词 → 人话。闭集穷尽:{@link SessionPermissionRulesProblem} 加一种判词 ⇒ 本 switch 编译红。 */
|
|
100
|
+
export declare function describeSessionPermissionRulesProblem(p: SessionPermissionRulesProblem): string;
|
|
101
|
+
/** `policy[]` 判词 → 人话(sync/import 腿在它前面拼 `policy: `)。闭集穷尽同上。 */
|
|
102
|
+
export declare function describeSessionRulesRecordProblem(p: SessionRulesRecordProblem): string;
|
|
103
|
+
//# sourceMappingURL=session-policy-wire.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const RULE_FIELD_PRESENCE = {
|
|
2
|
+
toolAllow: true,
|
|
3
|
+
toolDeny: true,
|
|
4
|
+
allowDirs: true,
|
|
5
|
+
commandAllow: true,
|
|
6
|
+
commandDeny: true,
|
|
7
|
+
};
|
|
8
|
+
export const SESSION_PERMISSION_RULE_FIELDS = Object.keys(RULE_FIELD_PRESENCE);
|
|
9
|
+
export function readSessionPermissionRules(raw) {
|
|
10
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
11
|
+
return { problem: { kind: "not-an-object" } };
|
|
12
|
+
const src = raw;
|
|
13
|
+
const rules = {};
|
|
14
|
+
for (const field of SESSION_PERMISSION_RULE_FIELDS) {
|
|
15
|
+
const v = src[field];
|
|
16
|
+
if (v === undefined)
|
|
17
|
+
continue;
|
|
18
|
+
if (!Array.isArray(v) || !v.every((x) => typeof x === "string"))
|
|
19
|
+
return { problem: { kind: "field-not-string-array", field } };
|
|
20
|
+
rules[field] = v;
|
|
21
|
+
}
|
|
22
|
+
return { rules };
|
|
23
|
+
}
|
|
24
|
+
export function readSessionRulesRecords(raw) {
|
|
25
|
+
if (!Array.isArray(raw))
|
|
26
|
+
return { problem: { kind: "not-an-array" } };
|
|
27
|
+
const records = [];
|
|
28
|
+
for (let index = 0; index < raw.length; index++) {
|
|
29
|
+
const e = raw[index];
|
|
30
|
+
if (e === null || typeof e !== "object" || Array.isArray(e))
|
|
31
|
+
return { problem: { kind: "record-not-an-object", index } };
|
|
32
|
+
const { principal, rules, rev } = e;
|
|
33
|
+
if (principal !== undefined && typeof principal !== "string")
|
|
34
|
+
return { problem: { kind: "principal-not-a-string", index } };
|
|
35
|
+
const read = readSessionPermissionRules(rules);
|
|
36
|
+
if (read.problem !== undefined)
|
|
37
|
+
return { problem: { kind: "rules", index, problem: read.problem } };
|
|
38
|
+
const innerRev = rules?.rev;
|
|
39
|
+
const carried = typeof innerRev === "number" ? innerRev : typeof rev === "number" ? rev : 0;
|
|
40
|
+
records.push({ principal, rules: { ...read.rules, rev: carried } });
|
|
41
|
+
}
|
|
42
|
+
return { records };
|
|
43
|
+
}
|
|
44
|
+
export function describeSessionPermissionRulesProblem(p) {
|
|
45
|
+
switch (p.kind) {
|
|
46
|
+
case "not-an-object": return "rules must be a SessionPermissionRules object";
|
|
47
|
+
case "field-not-string-array": return `${p.field} must be an array of strings`;
|
|
48
|
+
default: {
|
|
49
|
+
const unhandled = p;
|
|
50
|
+
throw new Error(`unhandled SessionPermissionRules problem: ${JSON.stringify(unhandled)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function describeSessionRulesRecordProblem(p) {
|
|
55
|
+
switch (p.kind) {
|
|
56
|
+
case "not-an-array": return "must be an array of {principal, rules} records";
|
|
57
|
+
case "record-not-an-object": return `[${p.index}] must be a {principal, rules} record`;
|
|
58
|
+
case "principal-not-a-string": return `[${p.index}].principal must be a string when present (omit it for the session-wide row)`;
|
|
59
|
+
case "rules": return `[${p.index}].rules: ${describeSessionPermissionRulesProblem(p.problem)}`;
|
|
60
|
+
default: {
|
|
61
|
+
const unhandled = p;
|
|
62
|
+
throw new Error(`unhandled SessionRulesRecord problem: ${JSON.stringify(unhandled)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=session-policy-wire.js.map
|
package/dist/task-settings.d.ts
CHANGED
|
@@ -48,7 +48,16 @@ import { type KeyClosureIssue } from "./request-key-closure.js";
|
|
|
48
48
|
* deployment approval/governance baseline is composed OUTSIDE this derive and is untouchable from a settings stamp),
|
|
49
49
|
* and `auto` adds the same ask gate as `default` with core's auto-mode classifier screening the asks upstream
|
|
50
50
|
* (entitlement-gated in core, [ref]②). */
|
|
51
|
-
export
|
|
51
|
+
export declare const SETTINGS_PERMISSION_MODES: readonly ["default", "acceptEdits", "plan", "bypassPermissions", "auto"];
|
|
52
|
+
/** 五模式闭集的型形 —— **从值表派生**(S-496 / [ref]):改前本仓有 **4 份**同一张五词表(本处的手写联合 /
|
|
53
|
+
* 下面 `coercePermissionModeStrict` 的运行期字面量链 / `http/admission.ts` 顶层 `permissionMode` 的比较链 /
|
|
54
|
+
* 同文件 `settings.permissions.defaultMode` 的比较链),外加两句错误文案里各抄一遍词。现在值表一份、
|
|
55
|
+
* 型与文案都从它派生,比较链改调 {@link isSettingsPermissionMode}。 */
|
|
56
|
+
export type SettingsPermissionMode = (typeof SETTINGS_PERMISSION_MODES)[number];
|
|
57
|
+
/** 五词表的 wire 文案形(`"a" | "b" | …`)—— 两条 400 的句子从这里取词,不再各抄一份。 */
|
|
58
|
+
export declare const SETTINGS_PERMISSION_MODE_WORDS: string;
|
|
59
|
+
/** 闭集成员判定(`unknown` 入口用)—— 词表的**唯一**运行期读者:wire 门与 coerce 两族同调。 */
|
|
60
|
+
export declare function isSettingsPermissionMode(raw: unknown): raw is SettingsPermissionMode;
|
|
52
61
|
/**
|
|
53
62
|
* R4: coerce a raw top-level `body.permissionMode` (the LIGHT per-turn wire — the shell sends the
|
|
54
63
|
* CC permission mode RAW, axis-agnostic; the service interprets) to a service-honored mode, or undefined.
|
|
@@ -68,7 +77,8 @@ export type SettingsPermissionMode = "default" | "acceptEdits" | "plan" | "bypas
|
|
|
68
77
|
*/
|
|
69
78
|
export declare function coercePermissionMode(raw: unknown): SettingsPermissionMode | undefined;
|
|
70
79
|
/** 五模式闭集的**严格**读法([ref] ④:`/v1/capabilities?permissionMode=` 查询串用它 —— 读面对词表外的值 400,
|
|
71
|
-
* 不走上面那条给 resume 重放留的 `default` 宽臂)
|
|
80
|
+
* 不走上面那条给 resume 重放留的 `default` 宽臂)。上面的宽读只是本函数外再包一层折叠。
|
|
81
|
+
* S-496:词表单点已上移到 {@link SETTINGS_PERMISSION_MODES},本函数只是它的 `undefined` 形读者。 */
|
|
72
82
|
export declare function coercePermissionModeStrict(raw: unknown): SettingsPermissionMode | undefined;
|
|
73
83
|
/**
|
|
74
84
|
* S-433 —— 一份**被批准的计划**之后,这条会话继续用的 permission mode(`plan_review` 决裁体的
|