copilot-operator 0.1.2 → 0.1.4
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 +13 -3
- package/dist/src/api/operator.controller.js +6 -1
- package/dist/src/api/operator.service.js +57 -22
- package/dist/src/cli.js +1 -0
- package/dist/src/config/schema.js +25 -0
- package/dist/src/exec/authorizer.js +20 -10
- package/dist/src/exec/commandHeads.js +799 -0
- package/dist/src/exec/policy.js +1 -1
- package/dist/src/exec/policyManifest.js +16 -2
- package/dist/src/exec/programs.js +22 -82
- package/dist/src/exec/reportFile.js +5 -2
- package/dist/src/orchestrator/review.js +4 -3
- package/dist/src/orchestrator/taskRunner.js +33 -7
- package/dist/src/plan/schema.js +7 -0
- package/dist/src/plan/systemGuide.js +30 -6
- package/dist/src/protocol/reporter.js +8 -6
- package/dist/src/session/exports.js +128 -1
- package/dist/src/session/store.js +2 -0
- package/dist/src/vcs/taskVcs.js +33 -0
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/867-f0e3e152f6f67e45.js +1 -0
- package/dist/web/_next/static/chunks/89-382e9b29486d2747.js +1 -0
- package/dist/web/_next/static/chunks/app/defaults/page-1747c931adc746fd.js +1 -0
- package/dist/web/_next/static/chunks/app/history/page-8d4f817e36986e3f.js +1 -0
- package/dist/web/_next/static/chunks/app/page-f04d6e00182ccc76.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-185f5fcc383e6619.js +1 -0
- package/dist/web/appearance.html +2 -2
- package/dist/web/appearance.txt +10 -10
- package/dist/web/defaults.html +2 -2
- package/dist/web/defaults.txt +10 -10
- package/dist/web/history.html +2 -2
- package/dist/web/history.txt +10 -10
- package/dist/web/import.html +2 -2
- package/dist/web/import.txt +10 -10
- package/dist/web/index.html +2 -2
- package/dist/web/index.txt +10 -10
- package/dist/web/level1.html +2 -2
- package/dist/web/level1.txt +10 -10
- package/dist/web/presets.html +2 -2
- package/dist/web/presets.txt +10 -10
- package/dist/web/sessions/view.html +2 -2
- package/dist/web/sessions/view.txt +10 -10
- package/dist/web/system.html +2 -2
- package/dist/web/system.txt +10 -10
- package/package.json +1 -1
- package/dist/web/_next/static/chunks/867-5b104743e24a4e2f.js +0 -1
- package/dist/web/_next/static/chunks/955-cf291b8a60315c58.js +0 -1
- package/dist/web/_next/static/chunks/app/defaults/page-559af121c43127a1.js +0 -1
- package/dist/web/_next/static/chunks/app/history/page-0df90ab9998815db.js +0 -1
- package/dist/web/_next/static/chunks/app/page-050cf6f71187e8af.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-4d31690c22c8b331.js +0 -1
- /package/dist/web/_next/static/{eWzbORCbQ1h_2IeLH_njb → p9lexLS0u8W5N6VzLavHO}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{eWzbORCbQ1h_2IeLH_njb → p9lexLS0u8W5N6VzLavHO}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -82,7 +82,11 @@ file run. What `Start-Process` is asked to start is judged like any other progra
|
|
|
82
82
|
`Invoke-WebRequest`, `Invoke-RestMethod`, `curl`, `wget`, `Start-BitsTransfer`, the .NET web
|
|
83
83
|
clients, `ftp`/`scp`, a package named by URL or git address, `npx` of a tool not installed in the
|
|
84
84
|
project, `docker pull` — is neither refused nor run: it waits on the approval screen, in every
|
|
85
|
-
mode, unattended included, and "run the rest without asking" does not answer it.
|
|
85
|
+
mode, unattended included, and "run the rest without asking" does not answer it. That is the
|
|
86
|
+
default, and the one stop an unattended run still makes; `execution.networkFetch` (Settings →
|
|
87
|
+
Execution) may instead **refuse** it back to the chat with the reason, so the run never waits, or
|
|
88
|
+
**run** it unread on a machine where that is acceptable. The choice applies only to unattended
|
|
89
|
+
runs, and each task's policy manifest records which was in force. A check that
|
|
86
90
|
fetches is refused, since a check runs with nobody asked. Requests to this machine (`localhost`,
|
|
87
91
|
`127.0.0.1`) and the project's own package managers are not held. `npm install` runs with
|
|
88
92
|
`ignore-scripts`: a package's install script is code fetched and executed unread, and it does not
|
|
@@ -128,8 +132,14 @@ Six rules decide what a step may do, and each is recorded with the run that it g
|
|
|
128
132
|
- **Only the declared toolchain runs.** `execution.allowedPrograms` names the programs a command
|
|
129
133
|
may start — the shells, the JavaScript, .NET, Java, Python, Go and Rust tools, `git`, and a few
|
|
130
134
|
Windows utilities. A command that starts anything else is refused and sent back with the reason.
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
The programs are read the way PowerShell reads a line (`src/exec/commandHeads.ts`): strings,
|
|
136
|
+
operators, array values, method arguments and hashtable keys are not programs, and a command in a
|
|
137
|
+
script block, a subexpression, a string's `$( )` or after an assignment is still found. A line that
|
|
138
|
+
cannot be read with certainty — an unclosed quote or bracket, a program called through a variable
|
|
139
|
+
(`& $exe`) — is refused rather than guessed at. `cmd` steps are read by `cmd`'s own rules. A refused
|
|
140
|
+
step is reported to the chat as refused and never executed, with no exit code, apart from a
|
|
141
|
+
command that ran and failed. An empty list turns the gate off. This is a floor against the unknown
|
|
142
|
+
binary, not a boundary: allowing `node` allows `node -e`.
|
|
133
143
|
- **Autonomy is graded.** An unattended run carries strictly more restrictions than a watched one,
|
|
134
144
|
because the thing that makes a watched run safe is a person reading each line. Unattended
|
|
135
145
|
requires a non-empty allowlist, and refuses an allowed program used to evaluate a string
|
|
@@ -251,7 +251,12 @@ let OperatorController = class OperatorController {
|
|
|
251
251
|
*/
|
|
252
252
|
async exportBundle(res, body) {
|
|
253
253
|
const pairs = (body?.tasks ?? [])
|
|
254
|
-
.map((x) => ({
|
|
254
|
+
.map((x) => ({
|
|
255
|
+
sessionId: (x?.sessionId ?? '').trim(),
|
|
256
|
+
taskId: (x?.taskId ?? '').trim(),
|
|
257
|
+
// One attempt of the task rather than the task as it is now; see `exportBundle`.
|
|
258
|
+
...(Number.isInteger(x?.attempt) ? { attempt: x.attempt } : {}),
|
|
259
|
+
}))
|
|
255
260
|
.filter((x) => x.sessionId && x.taskId);
|
|
256
261
|
try {
|
|
257
262
|
const { fileName, content } = await this.ops.exportBundle(pairs);
|
|
@@ -23,7 +23,7 @@ import { newId, tidyVcsPlan } from '../session/model.js';
|
|
|
23
23
|
import { runSession, openBrowser } from '../orchestrator/taskRunner.js';
|
|
24
24
|
import { buildExport } from '../session/exportRecord.js';
|
|
25
25
|
import { buildDebugExport } from '../session/debugExport.js';
|
|
26
|
-
import { buildPlanExport, buildDomainExport, buildBotExport, buildBundleExport, exportFileName } from '../session/exports.js';
|
|
26
|
+
import { buildPlanExport, buildDomainExport, buildBotExport, buildBundleExport, exportFileName, exportMachine, taskAtAttempt, withTask, writeAttemptRecord } from '../session/exports.js';
|
|
27
27
|
import { suggestRunName } from '../session/runName.js';
|
|
28
28
|
import { mirrorAllProjects, removeProjectMirrors, contextRoot } from '../context/desktopMirror.js';
|
|
29
29
|
import { describeMirror } from '../context/projectMirror.js';
|
|
@@ -36,7 +36,7 @@ function sameFolder(a, b) {
|
|
|
36
36
|
return a.trim() !== '' && norm(a) === norm(b);
|
|
37
37
|
}
|
|
38
38
|
import { importPlan, plannedSessionSignature, taskSignature } from '../plan/importPlan.js';
|
|
39
|
-
import { vcsPreflight, restorePreview, restoreToBase, sessionBranches } from '../vcs/taskVcs.js';
|
|
39
|
+
import { commitInterrupted, vcsPreflight, restorePreview, restoreToBase, sessionBranches } from '../vcs/taskVcs.js';
|
|
40
40
|
import { gitAvailable, repoUnusableReason } from '../vcs/git.js';
|
|
41
41
|
import { makeAuthorizer } from '../exec/authorizer.js';
|
|
42
42
|
import { listSelectableDirs, collectFiles, findSelectionConflicts, describeConflicts, DEFAULT_IGNORE_DIRS } from '../context/projectMirror.js';
|
|
@@ -81,6 +81,9 @@ let OperatorService = class OperatorService {
|
|
|
81
81
|
level: 'warn',
|
|
82
82
|
message: `"${r.title}" was left unfinished by an earlier run and has been marked aborted`,
|
|
83
83
|
});
|
|
84
|
+
// What it had changed goes onto its branch, and its record into its run folder, as a
|
|
85
|
+
// finished task's would. See `commitInterrupted` and `writeAttemptRecord`.
|
|
86
|
+
await this.settleRecovered(r.sessionId, r.taskId).catch((e) => console.warn(`[api] could not settle "${r.title}": ${e.message}`));
|
|
84
87
|
}
|
|
85
88
|
if (recovered.length > 0) {
|
|
86
89
|
console.log(`[api] closed ${recovered.length} task(s) left unfinished by an earlier run`);
|
|
@@ -88,6 +91,31 @@ let OperatorService = class OperatorService {
|
|
|
88
91
|
})();
|
|
89
92
|
return this.ready;
|
|
90
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Closes the loose ends of one task the previous process left unfinished: commits its work on its
|
|
96
|
+
* branch, says so on the task, and writes the attempt's plan, work and runner into its run folder.
|
|
97
|
+
*/
|
|
98
|
+
async settleRecovered(sessionId, taskId) {
|
|
99
|
+
const session = await this.store.getSession(sessionId);
|
|
100
|
+
const task = session?.tasks.find((t) => t.id === taskId);
|
|
101
|
+
if (!session || !task)
|
|
102
|
+
return;
|
|
103
|
+
const vcs = await commitInterrupted(session, task, task.reason ?? 'interrupted', this.bus);
|
|
104
|
+
const settled = vcs?.commit
|
|
105
|
+
? await this.store.updateSession(sessionId, (s) => {
|
|
106
|
+
const t = s.tasks.find((x) => x.id === taskId);
|
|
107
|
+
if (!t)
|
|
108
|
+
return;
|
|
109
|
+
t.vcs = vcs;
|
|
110
|
+
t.reason = `${t.reason ?? ''} What it had changed is committed on ${vcs.branch} as ${vcs.commit?.slice(0, 8)}.`.trim();
|
|
111
|
+
})
|
|
112
|
+
: session;
|
|
113
|
+
const ended = settled.tasks.find((t) => t.id === taskId);
|
|
114
|
+
if (!ended)
|
|
115
|
+
return;
|
|
116
|
+
const cfg = await this.settings.load();
|
|
117
|
+
await writeAttemptRecord(settled, ended, cfg.resolved.runsDir, exportMachine(cfg));
|
|
118
|
+
}
|
|
91
119
|
/** Runs the startup work now, so it does not wait for the first request. */
|
|
92
120
|
bootstrap() {
|
|
93
121
|
return this.init();
|
|
@@ -498,12 +526,7 @@ let OperatorService = class OperatorService {
|
|
|
498
526
|
? buildPlanExport(scope)
|
|
499
527
|
: kind === 'domain'
|
|
500
528
|
? await buildDomainExport(scope, cfg.resolved.runsDir)
|
|
501
|
-
: await buildBotExport(scope, cfg.resolved.runsDir,
|
|
502
|
-
node: process.versions.node,
|
|
503
|
-
platform: process.platform,
|
|
504
|
-
cwd: cfg.resolved.cwd,
|
|
505
|
-
limits: { ...cfg.limits, execution: { commandTimeoutSec: cfg.execution.commandTimeoutSec, idleTimeoutSec: cfg.execution.idleTimeoutSec, mode: cfg.execution.mode } },
|
|
506
|
-
});
|
|
529
|
+
: await buildBotExport(scope, cfg.resolved.runsDir, exportMachine(cfg));
|
|
507
530
|
return { fileName: exportFileName(kind, scope.label), content: JSON.stringify(document, null, 2) };
|
|
508
531
|
}
|
|
509
532
|
/**
|
|
@@ -525,29 +548,39 @@ let OperatorService = class OperatorService {
|
|
|
525
548
|
set.add(taskId);
|
|
526
549
|
wanted.set(sessionId, set);
|
|
527
550
|
}
|
|
551
|
+
/*
|
|
552
|
+
* A pair may name one attempt of its task — the button beside an earlier attempt does. That
|
|
553
|
+
* task is then exported as it stood for that attempt (`taskAtAttempt`): its text, its outcome,
|
|
554
|
+
* its run folder, and only the attempts before it.
|
|
555
|
+
*/
|
|
528
556
|
const sessions = [];
|
|
529
557
|
for (const sessionId of wanted.keys()) {
|
|
530
|
-
|
|
531
|
-
if (session)
|
|
532
|
-
|
|
558
|
+
let session = await this.store.getSession(sessionId);
|
|
559
|
+
if (!session)
|
|
560
|
+
continue;
|
|
561
|
+
for (const pair of pairs.filter((x) => x.sessionId === sessionId && x.attempt !== undefined)) {
|
|
562
|
+
const task = session.tasks.find((t) => t.id === pair.taskId);
|
|
563
|
+
const asItWas = task ? taskAtAttempt(task, pair.attempt) : null;
|
|
564
|
+
if (task && !asItWas)
|
|
565
|
+
throw new Error(`"${task.title}" has no attempt ${pair.attempt}.`);
|
|
566
|
+
if (asItWas)
|
|
567
|
+
session = withTask(session, asItWas);
|
|
568
|
+
}
|
|
569
|
+
sessions.push(session);
|
|
533
570
|
}
|
|
534
571
|
const found = sessions.flatMap((s) => s.tasks.filter((t) => wanted.get(s.id)?.has(t.id)));
|
|
535
572
|
if (found.length === 0)
|
|
536
573
|
throw new Error('None of the chosen tasks still exists.');
|
|
537
574
|
// Named after what was chosen rather than after a run, because a selection is not a run: it
|
|
538
575
|
// may be one task of one, or a task from each of three.
|
|
576
|
+
const one = pairs.length === 1 ? pairs[0] : undefined;
|
|
539
577
|
const label = found.length === 1 && sessions.length === 1
|
|
540
|
-
? `${sessions[0].name}-${found[0].title}`
|
|
578
|
+
? `${sessions[0].name}-${found[0].title}${one?.attempt !== undefined ? `-attempt-${one.attempt}` : ''}`
|
|
541
579
|
: sessions.length === 1
|
|
542
580
|
? `${sessions[0].name}-${found.length}-tasks`
|
|
543
581
|
: `${found.length}-tasks-${sessions.length}-sessions`;
|
|
544
582
|
const cfg = await this.settings.load();
|
|
545
|
-
const document = await buildBundleExport({ sessions, taskFilter: (s, t) => !!wanted.get(s.id)?.has(t.id), label }, cfg.resolved.runsDir,
|
|
546
|
-
node: process.versions.node,
|
|
547
|
-
platform: process.platform,
|
|
548
|
-
cwd: cfg.resolved.cwd,
|
|
549
|
-
limits: { ...cfg.limits, execution: { commandTimeoutSec: cfg.execution.commandTimeoutSec, idleTimeoutSec: cfg.execution.idleTimeoutSec, mode: cfg.execution.mode } },
|
|
550
|
-
});
|
|
583
|
+
const document = await buildBundleExport({ sessions, taskFilter: (s, t) => !!wanted.get(s.id)?.has(t.id), label }, cfg.resolved.runsDir, exportMachine(cfg));
|
|
551
584
|
return { fileName: exportFileName('bundle', label), content: JSON.stringify(document, null, 2) };
|
|
552
585
|
}
|
|
553
586
|
// --- running --------------------------------------------------------------------------
|
|
@@ -618,6 +651,8 @@ let OperatorService = class OperatorService {
|
|
|
618
651
|
allowedPrograms: cfg.execution.allowedPrograms,
|
|
619
652
|
isolation: cfg.execution.isolation,
|
|
620
653
|
lockedToConfirm: cfg.policyLock?.maxMode === 'confirm',
|
|
654
|
+
// Read once per run like the rest of the policy: a change in Settings applies from the next start.
|
|
655
|
+
networkFetch: cfg.execution.networkFetch,
|
|
621
656
|
};
|
|
622
657
|
/*
|
|
623
658
|
* An unattended run that cannot legally execute anything is stopped here, where the operator
|
|
@@ -716,7 +751,7 @@ let OperatorService = class OperatorService {
|
|
|
716
751
|
r.controller.abort();
|
|
717
752
|
for (const [id, w] of this.waiting) {
|
|
718
753
|
if (w.approval.sessionId === sessionId) {
|
|
719
|
-
w.resolve({ action: 'abort', reason: 'stopped by the operator' });
|
|
754
|
+
w.resolve({ action: 'abort', reason: 'stopped by the operator', by: 'operator' });
|
|
720
755
|
this.waiting.delete(id);
|
|
721
756
|
}
|
|
722
757
|
}
|
|
@@ -1273,8 +1308,8 @@ let OperatorService = class OperatorService {
|
|
|
1273
1308
|
const decision = action === 'run' || action === 'run-all'
|
|
1274
1309
|
? { action: 'run' }
|
|
1275
1310
|
: action === 'skip'
|
|
1276
|
-
? { action: 'skip', reason: 'skipped by the operator' }
|
|
1277
|
-
: { action: 'abort', reason: 'aborted by the operator' };
|
|
1311
|
+
? { action: 'skip', reason: 'skipped by the operator', by: 'operator' }
|
|
1312
|
+
: { action: 'abort', reason: 'aborted by the operator', by: 'operator' };
|
|
1278
1313
|
w.resolve(decision);
|
|
1279
1314
|
this.bus.publish({ sessionId: w.approval.sessionId, taskId: w.approval.taskId, type: 'approval-decided', level: 'info',
|
|
1280
1315
|
message: `step ${w.approval.stepId}: ${action}`, data: { approvalId, action } });
|
|
@@ -1357,7 +1392,7 @@ let OperatorService = class OperatorService {
|
|
|
1357
1392
|
...(held ? { network: held.network } : {}),
|
|
1358
1393
|
};
|
|
1359
1394
|
if (signal.aborted) {
|
|
1360
|
-
resolvePromise({ action: 'abort', reason: 'stopped by the operator' });
|
|
1395
|
+
resolvePromise({ action: 'abort', reason: 'stopped by the operator', by: 'operator' });
|
|
1361
1396
|
return;
|
|
1362
1397
|
}
|
|
1363
1398
|
this.waiting.set(approval.id, { approval, resolve: resolvePromise });
|
package/dist/src/cli.js
CHANGED
|
@@ -484,6 +484,7 @@ program
|
|
|
484
484
|
allowedPrograms: cfg.execution.allowedPrograms,
|
|
485
485
|
isolation: cfg.execution.isolation,
|
|
486
486
|
lockedToConfirm: cfg.policyLock?.maxMode === 'confirm',
|
|
487
|
+
networkFetch: cfg.execution.networkFetch,
|
|
487
488
|
};
|
|
488
489
|
const authorizer = cfg.execution.mode === 'unattended'
|
|
489
490
|
? unattendedAuthorizer(policy)
|
|
@@ -117,7 +117,32 @@ export const RunConfigSchema = z.object({
|
|
|
117
117
|
task: TextOrFile.optional(),
|
|
118
118
|
execution: z
|
|
119
119
|
.object({
|
|
120
|
+
/*
|
|
121
|
+
* For the terminal, the mode `cop run` uses without `--unattended`. For the API it is the
|
|
122
|
+
* operator's standing choice from Settings: `unattended` means an unattended start is not
|
|
123
|
+
* questioned again with "are you sure" — they said so once, on the Settings page, and asking
|
|
124
|
+
* at every press is exactly the interruption they switched off. Every start still names its
|
|
125
|
+
* own mode; this never turns a step-by-step press into an unattended run.
|
|
126
|
+
*/
|
|
120
127
|
mode: z.enum(['confirm', 'unattended']).default('confirm'),
|
|
128
|
+
/**
|
|
129
|
+
* What an unattended run does with a step that fetches from the network. See `network.ts`.
|
|
130
|
+
*
|
|
131
|
+
* ask it waits on the approval screen for the operator, as it did before this setting
|
|
132
|
+
* existed — the one question an unattended run still asks.
|
|
133
|
+
* refuse it is not run, and the chat is told why at once and what to do instead, so the
|
|
134
|
+
* run never stops to wait for anybody. The terminal's unattended form has always
|
|
135
|
+
* done this, because it has nobody to ask.
|
|
136
|
+
* run it runs like every other step, with nobody having read it.
|
|
137
|
+
*
|
|
138
|
+
* `ask` is the default because the hold exists for a reason — a security team found
|
|
139
|
+
* model-written steps staging and running code on a work laptop — and a run that can fetch
|
|
140
|
+
* and then execute what arrived, unread, is the thing it was built to prevent. `run` is the
|
|
141
|
+
* operator's to choose on a machine where that is acceptable, and it is recorded in every
|
|
142
|
+
* task's policy manifest as exactly that. A step-by-step run is unaffected: every step
|
|
143
|
+
* there, a fetch included, is put in front of a person anyway.
|
|
144
|
+
*/
|
|
145
|
+
networkFetch: z.enum(['ask', 'refuse', 'run']).default('ask'),
|
|
121
146
|
/**
|
|
122
147
|
* The shell a step or a check runs in when it names none of its own.
|
|
123
148
|
*
|
|
@@ -6,11 +6,14 @@ import { stdin, stdout } from 'node:process';
|
|
|
6
6
|
/**
|
|
7
7
|
* Runs the static rules, then, unless unattended, asks the human through `ask`.
|
|
8
8
|
*
|
|
9
|
-
* A step that fetches from the network is the exception to "unless unattended": it is
|
|
10
|
-
* in every mode, with `held` saying why, because the runs nobody is watching are
|
|
11
|
-
* in which a download followed by a second step that runs it would otherwise go
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* A step that fetches from the network is the exception to "unless unattended": by default it is
|
|
10
|
+
* asked about in every mode, with `held` saying why, because the runs nobody is watching are
|
|
11
|
+
* exactly the ones in which a download followed by a second step that runs it would otherwise go
|
|
12
|
+
* unseen. The operator may choose otherwise for unattended runs (`cfg.networkFetch`): `refuse`
|
|
13
|
+
* answers the chat at once instead of waiting for anybody, `run` treats the fetch like any other
|
|
14
|
+
* step. Only for unattended runs — a watched run shows the fetch like every other step, and the
|
|
15
|
+
* person reading it is the whole point of watching. `cfg` is read on every call, not captured, so
|
|
16
|
+
* a run switched to unattended half-way is judged as one from its next step on.
|
|
14
17
|
*/
|
|
15
18
|
export function makeAuthorizer(cfg, ask) {
|
|
16
19
|
return {
|
|
@@ -20,8 +23,13 @@ export function makeAuthorizer(cfg, ask) {
|
|
|
20
23
|
return blocked;
|
|
21
24
|
const cwd = ctx.confinement?.cwd;
|
|
22
25
|
const network = networkFetchReason(step.cmd, cwd) ?? (cwd ? scriptNetworkReason(step.cmd, cwd) : null);
|
|
23
|
-
if (network)
|
|
26
|
+
if (network) {
|
|
27
|
+
if (cfg.mode === 'unattended' && cfg.networkFetch === 'refuse')
|
|
28
|
+
return { action: 'skip', reason: networkFetchRefusal(network) };
|
|
29
|
+
if (cfg.mode === 'unattended' && cfg.networkFetch === 'run')
|
|
30
|
+
return { action: 'run' };
|
|
24
31
|
return await ask(step, ctx, { network });
|
|
32
|
+
}
|
|
25
33
|
if (cfg.mode === 'unattended')
|
|
26
34
|
return { action: 'run' };
|
|
27
35
|
return await ask(step, ctx);
|
|
@@ -39,9 +47,9 @@ export function terminalAuthorizer(cfg, print) {
|
|
|
39
47
|
try {
|
|
40
48
|
const answer = (await rl.question(' [Enter] run [s] skip [q] abort > ')).trim().toLowerCase();
|
|
41
49
|
if (answer === 'q')
|
|
42
|
-
return { action: 'abort', reason: 'aborted by the operator' };
|
|
50
|
+
return { action: 'abort', reason: 'aborted by the operator', by: 'operator' };
|
|
43
51
|
if (answer === 's')
|
|
44
|
-
return { action: 'skip', reason: 'skipped by the operator' };
|
|
52
|
+
return { action: 'skip', reason: 'skipped by the operator', by: 'operator' };
|
|
45
53
|
return { action: 'run' };
|
|
46
54
|
}
|
|
47
55
|
finally {
|
|
@@ -53,8 +61,10 @@ export function terminalAuthorizer(cfg, print) {
|
|
|
53
61
|
* Everything runs after the static rules. Only for runs the user explicitly marked so.
|
|
54
62
|
*
|
|
55
63
|
* A step held for a person (a network fetch) is refused here, with the reason, because this form
|
|
56
|
-
* has nobody to ask
|
|
57
|
-
*
|
|
64
|
+
* has nobody to ask — `ask` and `refuse` come to the same thing at a terminal nobody is at. Only
|
|
65
|
+
* `networkFetch: run` lets one through, and that is decided in `makeAuthorizer` before this is
|
|
66
|
+
* reached. The web UI does not use it: its unattended runs keep the approval screen for exactly
|
|
67
|
+
* those steps.
|
|
58
68
|
*/
|
|
59
69
|
export function unattendedAuthorizer(cfg) {
|
|
60
70
|
return makeAuthorizer({ ...cfg, mode: 'unattended' }, async (_step, _ctx, held) => held ? { action: 'skip', reason: networkFetchRefusal(held.network) } : { action: 'run' });
|