@skrr-ai/cli 0.1.50 → 0.1.51
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/base-command.js +59 -2
- package/dist/commands/agents/chat.d.ts +14 -0
- package/dist/commands/agents/chat.js +37 -1
- package/dist/commands/agents/show.d.ts +9 -0
- package/dist/commands/agents/show.js +53 -0
- package/dist/commands/agents/update.d.ts +9 -0
- package/dist/commands/agents/update.js +27 -0
- package/dist/commands/balance/show.d.ts +34 -0
- package/dist/commands/balance/show.js +92 -2
- package/dist/commands/commitments/doctor.d.ts +1 -0
- package/dist/commands/commitments/doctor.js +19 -4
- package/dist/commands/commitments/effective-policy.js +15 -1
- package/dist/commands/commitments/next-tick.js +3 -1
- package/dist/commands/commitments/preflight.js +10 -0
- package/dist/commands/commitments/show.js +11 -1
- package/dist/commands/daemon/config.d.ts +15 -0
- package/dist/commands/daemon/config.js +38 -0
- package/dist/commands/daemon/index.js +1 -0
- package/dist/commands/daemon/status.js +15 -1
- package/dist/commands/harnesses/models.d.ts +54 -0
- package/dist/commands/harnesses/models.js +100 -7
- package/dist/commands/harnesses/show.d.ts +11 -0
- package/dist/commands/harnesses/show.js +35 -0
- package/dist/commands/harnesses/update.d.ts +28 -1
- package/dist/commands/harnesses/update.js +73 -10
- package/dist/commands/outbox/list.d.ts +12 -1
- package/dist/commands/outbox/list.js +31 -2
- package/dist/commands/revoke-daemon.js +9 -0
- package/dist/commands/spaces/create.d.ts +1 -0
- package/dist/commands/spaces/create.js +17 -0
- package/dist/commands/spaces/show.js +5 -0
- package/dist/commands/spaces/update.d.ts +1 -0
- package/dist/commands/spaces/update.js +16 -0
- package/dist/commands/tasks/create.d.ts +1 -0
- package/dist/commands/tasks/create.js +5 -0
- package/dist/commands/tasks/show.js +45 -0
- package/dist/commands/tasks/update.d.ts +1 -0
- package/dist/commands/tasks/update.js +21 -0
- package/dist/commands/tasks/upsert.d.ts +1 -0
- package/dist/commands/tasks/upsert.js +16 -0
- package/dist/lib/agentic-stream.d.ts +68 -1
- package/dist/lib/agentic-stream.js +294 -7
- package/dist/lib/auth-core-init.d.ts +2 -10
- package/dist/lib/auth-core-init.js +20 -67
- package/dist/lib/auth-failure.d.ts +33 -0
- package/dist/lib/auth-failure.js +65 -0
- package/dist/lib/commitments.d.ts +21 -1
- package/dist/lib/commitments.js +34 -0
- package/dist/lib/daemonBroker.d.ts +14 -0
- package/dist/lib/daemonBroker.js +64 -12
- package/dist/lib/delegated-cli.js +4 -1
- package/dist/lib/exec-runtime-binary.d.ts +3 -1
- package/dist/lib/exec-runtime-binary.js +4 -2
- package/dist/lib/first-party-harness-broker.js +4 -1
- package/dist/lib/harnesses.d.ts +10 -0
- package/dist/lib/live-runtime-binary.d.ts +40 -0
- package/dist/lib/live-runtime-binary.js +193 -0
- package/dist/lib/login.d.ts +23 -0
- package/dist/lib/login.js +44 -10
- package/dist/lib/message-intent.js +3 -1
- package/dist/lib/tasks.d.ts +20 -0
- package/dist/lib/tasks.js +54 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +28 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +23 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +8 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +28 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +22 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +8 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +1928 -1924
- package/oclif.manifest.json +23832 -23688
- package/package.json +1 -1
|
@@ -2,17 +2,22 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AgenticStreamClient = exports.USER_CONNECTION_CAP_CODE = exports.NON_INTERACTIVE_QUESTION_DISMISSAL = void 0;
|
|
4
4
|
exports.writeJsonLine = writeJsonLine;
|
|
5
|
+
exports.describePermissionTarget = describePermissionTarget;
|
|
5
6
|
exports.envelopesText = envelopesText;
|
|
6
7
|
exports.envelopeKey = envelopeKey;
|
|
7
8
|
exports.unseenEnvelopes = unseenEnvelopes;
|
|
8
9
|
exports.projectDaemonToolEvents = projectDaemonToolEvents;
|
|
10
|
+
exports.describePermissionDecider = describePermissionDecider;
|
|
11
|
+
exports.projectPermissionResolutions = projectPermissionResolutions;
|
|
9
12
|
exports.renderableDelta = renderableDelta;
|
|
10
13
|
exports.transportRefusal = transportRefusal;
|
|
11
14
|
exports.transportRefusalMessage = transportRefusalMessage;
|
|
12
15
|
exports.attachDeadlineFor = attachDeadlineFor;
|
|
13
16
|
exports.jitteredRetryDelay = jitteredRetryDelay;
|
|
17
|
+
exports.projectSessionNotices = projectSessionNotices;
|
|
14
18
|
exports.detectSeqGap = detectSeqGap;
|
|
15
19
|
exports.seqGapNotice = seqGapNotice;
|
|
20
|
+
exports.describeRunPermission = describeRunPermission;
|
|
16
21
|
exports.toWebSocketOrigin = toWebSocketOrigin;
|
|
17
22
|
const node_readline_1 = require("node:readline");
|
|
18
23
|
const socket_io_client_1 = require("socket.io-client");
|
|
@@ -48,6 +53,71 @@ exports.NON_INTERACTIVE_QUESTION_DISMISSAL = 'No answer: this turn was started n
|
|
|
48
53
|
function writeJsonLine(output, value) {
|
|
49
54
|
output.write(`${JSON.stringify(value) ?? 'null'}\n`);
|
|
50
55
|
}
|
|
56
|
+
const PERMISSION_DETAIL_MAX = 400;
|
|
57
|
+
/**
|
|
58
|
+
* What a permission request would DO, in one line a person can judge.
|
|
59
|
+
*
|
|
60
|
+
* The prompt used to be `Allow tool <name>?` and nothing else, so a shell
|
|
61
|
+
* approval read "Allow tool unknown? [y/N]" and a person approved a command
|
|
62
|
+
* they never saw (OSK-12119). The command, or else the path, or else the
|
|
63
|
+
* arguments, is the part that decides the answer.
|
|
64
|
+
*
|
|
65
|
+
* The text is chosen by the agent and printed to a terminal, so control
|
|
66
|
+
* characters are escaped: an embedded carriage return or escape sequence could
|
|
67
|
+
* otherwise redraw the line and show the person a different command than the
|
|
68
|
+
* one they are approving.
|
|
69
|
+
*/
|
|
70
|
+
function describePermissionTarget(toolInput) {
|
|
71
|
+
if (!toolInput || typeof toolInput !== 'object' || Array.isArray(toolInput))
|
|
72
|
+
return null;
|
|
73
|
+
const input = toolInput;
|
|
74
|
+
const pick = (...keys) => {
|
|
75
|
+
for (const key of keys) {
|
|
76
|
+
const value = input[key];
|
|
77
|
+
if (typeof value === 'string' && value.trim())
|
|
78
|
+
return value;
|
|
79
|
+
if (Array.isArray(value) && value.length > 0 && value.every((v) => typeof v === 'string')) {
|
|
80
|
+
return value.join(' ');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
};
|
|
85
|
+
let detail = pick('command', 'cmd', 'script');
|
|
86
|
+
if (!detail) {
|
|
87
|
+
const target = pick('file_path', 'filePath', 'path', 'url');
|
|
88
|
+
if (target)
|
|
89
|
+
detail = target;
|
|
90
|
+
}
|
|
91
|
+
if (!detail) {
|
|
92
|
+
if (Object.keys(input).length === 0)
|
|
93
|
+
return null;
|
|
94
|
+
try {
|
|
95
|
+
detail = JSON.stringify(input);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
let escaped = '';
|
|
102
|
+
for (const c of detail) {
|
|
103
|
+
const code = c.charCodeAt(0);
|
|
104
|
+
const control = code < 0x20 || (code >= 0x7f && code <= 0x9f);
|
|
105
|
+
if (!control)
|
|
106
|
+
escaped += c;
|
|
107
|
+
else
|
|
108
|
+
escaped += c === '\n' ? '\\n' : `\\x${code.toString(16).padStart(2, '0')}`;
|
|
109
|
+
}
|
|
110
|
+
return escaped.length > PERMISSION_DETAIL_MAX
|
|
111
|
+
? `${escaped.slice(0, PERMISSION_DETAIL_MAX)}… (${escaped.length - PERMISSION_DETAIL_MAX} more characters)`
|
|
112
|
+
: escaped;
|
|
113
|
+
}
|
|
114
|
+
function describePermission(event) {
|
|
115
|
+
// Permission events carry toolName/toolInput beyond the base event type.
|
|
116
|
+
const fields = event;
|
|
117
|
+
const name = String(fields.toolName ?? 'unknown');
|
|
118
|
+
const detail = describePermissionTarget(fields.toolInput);
|
|
119
|
+
return detail ? `${name}: ${detail}` : name;
|
|
120
|
+
}
|
|
51
121
|
/**
|
|
52
122
|
* Small Node-side adapter for the existing Agentic Socket.IO contract.
|
|
53
123
|
* It deliberately normalizes only lifecycle/text/tool events; the server's
|
|
@@ -179,6 +249,82 @@ function projectDaemonToolEvents(envelopes, namesByCall) {
|
|
|
179
249
|
}
|
|
180
250
|
return out;
|
|
181
251
|
}
|
|
252
|
+
const SURFACE_LABELS = {
|
|
253
|
+
web: 'the web app',
|
|
254
|
+
cli: 'the skrr CLI',
|
|
255
|
+
desktop: 'the desktop app',
|
|
256
|
+
mobile: 'the mobile app',
|
|
257
|
+
};
|
|
258
|
+
/** `by` alone, for a daemon that predates `basis` — said no more precisely than it knows. */
|
|
259
|
+
const BY_LABELS = {
|
|
260
|
+
cockpit: 'an answer relayed by the server',
|
|
261
|
+
avatar: 'a person on the desktop app',
|
|
262
|
+
timeout: 'nobody — the request timed out',
|
|
263
|
+
server: 'the server (the run is unattended, so nobody could be asked)',
|
|
264
|
+
cli: 'the harness side, without asking anyone',
|
|
265
|
+
cache: 'an earlier decision for this session',
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* WHO or WHAT decided a permission request, as a reader needs it (OSK-12113).
|
|
269
|
+
*
|
|
270
|
+
* `by` names the surface or fallback that resolved the ask and is a closed wire
|
|
271
|
+
* enum; `basis` beside it says what decided. The distinction is the finding: an
|
|
272
|
+
* edit acceptEdits applied on its own was recorded `by: "cli"`, which read as
|
|
273
|
+
* the CLI user having approved something nobody was asked about.
|
|
274
|
+
*/
|
|
275
|
+
function describePermissionDecider(ev) {
|
|
276
|
+
const mode = typeof ev.mode === 'string' && ev.mode ? ev.mode : '';
|
|
277
|
+
switch (ev.basis) {
|
|
278
|
+
case 'mode':
|
|
279
|
+
return mode
|
|
280
|
+
? `the ${mode} permission mode (nobody was asked)`
|
|
281
|
+
: "the session's permission mode (nobody was asked)";
|
|
282
|
+
case 'policy':
|
|
283
|
+
return ev.by === 'server'
|
|
284
|
+
? 'the unattended-run policy (nobody could be asked)'
|
|
285
|
+
: 'a runtime policy (nobody was asked)';
|
|
286
|
+
case 'session-grant':
|
|
287
|
+
return 'an earlier "allow for this session"';
|
|
288
|
+
case 'person': {
|
|
289
|
+
const where = typeof ev.surface === 'string' ? SURFACE_LABELS[ev.surface] : undefined;
|
|
290
|
+
return where ? `a person on ${where}` : 'a person';
|
|
291
|
+
}
|
|
292
|
+
default:
|
|
293
|
+
return (typeof ev.by === 'string' && BY_LABELS[ev.by]) || 'an unrecorded decider';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Daemon `permission-resolved` envelopes as `permission.resolved` events, named
|
|
298
|
+
* by the tool their `permission-ask` announced and carrying who decided.
|
|
299
|
+
*/
|
|
300
|
+
function projectPermissionResolutions(envelopes, toolsByRequest) {
|
|
301
|
+
const out = [];
|
|
302
|
+
for (const envelope of envelopes) {
|
|
303
|
+
const ev = envelope?.ev;
|
|
304
|
+
if (!ev || typeof ev.requestId !== 'string')
|
|
305
|
+
continue;
|
|
306
|
+
if (ev.t === 'permission-ask') {
|
|
307
|
+
if (typeof ev.toolName === 'string')
|
|
308
|
+
toolsByRequest.set(ev.requestId, ev.toolName);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (ev.t !== 'permission-resolved')
|
|
312
|
+
continue;
|
|
313
|
+
const toolName = toolsByRequest.get(ev.requestId);
|
|
314
|
+
out.push({
|
|
315
|
+
type: 'permission.resolved',
|
|
316
|
+
requestId: ev.requestId,
|
|
317
|
+
...(toolName ? { toolName } : {}),
|
|
318
|
+
decision: ev.decision,
|
|
319
|
+
by: ev.by,
|
|
320
|
+
...(typeof ev.basis === 'string' ? { basis: ev.basis } : {}),
|
|
321
|
+
...(typeof ev.mode === 'string' ? { mode: ev.mode } : {}),
|
|
322
|
+
...(typeof ev.surface === 'string' ? { surface: ev.surface } : {}),
|
|
323
|
+
decidedBy: describePermissionDecider(ev),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
182
328
|
/**
|
|
183
329
|
* The part of a cumulative snapshot that has not been shown yet.
|
|
184
330
|
*
|
|
@@ -271,6 +417,23 @@ function attachDeadlineFor(timeoutMs, refusal) {
|
|
|
271
417
|
function jitteredRetryDelay(retryAfterMs, random = Math.random) {
|
|
272
418
|
return Math.round(retryAfterMs * (1 + random() * 0.25));
|
|
273
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* `service` envelopes that carry a `notice` — words for the person, not
|
|
422
|
+
* diagnostics — projected as `session.notice` events. Today the only notice is
|
|
423
|
+
* `harness-session-reset`: the local harness could not resume its previous
|
|
424
|
+
* session and began a new one, so its local working context is gone.
|
|
425
|
+
*/
|
|
426
|
+
function projectSessionNotices(envelopes) {
|
|
427
|
+
const out = [];
|
|
428
|
+
for (const envelope of envelopes) {
|
|
429
|
+
const ev = envelope?.ev;
|
|
430
|
+
if (ev?.t !== 'service' || typeof ev.notice !== 'string' || typeof ev.text !== 'string') {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
out.push({ type: 'session.notice', notice: ev.notice, message: ev.text });
|
|
434
|
+
}
|
|
435
|
+
return out;
|
|
436
|
+
}
|
|
274
437
|
function detectSeqGap(highestApplied, incoming) {
|
|
275
438
|
if (!Number.isFinite(incoming) || incoming <= 0)
|
|
276
439
|
return null;
|
|
@@ -281,11 +444,54 @@ function detectSeqGap(highestApplied, incoming) {
|
|
|
281
444
|
return null;
|
|
282
445
|
return { from: highestApplied + 1, to: incoming - 1 };
|
|
283
446
|
}
|
|
284
|
-
|
|
447
|
+
/**
|
|
448
|
+
* The notice a reader sees when chunks went missing.
|
|
449
|
+
*
|
|
450
|
+
* It names `run.result` only when this client will actually fill `run.result`
|
|
451
|
+
* from the persisted message (`recordBackfill`). The sentence used to promise
|
|
452
|
+
* it unconditionally, while `run.result.text` was folded from the envelopes
|
|
453
|
+
* that DID arrive — so the one run told to trust `run.result` was the run whose
|
|
454
|
+
* `run.result.text` was empty, with the answer only in the record (OSK-12132).
|
|
455
|
+
*/
|
|
456
|
+
function seqGapNotice(gap, options = {}) {
|
|
285
457
|
const count = gap.to - gap.from + 1;
|
|
458
|
+
const where = options.recordBackfill === false
|
|
459
|
+
? 'The full answer is in the persisted message.'
|
|
460
|
+
: 'The full answer is in the persisted message and in run.result.';
|
|
286
461
|
return (`[${count} stream chunk${count === 1 ? '' : 's'} (seq ${gap.from}` +
|
|
287
462
|
`${count === 1 ? '' : `-${gap.to}`}) never arrived — the text above is incomplete. ` +
|
|
288
|
-
|
|
463
|
+
`${where}]`);
|
|
464
|
+
}
|
|
465
|
+
/** How long, at most, a gapped run waits for the record before settling anyway. */
|
|
466
|
+
const GAP_BACKFILL_ATTEMPTS = 3;
|
|
467
|
+
const GAP_BACKFILL_RETRY_MS = 400;
|
|
468
|
+
const PERMISSION_SOURCE_LABELS = {
|
|
469
|
+
request: 'this turn',
|
|
470
|
+
agent: "the agent's default",
|
|
471
|
+
harness: "the harness's default",
|
|
472
|
+
contract: 'the execution contract',
|
|
473
|
+
policy: 'the unattended-run policy',
|
|
474
|
+
};
|
|
475
|
+
/**
|
|
476
|
+
* The permission mode a `run.started` reports, and where it came from
|
|
477
|
+
* (OSK-12089). `null` when the event carries no mode report at all — a cloud
|
|
478
|
+
* turn from an older server, say — so nothing is claimed.
|
|
479
|
+
*/
|
|
480
|
+
function describeRunPermission(event) {
|
|
481
|
+
const source = typeof event.permissionModeSource === 'string' ? event.permissionModeSource : '';
|
|
482
|
+
const mode = typeof event.permissionMode === 'string' && event.permissionMode ? event.permissionMode : '';
|
|
483
|
+
if (!source)
|
|
484
|
+
return null;
|
|
485
|
+
if (!mode) {
|
|
486
|
+
return source === 'daemon'
|
|
487
|
+
? "set by the daemon's own config on that machine (no agent, harness or turn mode is set)"
|
|
488
|
+
: 'unknown';
|
|
489
|
+
}
|
|
490
|
+
const from = PERMISSION_SOURCE_LABELS[source] ?? source;
|
|
491
|
+
const coerced = typeof event.permissionModeCoercedFrom === 'string' && event.permissionModeCoercedFrom
|
|
492
|
+
? ` — ${event.permissionModeCoercedFrom} was asked for, which this harness cannot honour`
|
|
493
|
+
: '';
|
|
494
|
+
return `${mode} (from ${from})${coerced}`;
|
|
289
495
|
}
|
|
290
496
|
class AgenticStreamClient {
|
|
291
497
|
options;
|
|
@@ -316,6 +522,8 @@ class AgenticStreamClient {
|
|
|
316
522
|
seenLifecycle = new Set();
|
|
317
523
|
/** `call` → tool name, so a daemon `tool-call-end` can be named (OSK-7730). */
|
|
318
524
|
toolNamesByCall = new Map();
|
|
525
|
+
/** `permission-ask` requestId → tool name, to name its resolution (OSK-12113). */
|
|
526
|
+
toolNamesByPermission = new Map();
|
|
319
527
|
/** Text already written to the terminal, for suffix-only rendering. */
|
|
320
528
|
renderedText = '';
|
|
321
529
|
idleTimer = null;
|
|
@@ -507,7 +715,7 @@ class AgenticStreamClient {
|
|
|
507
715
|
sessionId: this.options.sessionId,
|
|
508
716
|
fromSeq: gap.from,
|
|
509
717
|
toSeq: gap.to,
|
|
510
|
-
message: seqGapNotice(gap),
|
|
718
|
+
message: seqGapNotice(gap, { recordBackfill: Boolean(this.options.reconcileTerminal) }),
|
|
511
719
|
});
|
|
512
720
|
}
|
|
513
721
|
if (seq > this.highestSeq)
|
|
@@ -605,6 +813,18 @@ class AgenticStreamClient {
|
|
|
605
813
|
for (const projected of projectDaemonToolEvents(fresh, this.toolNamesByCall)) {
|
|
606
814
|
this.emitToolEvent({ ...projected, sessionId: this.options.sessionId });
|
|
607
815
|
}
|
|
816
|
+
// Notices meant for the person — a local harness that could not resume
|
|
817
|
+
// its previous session and started a fresh one (OSK-12131). A plain
|
|
818
|
+
// `service` line is diagnostic chatter and stays in the envelope; one
|
|
819
|
+
// marked `notice` is said out loud.
|
|
820
|
+
for (const notice of projectSessionNotices(fresh)) {
|
|
821
|
+
this.emit({ ...notice, sessionId: this.options.sessionId });
|
|
822
|
+
}
|
|
823
|
+
// Who decided each permission request — a person (and where), the
|
|
824
|
+
// mode, a policy — said beside the decision (OSK-12113).
|
|
825
|
+
for (const resolved of projectPermissionResolutions(fresh, this.toolNamesByPermission)) {
|
|
826
|
+
this.emit({ ...resolved, sessionId: this.options.sessionId });
|
|
827
|
+
}
|
|
608
828
|
this.emit({ ...scoped, type: 'session.output' });
|
|
609
829
|
return;
|
|
610
830
|
}
|
|
@@ -731,7 +951,7 @@ class AgenticStreamClient {
|
|
|
731
951
|
});
|
|
732
952
|
return;
|
|
733
953
|
}
|
|
734
|
-
const answer = await this.ask(`Allow
|
|
954
|
+
const answer = await this.ask(`Allow ${describePermission(event)}? [y/N] `);
|
|
735
955
|
const approved = /^y(es)?$/i.test(answer.trim());
|
|
736
956
|
this.socket.emit(approved ? 'permission:approve' : 'permission:deny', {
|
|
737
957
|
sessionId: this.options.sessionId,
|
|
@@ -843,7 +1063,19 @@ class AgenticStreamClient {
|
|
|
843
1063
|
writeJsonLine(output, event);
|
|
844
1064
|
return;
|
|
845
1065
|
}
|
|
846
|
-
if (event.type === '
|
|
1066
|
+
if (event.type === 'run.started') {
|
|
1067
|
+
const permission = describeRunPermission(event);
|
|
1068
|
+
if (permission)
|
|
1069
|
+
output.write(`[run] permission: ${permission}\n`);
|
|
1070
|
+
}
|
|
1071
|
+
else if ((event.type === 'run.completed' || event.type === 'run.failed') &&
|
|
1072
|
+
typeof event.harnessModel === 'string' &&
|
|
1073
|
+
event.harnessModel) {
|
|
1074
|
+
// Which model the harness ran — the agent's pick, or what `default`
|
|
1075
|
+
// resolved to on that machine (OSK-12111).
|
|
1076
|
+
output.write(`[run] model: ${event.harnessModel} (reported by the harness)\n`);
|
|
1077
|
+
}
|
|
1078
|
+
else if (event.type === 'text.delta' && typeof event.text === 'string') {
|
|
847
1079
|
// Print what is NEW. See `renderableDelta` — the old `\r` repaint could
|
|
848
1080
|
// not rewind past a newline, so a multi-line answer was reprinted once
|
|
849
1081
|
// per snapshot and its blocks fused (OSK-7701, OSK-7731).
|
|
@@ -870,18 +1102,31 @@ class AgenticStreamClient {
|
|
|
870
1102
|
output.write(`\n${String(event.message ?? '')}\n`);
|
|
871
1103
|
this.renderedText = this.accumulated;
|
|
872
1104
|
}
|
|
1105
|
+
else if (event.type === 'permission.resolved') {
|
|
1106
|
+
const verb = event.decision === 'allow' ? 'allowed' : 'denied';
|
|
1107
|
+
output.write(`\n[permission] ${String(event.toolName ?? 'tool')} ${verb} by ${String(event.decidedBy ?? '')}\n`);
|
|
1108
|
+
this.renderedText = this.accumulated;
|
|
1109
|
+
}
|
|
1110
|
+
else if (event.type === 'session.notice') {
|
|
1111
|
+
output.write(`\n[session] ${String(event.message ?? '')}\n`);
|
|
1112
|
+
this.renderedText = this.accumulated;
|
|
1113
|
+
}
|
|
873
1114
|
else if (event.type === 'transport.disconnected') {
|
|
874
1115
|
output.write(`\n[reconnecting] ${String(event.reason ?? '')}\n`);
|
|
875
1116
|
this.renderedText = this.accumulated;
|
|
876
1117
|
}
|
|
877
|
-
else if (event.type === 'permission.requested'
|
|
1118
|
+
else if (event.type === 'permission.requested') {
|
|
1119
|
+
output.write(`\n[${event.type}] ${describePermission(event)}\n`);
|
|
1120
|
+
this.renderedText = this.accumulated;
|
|
1121
|
+
}
|
|
1122
|
+
else if (event.type === 'question.requested') {
|
|
878
1123
|
output.write(`\n[${event.type}]\n`);
|
|
879
1124
|
this.renderedText = this.accumulated;
|
|
880
1125
|
}
|
|
881
1126
|
else if (event.type === 'permission.approved' && event.auto === true) {
|
|
882
1127
|
// Said out loud, because nobody was asked: the flag approved it, and a
|
|
883
1128
|
// reader of this log should see which tools that covered (OSK-10323).
|
|
884
|
-
output.write(`\n[permission] ${
|
|
1129
|
+
output.write(`\n[permission] ${describePermission(event)} allowed by --approve-tools\n`);
|
|
885
1130
|
this.renderedText = this.accumulated;
|
|
886
1131
|
}
|
|
887
1132
|
else if (event.type === 'question.answered' && event.nonInteractive) {
|
|
@@ -894,6 +1139,42 @@ class AgenticStreamClient {
|
|
|
894
1139
|
return;
|
|
895
1140
|
this.settled = true;
|
|
896
1141
|
this.lastEvent = event;
|
|
1142
|
+
// A stream that lost chunks told its reader the full answer would be in
|
|
1143
|
+
// `run.result`. The terminal event does not always carry it — a failed
|
|
1144
|
+
// turn's `execution:error` has no `result` — so read it from the record
|
|
1145
|
+
// before settling, or the promise is false exactly when it was made
|
|
1146
|
+
// (OSK-12132).
|
|
1147
|
+
if (this.reportedGap && this.options.reconcileTerminal) {
|
|
1148
|
+
void this.readRecordedText().then((recorded) => this.settle(status, event, recorded));
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
this.settle(status, event, undefined);
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* The answer as the durable record holds it, for a run whose stream had a
|
|
1155
|
+
* hole. Bounded: the record is written before the terminal event is sent, so
|
|
1156
|
+
* it is normally there at once, and a record that stays silent must not hold
|
|
1157
|
+
* the run open — the reader then gets what the stream delivered.
|
|
1158
|
+
*/
|
|
1159
|
+
async readRecordedText() {
|
|
1160
|
+
const reconcile = this.options.reconcileTerminal;
|
|
1161
|
+
if (!reconcile)
|
|
1162
|
+
return undefined;
|
|
1163
|
+
for (let attempt = 0; attempt < GAP_BACKFILL_ATTEMPTS; attempt += 1) {
|
|
1164
|
+
if (attempt > 0)
|
|
1165
|
+
await new Promise((r) => setTimeout(r, GAP_BACKFILL_RETRY_MS));
|
|
1166
|
+
try {
|
|
1167
|
+
const terminal = await reconcile();
|
|
1168
|
+
if (terminal && typeof terminal.text === 'string' && terminal.text)
|
|
1169
|
+
return terminal.text;
|
|
1170
|
+
}
|
|
1171
|
+
catch {
|
|
1172
|
+
// No evidence either way; try again, then settle on the stream.
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return undefined;
|
|
1176
|
+
}
|
|
1177
|
+
settle(status, event, recordedText) {
|
|
897
1178
|
// `execution:complete` carries the final answer as `result`, and it is
|
|
898
1179
|
// AUTHORITATIVE — preferred over whatever the deltas added up to, not just
|
|
899
1180
|
// used when they added up to nothing.
|
|
@@ -922,6 +1203,12 @@ class AgenticStreamClient {
|
|
|
922
1203
|
if (finalText && finalText !== this.accumulated && !accumulationIsSuperset) {
|
|
923
1204
|
this.accumulated = finalText;
|
|
924
1205
|
}
|
|
1206
|
+
// The record wins over a stream known to have lost chunks, unless what
|
|
1207
|
+
// arrived is already longer (the record lags a completed stream, never the
|
|
1208
|
+
// other way round).
|
|
1209
|
+
if (recordedText && recordedText.length > this.accumulated.length) {
|
|
1210
|
+
this.accumulated = recordedText;
|
|
1211
|
+
}
|
|
925
1212
|
// The finished answer, as every other surface shows it: the web applies
|
|
926
1213
|
// this same sanitizer, so a scheduling JSON block or leaked function-call
|
|
927
1214
|
// XML the model wrote into its reply is removed here too instead of being
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type AuthBundle } from './auth-storage';
|
|
2
1
|
type DedicatedGenerationPreparationOptions = {
|
|
3
2
|
machineUuid?: string;
|
|
4
3
|
configDir?: string;
|
|
@@ -43,15 +42,9 @@ export declare class CliCredentialInitializationError extends Error {
|
|
|
43
42
|
* `isCredEnvelopeActive()` to decide whether to wrap the private
|
|
44
43
|
* key on persist.
|
|
45
44
|
* 2. `initDeviceIdentity` second.
|
|
46
|
-
* 3. Server enrollment is fire-and-forget; only attempts if a current
|
|
47
|
-
* access token is already on disk (peek via the file backend slot
|
|
48
|
-
* so a missing token short-circuits cleanly without creating a
|
|
49
|
-
* whole CredentialResolver chain here).
|
|
50
45
|
*
|
|
51
|
-
* The
|
|
52
|
-
*
|
|
53
|
-
* spawn the auth-helper when the user is unauthenticated. Mirrors the
|
|
54
|
-
* daemon's `peekAuthAccessToken()` shape.
|
|
46
|
+
* The CLI never enrolls the device key with the server; see the comment
|
|
47
|
+
* in `ensureCliAuthPrefetchStarted` (OSK-12063).
|
|
55
48
|
*/
|
|
56
49
|
export declare function prefetchCliAuth(): Promise<void>;
|
|
57
50
|
/**
|
|
@@ -63,5 +56,4 @@ export declare function prefetchCliAuth(): Promise<void>;
|
|
|
63
56
|
export declare function waitForCliCredentialEnvelope(): Promise<void>;
|
|
64
57
|
/** @internal Test seam — reset the module-level one-shot initialization. */
|
|
65
58
|
export declare function __resetPrefetchCliAuthForTest(): void;
|
|
66
|
-
export declare function shouldUseAccessTokenForEnrollment(bundle: Pick<AuthBundle, 'token' | 'expiresAt'> | null | undefined, nowMs?: number, minTtlMs?: number): boolean;
|
|
67
59
|
export {};
|
|
@@ -9,7 +9,6 @@ exports.ensureAuthCoreConfigured = ensureAuthCoreConfigured;
|
|
|
9
9
|
exports.prefetchCliAuth = prefetchCliAuth;
|
|
10
10
|
exports.waitForCliCredentialEnvelope = waitForCliCredentialEnvelope;
|
|
11
11
|
exports.__resetPrefetchCliAuthForTest = __resetPrefetchCliAuthForTest;
|
|
12
|
-
exports.shouldUseAccessTokenForEnrollment = shouldUseAccessTokenForEnrollment;
|
|
13
12
|
/**
|
|
14
13
|
* auth-core-init.ts — wire @skrr-ai/auth-core into the Sky CLI process.
|
|
15
14
|
*
|
|
@@ -31,8 +30,8 @@ exports.shouldUseAccessTokenForEnrollment = shouldUseAccessTokenForEnrollment;
|
|
|
31
30
|
* share the daemon's on-disk artifacts (~/.skrr/profiles/<p>/...)
|
|
32
31
|
* so a single physical machine has ONE device identity that `skrr`
|
|
33
32
|
* and `oversky` both consume.
|
|
34
|
-
* -
|
|
35
|
-
*
|
|
33
|
+
* - No server enrollment of the device key: that is the daemon login
|
|
34
|
+
* path's job (OSK-12063 — see `ensureCliAuthPrefetchStarted`).
|
|
36
35
|
*/
|
|
37
36
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
38
37
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -41,7 +40,6 @@ const auth_core_1 = require("@skrr-ai/auth-core");
|
|
|
41
40
|
const cred_envelope_1 = require("./cred-envelope");
|
|
42
41
|
const device_identity_1 = require("./device-identity");
|
|
43
42
|
const auth_storage_1 = require("./auth-storage");
|
|
44
|
-
const config_1 = require("./config");
|
|
45
43
|
function envFlag(name) {
|
|
46
44
|
const v = process.env[name];
|
|
47
45
|
return v === '1' || v === 'true' || v === 'TRUE';
|
|
@@ -135,7 +133,6 @@ const PREFETCH_TIMEOUT_MS = 5_000;
|
|
|
135
133
|
// with a 100–400 ms backoff (~20 s worst-case). Encrypted credentials get this
|
|
136
134
|
// larger, still-finite budget so lock contention cannot masquerade as logout.
|
|
137
135
|
const ENCRYPTED_CREDENTIAL_INIT_TIMEOUT_MS = 25_000;
|
|
138
|
-
const PREFETCH_ENROLL_MIN_TTL_MS = 5 * 60 * 1000;
|
|
139
136
|
let configured = false;
|
|
140
137
|
function ensureAuthCoreConfigured() {
|
|
141
138
|
if (configured)
|
|
@@ -247,31 +244,22 @@ function ensureCliAuthPrefetchStarted() {
|
|
|
247
244
|
try {
|
|
248
245
|
await envelopeInit;
|
|
249
246
|
await (0, device_identity_1.initDeviceIdentity)(DEFAULT_PROFILE);
|
|
250
|
-
//
|
|
251
|
-
// (
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
// reads the same value.
|
|
267
|
-
const machineUuid = (0, auth_core_1.ensureMachineDaemonId)();
|
|
268
|
-
(0, device_identity_1.enrollWithServer)({
|
|
269
|
-
serverUrl: cfg.baseURL,
|
|
270
|
-
accessToken: authBundle.token,
|
|
271
|
-
machineUuid,
|
|
272
|
-
}).catch(() => {
|
|
273
|
-
/* enrollment failure is non-fatal */
|
|
274
|
-
});
|
|
247
|
+
// The CLI deliberately does NOT enroll a device key with the server
|
|
248
|
+
// (OSK-12063). Enrollment pins the FIRST key presented on a machine's
|
|
249
|
+
// Daemon row, and every later refresh signed by a different key is
|
|
250
|
+
// refused. The CLI and the daemon of one profile can hold DIFFERENT
|
|
251
|
+
// keys (OSK-10314: the daemon stays on the legacy nested profile dir
|
|
252
|
+
// while consolidation is deferred), so a CLI enrollment could pin a
|
|
253
|
+
// key the daemon never signs with and lock the daemon out for good.
|
|
254
|
+
// Enrollment belongs to the daemon's own login path, which enrolls
|
|
255
|
+
// the key it will actually use.
|
|
256
|
+
//
|
|
257
|
+
// Until OSK-12063 this block did enroll, sending the minted UUIDv7
|
|
258
|
+
// from `~/.skrr/daemon.id` as `machineUuid`. The comment above it
|
|
259
|
+
// claimed that was the daemon's identifier too; it was not — the
|
|
260
|
+
// daemon sends the hardware UUID (`getMachineUuid()`), so every CLI
|
|
261
|
+
// enrollment 404'd DAEMON_NOT_FOUND and backed off. That 404 was the
|
|
262
|
+
// only thing keeping the pinning hazard above from firing.
|
|
275
263
|
}
|
|
276
264
|
catch (err) {
|
|
277
265
|
if (process.env.DEBUG) {
|
|
@@ -292,15 +280,9 @@ function ensureCliAuthPrefetchStarted() {
|
|
|
292
280
|
* `isCredEnvelopeActive()` to decide whether to wrap the private
|
|
293
281
|
* key on persist.
|
|
294
282
|
* 2. `initDeviceIdentity` second.
|
|
295
|
-
* 3. Server enrollment is fire-and-forget; only attempts if a current
|
|
296
|
-
* access token is already on disk (peek via the file backend slot
|
|
297
|
-
* so a missing token short-circuits cleanly without creating a
|
|
298
|
-
* whole CredentialResolver chain here).
|
|
299
283
|
*
|
|
300
|
-
* The
|
|
301
|
-
*
|
|
302
|
-
* spawn the auth-helper when the user is unauthenticated. Mirrors the
|
|
303
|
-
* daemon's `peekAuthAccessToken()` shape.
|
|
284
|
+
* The CLI never enrolls the device key with the server; see the comment
|
|
285
|
+
* in `ensureCliAuthPrefetchStarted` (OSK-12063).
|
|
304
286
|
*/
|
|
305
287
|
async function prefetchCliAuth() {
|
|
306
288
|
ensureCliAuthPrefetchStarted();
|
|
@@ -343,35 +325,6 @@ function __resetPrefetchCliAuthForTest() {
|
|
|
343
325
|
_credEnvelopeInitPromise = null;
|
|
344
326
|
_credEnvelopeInitStartedAt = 0;
|
|
345
327
|
}
|
|
346
|
-
/**
|
|
347
|
-
* Read the access token from keychain/file backends WITHOUT firing the
|
|
348
|
-
* full CredentialResolver chain. The full chain might prompt, spawn the
|
|
349
|
-
* auth-helper, or have other side effects we don't want during a
|
|
350
|
-
* prefetch-on-init pass.
|
|
351
|
-
*/
|
|
352
|
-
function peekStoredAuthBundle(expectedServerOrigin) {
|
|
353
|
-
try {
|
|
354
|
-
return (0, auth_storage_1.readFromBackend)(expectedServerOrigin).bundle;
|
|
355
|
-
}
|
|
356
|
-
catch {
|
|
357
|
-
return null;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
function shouldUseAccessTokenForEnrollment(bundle, nowMs = Date.now(), minTtlMs = PREFETCH_ENROLL_MIN_TTL_MS) {
|
|
361
|
-
if (!bundle?.token)
|
|
362
|
-
return false;
|
|
363
|
-
const expMs = accessExpiryMs(bundle);
|
|
364
|
-
if (expMs === undefined)
|
|
365
|
-
return true;
|
|
366
|
-
return expMs - nowMs > minTtlMs;
|
|
367
|
-
}
|
|
368
|
-
function accessExpiryMs(bundle) {
|
|
369
|
-
if (typeof bundle.expiresAt === 'number' && Number.isFinite(bundle.expiresAt)) {
|
|
370
|
-
return bundle.expiresAt;
|
|
371
|
-
}
|
|
372
|
-
const exp = (0, auth_core_1.decodeJwtExpiry)(bundle.token);
|
|
373
|
-
return typeof exp === 'number' && Number.isFinite(exp) ? exp * 1000 : undefined;
|
|
374
|
-
}
|
|
375
328
|
// Eager-configure on first import. Cheap and avoids ordering pitfalls in
|
|
376
329
|
// any future command that calls auth-core helpers from outside a Command.
|
|
377
330
|
ensureAuthCoreConfigured();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auth-failure.ts — say WHY a request was refused on authentication (OSK-12116).
|
|
3
|
+
*
|
|
4
|
+
* A 401 reaches the CLI after its own renewal has already failed: the access
|
|
5
|
+
* token expired, the refresh was refused, and the retry went out with the dead
|
|
6
|
+
* token. The server's answer to that retry is `TOKEN_EXPIRED`, which is true and
|
|
7
|
+
* useless — the expiry is routine, the refusal to RENEW is the fault, and it was
|
|
8
|
+
* visible only as an `[auth-core warn]` line on stderr.
|
|
9
|
+
*
|
|
10
|
+
* auth-core latches every permanent refresh refusal (`needs-reauth.json`, the
|
|
11
|
+
* same record `skrr whoami` prints as "Refresh state"). This module reads that
|
|
12
|
+
* latch and turns it into the sentence the error should have carried.
|
|
13
|
+
*/
|
|
14
|
+
export interface RefreshRefusal {
|
|
15
|
+
/** Permanent reason code the refresh latched (`REFRESH_INVALID`, `SESSION_REVOKED`, …). */
|
|
16
|
+
reason?: string;
|
|
17
|
+
/** The server's own message for the refusal, when auth-core recorded one. */
|
|
18
|
+
message?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface DescribedAuthFailure {
|
|
21
|
+
message: string;
|
|
22
|
+
/** The code a script should key on: the refresh refusal when known. */
|
|
23
|
+
code: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Explain one refresh refusal in terms of what the reader can do about it.
|
|
27
|
+
* Returns null when there is no latched refusal, so the caller keeps its
|
|
28
|
+
* generic wording rather than inventing a cause.
|
|
29
|
+
*/
|
|
30
|
+
export declare function describeRefreshRefusal(refusal: RefreshRefusal | null | undefined, opts: {
|
|
31
|
+
bin: string;
|
|
32
|
+
cliId?: string;
|
|
33
|
+
}): DescribedAuthFailure | null;
|