@skrr-ai/cli 0.1.55 → 0.1.57
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.d.ts +19 -1
- package/dist/base-command.js +104 -7
- package/dist/commands/agents/chat.js +5 -3
- package/dist/commands/agents/create.js +7 -0
- package/dist/commands/agents/update.js +7 -0
- package/dist/commands/login.js +14 -0
- package/dist/commands/tasks/move.js +11 -0
- package/dist/lib/agent-model-check.d.ts +19 -0
- package/dist/lib/agent-model-check.js +68 -0
- package/dist/lib/agentic-stream.d.ts +22 -0
- package/dist/lib/agentic-stream.js +91 -5
- package/dist/lib/auth-core-init.d.ts +20 -0
- package/dist/lib/auth-core-init.js +72 -2
- package/dist/lib/auth-storage.d.ts +15 -0
- package/dist/lib/auth-storage.js +33 -7
- package/dist/lib/commitments.js +3 -0
- package/dist/lib/config.d.ts +4 -0
- package/dist/lib/config.js +4 -2
- package/dist/lib/credential-resolver.d.ts +27 -0
- package/dist/lib/credential-resolver.js +19 -0
- package/dist/lib/daemonBroker.d.ts +25 -0
- package/dist/lib/daemonBroker.js +54 -15
- package/dist/lib/daemonHandoff.d.ts +1 -1
- package/dist/lib/daemonHandoff.js +21 -1
- package/dist/lib/internal-id.d.ts +28 -0
- package/dist/lib/internal-id.js +41 -0
- package/dist/lib/workflow-state-hint.d.ts +10 -0
- package/dist/lib/workflow-state-hint.js +27 -0
- package/dist/node_modules/@skrr-ai/data-provider/index.js +1896 -1893
- package/oclif.manifest.json +30434 -30434
- package/package.json +1 -1
package/dist/base-command.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { resolveProfileFromArgv } from './lib/keychain';
|
|
|
3
3
|
import { type AssigneeResolution, type MemberKind, type SpaceMember } from './lib/assignee-resolver';
|
|
4
4
|
import { type CliConfig } from './lib/config';
|
|
5
5
|
import { type OutboxMethod } from './lib/outbox';
|
|
6
|
-
import { type ResolvedCredential } from './lib/credential-resolver';
|
|
6
|
+
import { type CredentialProvenance, type ResolvedCredential } from './lib/credential-resolver';
|
|
7
7
|
import { type BrokerRefusalDescription } from './lib/daemonBrokerRefusal';
|
|
8
8
|
import { type DefaultAgentResolution } from './lib/tasks';
|
|
9
9
|
export declare function assigneePayloadNeedsResolution(payload: Record<string, unknown>): boolean;
|
|
@@ -68,6 +68,13 @@ export declare abstract class BaseCommand extends Command {
|
|
|
68
68
|
* from here.
|
|
69
69
|
*/
|
|
70
70
|
protected resolvedCredential: ResolvedCredential;
|
|
71
|
+
/**
|
|
72
|
+
* Where `resolvedCredential` really came from when its `source` slot cannot
|
|
73
|
+
* say — an in-memory daemon hand-off or Agent session token is recorded as
|
|
74
|
+
* `env-token` for refresh semantics, but was not read from OVERSKY_TOKEN
|
|
75
|
+
* (OSK-12185). Null for every stored/env/flag credential.
|
|
76
|
+
*/
|
|
77
|
+
protected credentialProvenance: CredentialProvenance | null;
|
|
71
78
|
protected bareMode: boolean;
|
|
72
79
|
/**
|
|
73
80
|
* Set when the auto-broker reached a live daemon and it REFUSED the hand-off.
|
|
@@ -102,6 +109,17 @@ export declare abstract class BaseCommand extends Command {
|
|
|
102
109
|
* silently matched nothing reaches the search.
|
|
103
110
|
*/
|
|
104
111
|
protected resolveSpaceReference(value: string): Promise<string>;
|
|
112
|
+
/**
|
|
113
|
+
* A 24-hex value: almost always the internal `_id` copied from `--json`,
|
|
114
|
+
* which prints it first (OSK-12205). No space route accepts it, so passing it
|
|
115
|
+
* through produced a not-found or a raw 404 that never said to use `id`.
|
|
116
|
+
*
|
|
117
|
+
* One listing tells the cases apart: a space whose `id` really is this value
|
|
118
|
+
* passes through unchanged; one whose `_id` is this value fails naming the
|
|
119
|
+
* exact `id` to use. A listing that fails, or finds neither, is not proof of
|
|
120
|
+
* anything, so it keeps the old pass-through and lets the route answer.
|
|
121
|
+
*/
|
|
122
|
+
private resolveObjectIdShapedSpace;
|
|
105
123
|
/**
|
|
106
124
|
* Turn a project reference into `{ projectRef, spaceId }`, or fail the
|
|
107
125
|
* command.
|
package/dist/base-command.js
CHANGED
|
@@ -20,6 +20,7 @@ const core_1 = require("@oclif/core");
|
|
|
20
20
|
Object.defineProperty(exports, "Flags", { enumerable: true, get: function () { return core_1.Flags; } });
|
|
21
21
|
const option_hint_1 = require("./lib/option-hint");
|
|
22
22
|
const failed_lookup_1 = require("./lib/failed-lookup");
|
|
23
|
+
const internal_id_1 = require("./lib/internal-id");
|
|
23
24
|
const http_error_1 = require("./lib/http-error");
|
|
24
25
|
// Re-exported from its home in `lib/keychain`, beside the other profile
|
|
25
26
|
// functions. It lived here while `init()` was its only caller; a lib module
|
|
@@ -132,6 +133,13 @@ class BaseCommand extends core_1.Command {
|
|
|
132
133
|
* from here.
|
|
133
134
|
*/
|
|
134
135
|
resolvedCredential;
|
|
136
|
+
/**
|
|
137
|
+
* Where `resolvedCredential` really came from when its `source` slot cannot
|
|
138
|
+
* say — an in-memory daemon hand-off or Agent session token is recorded as
|
|
139
|
+
* `env-token` for refresh semantics, but was not read from OVERSKY_TOKEN
|
|
140
|
+
* (OSK-12185). Null for every stored/env/flag credential.
|
|
141
|
+
*/
|
|
142
|
+
credentialProvenance = null;
|
|
135
143
|
bareMode = false;
|
|
136
144
|
/**
|
|
137
145
|
* Set when the auto-broker reached a live daemon and it REFUSED the hand-off.
|
|
@@ -173,6 +181,7 @@ class BaseCommand extends core_1.Command {
|
|
|
173
181
|
return;
|
|
174
182
|
}
|
|
175
183
|
autonomousCredential = delegatedCredential;
|
|
184
|
+
this.credentialProvenance = { kind: 'daemon-delegated' };
|
|
176
185
|
}
|
|
177
186
|
catch (err) {
|
|
178
187
|
this.failWithCliError({
|
|
@@ -214,6 +223,10 @@ class BaseCommand extends core_1.Command {
|
|
|
214
223
|
delete process.env.OVERSKY_WORKSPACE_ID;
|
|
215
224
|
}
|
|
216
225
|
this.resolvedCredential = autonomousCredential;
|
|
226
|
+
if (!this.credentialProvenance && autonomousCredential.source === 'env-token') {
|
|
227
|
+
// `resolveTrustedAutonomousCredential` read OVERSKY_SESSION_TOKEN.
|
|
228
|
+
this.credentialProvenance = { kind: 'agent-session' };
|
|
229
|
+
}
|
|
217
230
|
(0, node_adapter_1.setAdapterCredentialOverride)(this.resolvedCredential);
|
|
218
231
|
(0, data_provider_1.setHttpAdapter)((0, node_adapter_1.createNodeAdapter)());
|
|
219
232
|
return;
|
|
@@ -248,6 +261,27 @@ class BaseCommand extends core_1.Command {
|
|
|
248
261
|
// can't wedge the CLI; on timeout we fall through to legacy
|
|
249
262
|
// plaintext and the resolver still finds the token.
|
|
250
263
|
await (0, auth_core_init_1.prefetchCliAuth)();
|
|
264
|
+
// On a brokered Dedicated guest a stored cli-auth.json predates broker
|
|
265
|
+
// mode, and the broker's success path retires it (revoke + delete). That
|
|
266
|
+
// could never run when the file carried an access token: the resolver
|
|
267
|
+
// below adopted it first, and the auto-broker only asks when the resolver
|
|
268
|
+
// found nothing — so a live pre-broker family was used and kept forever
|
|
269
|
+
// (OSK-12194). Broker FIRST in exactly that case. The file is retired only
|
|
270
|
+
// after a redeem succeeds; if the broker cannot answer, the resolver still
|
|
271
|
+
// falls back to the file, so a working login is never lost to an outage.
|
|
272
|
+
// `maybeAutoBroker` applies the same skip rules as below (bare, env token,
|
|
273
|
+
// login/logout/pair), and a laptop never has the guest descriptor.
|
|
274
|
+
let earlyAutoBroker = null;
|
|
275
|
+
if (!this.bareMode && !parsed.token && (0, daemonBroker_1.shouldBrokerBeforeStoredCredential)()) {
|
|
276
|
+
earlyAutoBroker = await (0, daemonBroker_1.maybeAutoBroker)({
|
|
277
|
+
cliConfig: this.cliConfig,
|
|
278
|
+
bareMode: this.bareMode,
|
|
279
|
+
commandId: this.id,
|
|
280
|
+
});
|
|
281
|
+
if (!earlyAutoBroker.triggered)
|
|
282
|
+
earlyAutoBroker = null;
|
|
283
|
+
}
|
|
284
|
+
const brokeredFirst = earlyAutoBroker?.outcome?.ok === true;
|
|
251
285
|
// Resolve credential via the Phase H.1 precedence chain. The
|
|
252
286
|
// adapter then cherry-picks the token for the Authorization
|
|
253
287
|
// header; we hand it a pre-resolved override so it doesn't
|
|
@@ -284,12 +318,14 @@ class BaseCommand extends core_1.Command {
|
|
|
284
318
|
// repair that reason implies — "Not signed in. Run `skrr login`" was true
|
|
285
319
|
// and pointed at the wrong fix (OSK-12149). Rate limiting and a mismatched
|
|
286
320
|
// baseURL keep their own one-line hints below.
|
|
287
|
-
if (!this.resolvedCredential.token) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
321
|
+
if (!this.resolvedCredential.token || brokeredFirst) {
|
|
322
|
+
// The guest's early broker above already asked; its answer stands.
|
|
323
|
+
const autoResult = earlyAutoBroker ??
|
|
324
|
+
(await (0, daemonBroker_1.maybeAutoBroker)({
|
|
325
|
+
cliConfig: this.cliConfig,
|
|
326
|
+
bareMode: this.bareMode,
|
|
327
|
+
commandId: this.id,
|
|
328
|
+
}));
|
|
293
329
|
if (autoResult.triggered && autoResult.outcome) {
|
|
294
330
|
if (autoResult.outcome.ok) {
|
|
295
331
|
// Persist the cliId now so a sibling skrr process spawned
|
|
@@ -327,6 +363,17 @@ class BaseCommand extends core_1.Command {
|
|
|
327
363
|
: 'file',
|
|
328
364
|
kind: (0, auth_core_1.classifyTokenKind)(accessToken),
|
|
329
365
|
};
|
|
366
|
+
// The slot above says `env-token`; the label must not. The token
|
|
367
|
+
// came from the daemon's hand-off descriptor, and OVERSKY_TOKEN is
|
|
368
|
+
// typically unset (OSK-12185).
|
|
369
|
+
this.credentialProvenance = autoResult.outcome.brokered
|
|
370
|
+
? {
|
|
371
|
+
kind: 'daemon-handoff',
|
|
372
|
+
...(autoResult.outcome.descriptorPath
|
|
373
|
+
? { descriptorPath: autoResult.outcome.descriptorPath }
|
|
374
|
+
: {}),
|
|
375
|
+
}
|
|
376
|
+
: null;
|
|
330
377
|
}
|
|
331
378
|
else if ((this.brokerRefusal = (0, daemonBrokerRefusal_1.describeBrokerRefusal)(autoResult.outcome, this.config.bin))) {
|
|
332
379
|
// Reported by requireAuth(); nothing to print here.
|
|
@@ -426,6 +473,8 @@ class BaseCommand extends core_1.Command {
|
|
|
426
473
|
// Only canonical ids short-circuit. Human-readable refs are accepted by
|
|
427
474
|
// some read endpoints, but space-scoped mutation endpoints require the
|
|
428
475
|
// UUID, so refs go through the same exact-match resolution as titles.
|
|
476
|
+
if ((0, internal_id_1.looksLikeInternalObjectId)(query))
|
|
477
|
+
return this.resolveObjectIdShapedSpace(query);
|
|
429
478
|
if (!query || (0, space_resolver_1.looksLikeCanonicalSpaceId)(query))
|
|
430
479
|
return query;
|
|
431
480
|
if ((0, failed_lookup_1.isFailedLookupValue)(query)) {
|
|
@@ -470,6 +519,43 @@ class BaseCommand extends core_1.Command {
|
|
|
470
519
|
},
|
|
471
520
|
});
|
|
472
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* A 24-hex value: almost always the internal `_id` copied from `--json`,
|
|
524
|
+
* which prints it first (OSK-12205). No space route accepts it, so passing it
|
|
525
|
+
* through produced a not-found or a raw 404 that never said to use `id`.
|
|
526
|
+
*
|
|
527
|
+
* One listing tells the cases apart: a space whose `id` really is this value
|
|
528
|
+
* passes through unchanged; one whose `_id` is this value fails naming the
|
|
529
|
+
* exact `id` to use. A listing that fails, or finds neither, is not proof of
|
|
530
|
+
* anything, so it keeps the old pass-through and lets the route answer.
|
|
531
|
+
*/
|
|
532
|
+
async resolveObjectIdShapedSpace(query) {
|
|
533
|
+
let rows = [];
|
|
534
|
+
try {
|
|
535
|
+
const response = (await data_provider_1.dataService.listSpaces({ limit: 200, includeArchived: true }));
|
|
536
|
+
rows = response?.data ?? [];
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
return query;
|
|
540
|
+
}
|
|
541
|
+
if (rows.some((row) => row.id === query))
|
|
542
|
+
return query;
|
|
543
|
+
const byInternalId = rows.find((row) => String(row._id ?? '') === query);
|
|
544
|
+
if (!byInternalId)
|
|
545
|
+
return query;
|
|
546
|
+
this.failWithCliError({
|
|
547
|
+
message: (0, internal_id_1.describeInternalIdMisuse)({
|
|
548
|
+
value: query,
|
|
549
|
+
noun: 'space',
|
|
550
|
+
canonicalId: typeof byInternalId.id === 'string' ? byInternalId.id : null,
|
|
551
|
+
bin: this.config.bin,
|
|
552
|
+
}),
|
|
553
|
+
code: 'SPACE_INTERNAL_ID',
|
|
554
|
+
exit: 2,
|
|
555
|
+
retryable: false,
|
|
556
|
+
details: { context: { query, id: byInternalId.id } },
|
|
557
|
+
});
|
|
558
|
+
}
|
|
473
559
|
async resolveProjectReference(value, spaceFlag, opts = {}) {
|
|
474
560
|
const spaceRequired = opts.spaceRequired !== false;
|
|
475
561
|
const { projectRef, spaceRef } = (0, projects_1.splitQualifiedProjectRef)(value, spaceFlag);
|
|
@@ -534,6 +620,17 @@ class BaseCommand extends core_1.Command {
|
|
|
534
620
|
const query = value.trim();
|
|
535
621
|
if (!query)
|
|
536
622
|
return query;
|
|
623
|
+
// A task's `id` is a UUID, so a 24-hex value is the internal `_id` that
|
|
624
|
+
// `--json` prints first. The title search it used to reach can only say
|
|
625
|
+
// "no task matching", which never names the field to use (OSK-12205).
|
|
626
|
+
if ((0, internal_id_1.looksLikeInternalObjectId)(query)) {
|
|
627
|
+
this.failWithCliError({
|
|
628
|
+
message: (0, internal_id_1.describeInternalIdMisuse)({ value: query, noun: 'task', bin: this.config.bin }),
|
|
629
|
+
code: 'TASK_INTERNAL_ID',
|
|
630
|
+
exit: 2,
|
|
631
|
+
retryable: false,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
537
634
|
// Before the search, not after it: `null` from a failed `jq -r` must never
|
|
538
635
|
// reach the title tiers, and it should not cost a request to say so.
|
|
539
636
|
if ((0, failed_lookup_1.isFailedLookupValue)(query)) {
|
|
@@ -821,7 +918,7 @@ class BaseCommand extends core_1.Command {
|
|
|
821
918
|
credentialSourceLabel() {
|
|
822
919
|
if (!this.resolvedCredential?.token)
|
|
823
920
|
return null;
|
|
824
|
-
return (0, credential_resolver_1.
|
|
921
|
+
return (0, credential_resolver_1.describeCredentialSource)(this.resolvedCredential.source, this.credentialProvenance);
|
|
825
922
|
}
|
|
826
923
|
async catch(err) {
|
|
827
924
|
if (isOclifExitError(err)) {
|
|
@@ -287,9 +287,11 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
287
287
|
description: 'Permission mode for THIS turn only, on a local-harness agent. It beats the agent’s and ' +
|
|
288
288
|
'the harness’s stored defaults for this turn and changes nothing else — pass it again ' +
|
|
289
289
|
'on each turn that needs it (`agents update --default-permission-mode` changes every ' +
|
|
290
|
-
'conversation of the agent). A mode the harness cannot honour runs as the
|
|
291
|
-
'and run.started says which
|
|
292
|
-
'
|
|
290
|
+
'conversation of the agent). A mode the harness cannot honour runs as the closest ' +
|
|
291
|
+
'STRICTER mode it can, and run.started says which; when the only mode it can run would ' +
|
|
292
|
+
'let more run without asking, the turn is refused before it starts (run.failed, ' +
|
|
293
|
+
'PERMISSION_MODE_UNSUPPORTED_BY_HARNESS). --approve-tools is separate: it answers the ' +
|
|
294
|
+
'approval prompts a mode asks for.',
|
|
293
295
|
options: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
|
|
294
296
|
}),
|
|
295
297
|
'local-skill': core_1.Flags.string({
|
|
@@ -9,6 +9,7 @@ const core_1 = require("@oclif/core");
|
|
|
9
9
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
10
10
|
const agent_visual_refs_1 = require("../../lib/agent-visual-refs");
|
|
11
11
|
const runtime_tool_support_1 = require("../../lib/runtime-tool-support");
|
|
12
|
+
const agent_model_check_1 = require("../../lib/agent-model-check");
|
|
12
13
|
const base_command_1 = require("../../base-command");
|
|
13
14
|
const execution_target_1 = require("../../lib/execution-target");
|
|
14
15
|
const prompt_1 = require("../../lib/prompt");
|
|
@@ -407,6 +408,12 @@ class AgentsCreate extends base_command_1.BaseCommand {
|
|
|
407
408
|
this.warn(unappliedPayloadKeysWarning(unapplied));
|
|
408
409
|
}
|
|
409
410
|
await (0, runtime_tool_support_1.warnPluginsNotLoaded)(payload.tools, (message) => this.warn(message));
|
|
411
|
+
await (0, agent_model_check_1.warnIfModelNotListed)({
|
|
412
|
+
model: payload.model,
|
|
413
|
+
agent: created,
|
|
414
|
+
bin: this.config.bin,
|
|
415
|
+
warn: (message) => this.warn(message),
|
|
416
|
+
});
|
|
410
417
|
if (flags.json) {
|
|
411
418
|
this.log(JSON.stringify((0, web_url_1.withAgentUrl)(this.cliConfig.baseURL, created), null, 2));
|
|
412
419
|
return;
|
|
@@ -10,6 +10,7 @@ const data_provider_1 = require("@skrr-ai/data-provider");
|
|
|
10
10
|
const agent_visual_refs_1 = require("../../lib/agent-visual-refs");
|
|
11
11
|
const create_1 = require("./create");
|
|
12
12
|
const runtime_tool_support_1 = require("../../lib/runtime-tool-support");
|
|
13
|
+
const agent_model_check_1 = require("../../lib/agent-model-check");
|
|
13
14
|
const base_command_1 = require("../../base-command");
|
|
14
15
|
const execution_target_1 = require("../../lib/execution-target");
|
|
15
16
|
const prompt_1 = require("../../lib/prompt");
|
|
@@ -221,6 +222,12 @@ class AgentsUpdate extends base_command_1.BaseCommand {
|
|
|
221
222
|
for (const warning of agentUpdateWarnings(payload, updated))
|
|
222
223
|
this.warn(warning);
|
|
223
224
|
await (0, runtime_tool_support_1.warnPluginsNotLoaded)(payload.tools, (message) => this.warn(message));
|
|
225
|
+
await (0, agent_model_check_1.warnIfModelNotListed)({
|
|
226
|
+
model: payload.model,
|
|
227
|
+
agent: updated,
|
|
228
|
+
bin: this.config.bin,
|
|
229
|
+
warn: (message) => this.warn(message),
|
|
230
|
+
});
|
|
224
231
|
if (flags.json) {
|
|
225
232
|
this.log(JSON.stringify((0, web_url_1.withAgentUrl)(this.cliConfig.baseURL, updated), null, 2));
|
|
226
233
|
return;
|
package/dist/commands/login.js
CHANGED
|
@@ -234,6 +234,20 @@ class Login extends base_command_1.BaseCommand {
|
|
|
234
234
|
if (handoff.status === 'delivered') {
|
|
235
235
|
this.log(`The local daemon was signed in too — ${handoff.detail}.`);
|
|
236
236
|
}
|
|
237
|
+
else if (handoff.reason === 'foreign-root') {
|
|
238
|
+
// Not a failure and not a hint to re-login anything: the daemon is
|
|
239
|
+
// the default root's, and this is a second root (OSK-12189).
|
|
240
|
+
this.log(`The local daemon was left as it was — ${handoff.detail}.`);
|
|
241
|
+
}
|
|
242
|
+
else if (handoff.reason === 'mint-refused') {
|
|
243
|
+
// The server refused before the daemon was touched, so its own
|
|
244
|
+
// credential is exactly what it was. Saying "not signed in" here sent
|
|
245
|
+
// people to re-login a healthy daemon (OSK-12189).
|
|
246
|
+
this.log(`This login was not handed to the local daemon (${handoff.detail}). ` +
|
|
247
|
+
`The daemon keeps whatever sign-in it already had — check it with ` +
|
|
248
|
+
`\`${this.config.bin} daemon status\`, and run \`${this.config.bin} daemon login\` ` +
|
|
249
|
+
`only if that says it is signed out.`);
|
|
250
|
+
}
|
|
237
251
|
else if (handoff.status === 'failed') {
|
|
238
252
|
this.log(`The local daemon was not signed in (${handoff.detail}). ` +
|
|
239
253
|
`Run \`${this.config.bin} daemon login\` to finish.`);
|
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const core_1 = require("@oclif/core");
|
|
4
4
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
5
5
|
const api_fetch_1 = require("../../lib/api-fetch");
|
|
6
|
+
const http_error_1 = require("../../lib/http-error");
|
|
7
|
+
const workflow_state_hint_1 = require("../../lib/workflow-state-hint");
|
|
6
8
|
const session_task_endpoints_1 = require("../../lib/session-task-endpoints");
|
|
7
9
|
const base_command_1 = require("../../base-command");
|
|
8
10
|
const task_closure_1 = require("../../lib/task-closure");
|
|
@@ -129,6 +131,15 @@ class TasksMove extends base_command_1.BaseCommand {
|
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
catch (err) {
|
|
134
|
+
const unknownState = (0, workflow_state_hint_1.describeUnknownWorkflowState)((0, http_error_1.httpBodyOf)(err), this.config.bin);
|
|
135
|
+
if (unknownState) {
|
|
136
|
+
this.failWithCliError({
|
|
137
|
+
message: unknownState,
|
|
138
|
+
code: 'WORKFLOW_STATE_UNKNOWN',
|
|
139
|
+
exit: 2,
|
|
140
|
+
retryable: false,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
132
143
|
this.handleApiError(err);
|
|
133
144
|
}
|
|
134
145
|
if (flags.json) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface ModelsReport {
|
|
2
|
+
provider?: string | null;
|
|
3
|
+
source?: string;
|
|
4
|
+
sourceReason?: string;
|
|
5
|
+
models?: Array<{
|
|
6
|
+
value?: string;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
/** The harness the saved agent runs on first, as the write response names it. */
|
|
10
|
+
export declare function primaryHarnessId(agent: unknown): string | null;
|
|
11
|
+
export declare function describeUnlistedModel(model: string | undefined, report: ModelsReport | undefined, harnessId: string, bin: string): string | null;
|
|
12
|
+
/** Best effort: a check that cannot run says nothing rather than failing the save. */
|
|
13
|
+
export declare function warnIfModelNotListed(opts: {
|
|
14
|
+
model: string | undefined;
|
|
15
|
+
agent: unknown;
|
|
16
|
+
bin: string;
|
|
17
|
+
warn: (message: string) => void;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.primaryHarnessId = primaryHarnessId;
|
|
4
|
+
exports.describeUnlistedModel = describeUnlistedModel;
|
|
5
|
+
exports.warnIfModelNotListed = warnIfModelNotListed;
|
|
6
|
+
const harnesses_1 = require("./harnesses");
|
|
7
|
+
/**
|
|
8
|
+
* A model id the agent's harness does not list, said at save time (OSK-12205).
|
|
9
|
+
*
|
|
10
|
+
* `agents create/update --model <id>` stored any well-formed id without a
|
|
11
|
+
* word; the first anyone heard of a typo was a failed turn, whose remedy then
|
|
12
|
+
* sent a local agent to "publish the agent". The server only validates the
|
|
13
|
+
* id's SHAPE on write — asking the daemon for its live list on every PATCH
|
|
14
|
+
* would put a machine round-trip on the write path — so the check lives here,
|
|
15
|
+
* after the write, and never fails the command.
|
|
16
|
+
*
|
|
17
|
+
* Only a list that speaks for the harness can say "not there": `live` (the
|
|
18
|
+
* installed harness enumerated it) or `managed` (the admission policy). A
|
|
19
|
+
* `curated` list is our suggestion, so a miss against it is reported as
|
|
20
|
+
* unverified, with the reason the live list was unavailable.
|
|
21
|
+
*/
|
|
22
|
+
/** Not model names: "no selection", which runs the harness default. */
|
|
23
|
+
const SENTINELS = new Set(['', 'default', 'auto']);
|
|
24
|
+
/** The harness the saved agent runs on first, as the write response names it. */
|
|
25
|
+
function primaryHarnessId(agent) {
|
|
26
|
+
const record = (agent ?? {});
|
|
27
|
+
if (typeof record.harnessId === 'string' && record.harnessId)
|
|
28
|
+
return record.harnessId;
|
|
29
|
+
const policy = record.executionPolicy;
|
|
30
|
+
const target = (policy?.targets ?? []).find((t) => t?.kind === 'harness' && t.enabled !== false && typeof t.harnessId === 'string');
|
|
31
|
+
return target?.harnessId ?? null;
|
|
32
|
+
}
|
|
33
|
+
function describeUnlistedModel(model, report, harnessId, bin) {
|
|
34
|
+
const id = typeof model === 'string' ? model.trim() : '';
|
|
35
|
+
if (SENTINELS.has(id.toLowerCase()))
|
|
36
|
+
return null;
|
|
37
|
+
const listed = (report?.models ?? []).some((entry) => entry?.value === id);
|
|
38
|
+
if (listed)
|
|
39
|
+
return null;
|
|
40
|
+
const provider = report?.provider || 'this';
|
|
41
|
+
const remedy = `\`${bin} harnesses models ${harnessId}\` lists what it can run.`;
|
|
42
|
+
if (report?.source === 'live' || report?.source === 'managed') {
|
|
43
|
+
return (`Model '${id}' is not in the ${provider} harness's ${report.source} model list ` +
|
|
44
|
+
`(${harnessId}). It was saved, but a turn fails when the harness refuses it. ${remedy}`);
|
|
45
|
+
}
|
|
46
|
+
const why = report?.sourceReason ? ` (${report.sourceReason})` : '';
|
|
47
|
+
return (`Model '${id}' is not in the suggested list for the ${provider} harness, and the ` +
|
|
48
|
+
`harness's live list was unavailable${why}, so it could not be checked. It was saved. ` +
|
|
49
|
+
remedy);
|
|
50
|
+
}
|
|
51
|
+
/** Best effort: a check that cannot run says nothing rather than failing the save. */
|
|
52
|
+
async function warnIfModelNotListed(opts) {
|
|
53
|
+
const id = typeof opts.model === 'string' ? opts.model.trim() : '';
|
|
54
|
+
if (SENTINELS.has(id.toLowerCase()))
|
|
55
|
+
return;
|
|
56
|
+
const harnessId = primaryHarnessId(opts.agent);
|
|
57
|
+
if (!harnessId)
|
|
58
|
+
return;
|
|
59
|
+
try {
|
|
60
|
+
const report = (await (0, harnesses_1.getHarnessModels)(harnessId));
|
|
61
|
+
const message = describeUnlistedModel(id, report, harnessId, opts.bin);
|
|
62
|
+
if (message)
|
|
63
|
+
opts.warn(message);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* the save stands; the harness list is a hint */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -173,6 +173,16 @@ export type ProjectedToolEvent = {
|
|
|
173
173
|
* of the run. The pairing is unambiguous within a turn.
|
|
174
174
|
*/
|
|
175
175
|
export declare function projectDaemonToolEvents(envelopes: unknown[], namesByCall: Map<string, string>): ProjectedToolEvent[];
|
|
176
|
+
/**
|
|
177
|
+
* The line printed when a tool call ends, in the words its status earns.
|
|
178
|
+
*
|
|
179
|
+
* Every end used to print `[tool completed]`, including a failed call. And a
|
|
180
|
+
* call the harness never closed — which the daemon now reports as
|
|
181
|
+
* `unreported` rather than `failed` (OSK-12188) — must read as neither: it may
|
|
182
|
+
* well have run. Its close is made when the session exits, so the line says
|
|
183
|
+
* that too, rather than let the moment it prints stand for when the call ran.
|
|
184
|
+
*/
|
|
185
|
+
export declare function describeToolEnd(toolName: unknown, status: unknown): string;
|
|
176
186
|
/**
|
|
177
187
|
* WHO or WHAT decided a permission request, as a reader needs it (OSK-12113).
|
|
178
188
|
*
|
|
@@ -355,6 +365,12 @@ export declare class AgenticStreamClient {
|
|
|
355
365
|
private readonly toolNamesByCall;
|
|
356
366
|
/** `permission-ask` requestId → tool name, to name its resolution (OSK-12113). */
|
|
357
367
|
private readonly toolNamesByPermission;
|
|
368
|
+
/**
|
|
369
|
+
* Permission prompts on the terminal still waiting for an answer, by
|
|
370
|
+
* toolUseId, so a server `permission:cancel` can close the one it names
|
|
371
|
+
* (OSK-12197) instead of leaving `[y/N]` open after the run stopped asking.
|
|
372
|
+
*/
|
|
373
|
+
private readonly openPermissionPrompts;
|
|
358
374
|
/** Text already written to the terminal, for suffix-only rendering. */
|
|
359
375
|
private renderedText;
|
|
360
376
|
private idleTimer;
|
|
@@ -433,6 +449,12 @@ export declare class AgenticStreamClient {
|
|
|
433
449
|
private installHandlers;
|
|
434
450
|
private handlePermission;
|
|
435
451
|
private handleQuestion;
|
|
452
|
+
/**
|
|
453
|
+
* The server closed a permission request (OSK-12197): it expired unanswered,
|
|
454
|
+
* the pre-execution step gave up, or another surface answered it. Say so,
|
|
455
|
+
* and close the terminal prompt if this client is still showing it.
|
|
456
|
+
*/
|
|
457
|
+
private handlePermissionCancel;
|
|
436
458
|
private ask;
|
|
437
459
|
/**
|
|
438
460
|
* True the first time this (kind, identity) pair is seen; false afterwards.
|
|
@@ -7,6 +7,7 @@ exports.envelopesText = envelopesText;
|
|
|
7
7
|
exports.envelopeKey = envelopeKey;
|
|
8
8
|
exports.unseenEnvelopes = unseenEnvelopes;
|
|
9
9
|
exports.projectDaemonToolEvents = projectDaemonToolEvents;
|
|
10
|
+
exports.describeToolEnd = describeToolEnd;
|
|
10
11
|
exports.describePermissionDecider = describePermissionDecider;
|
|
11
12
|
exports.projectPermissionResolutions = projectPermissionResolutions;
|
|
12
13
|
exports.renderableDelta = renderableDelta;
|
|
@@ -249,6 +250,28 @@ function projectDaemonToolEvents(envelopes, namesByCall) {
|
|
|
249
250
|
}
|
|
250
251
|
return out;
|
|
251
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* The line printed when a tool call ends, in the words its status earns.
|
|
255
|
+
*
|
|
256
|
+
* Every end used to print `[tool completed]`, including a failed call. And a
|
|
257
|
+
* call the harness never closed — which the daemon now reports as
|
|
258
|
+
* `unreported` rather than `failed` (OSK-12188) — must read as neither: it may
|
|
259
|
+
* well have run. Its close is made when the session exits, so the line says
|
|
260
|
+
* that too, rather than let the moment it prints stand for when the call ran.
|
|
261
|
+
*/
|
|
262
|
+
function describeToolEnd(toolName, status) {
|
|
263
|
+
const name = String(toolName ?? 'unknown');
|
|
264
|
+
switch (status) {
|
|
265
|
+
case 'failed':
|
|
266
|
+
return `[tool failed] ${name}`;
|
|
267
|
+
case 'cancelled':
|
|
268
|
+
return `[tool cancelled] ${name}`;
|
|
269
|
+
case 'unreported':
|
|
270
|
+
return `[tool result not reported] ${name} (the harness never said how it ended; closed when the session ended)`;
|
|
271
|
+
default:
|
|
272
|
+
return `[tool completed] ${name}`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
252
275
|
const SURFACE_LABELS = {
|
|
253
276
|
web: 'the web app',
|
|
254
277
|
cli: 'the skrr CLI',
|
|
@@ -534,6 +557,12 @@ class AgenticStreamClient {
|
|
|
534
557
|
toolNamesByCall = new Map();
|
|
535
558
|
/** `permission-ask` requestId → tool name, to name its resolution (OSK-12113). */
|
|
536
559
|
toolNamesByPermission = new Map();
|
|
560
|
+
/**
|
|
561
|
+
* Permission prompts on the terminal still waiting for an answer, by
|
|
562
|
+
* toolUseId, so a server `permission:cancel` can close the one it names
|
|
563
|
+
* (OSK-12197) instead of leaving `[y/N]` open after the run stopped asking.
|
|
564
|
+
*/
|
|
565
|
+
openPermissionPrompts = new Map();
|
|
537
566
|
/** Text already written to the terminal, for suffix-only rendering. */
|
|
538
567
|
renderedText = '';
|
|
539
568
|
idleTimer = null;
|
|
@@ -923,6 +952,11 @@ class AgenticStreamClient {
|
|
|
923
952
|
return;
|
|
924
953
|
void this.handlePermission(event);
|
|
925
954
|
});
|
|
955
|
+
socket.on('permission:cancel', (event) => {
|
|
956
|
+
if (!match(event))
|
|
957
|
+
return;
|
|
958
|
+
this.handlePermissionCancel(event);
|
|
959
|
+
});
|
|
926
960
|
socket.on('question:ask', (event) => {
|
|
927
961
|
if (match(event))
|
|
928
962
|
void this.handleQuestion(event);
|
|
@@ -985,7 +1019,16 @@ class AgenticStreamClient {
|
|
|
985
1019
|
});
|
|
986
1020
|
return;
|
|
987
1021
|
}
|
|
988
|
-
const
|
|
1022
|
+
const prompt = new AbortController();
|
|
1023
|
+
if (event.toolUseId)
|
|
1024
|
+
this.openPermissionPrompts.set(event.toolUseId, prompt);
|
|
1025
|
+
const answer = await this.ask(`Allow ${describePermission(event)}? [y/N] `, prompt.signal);
|
|
1026
|
+
if (event.toolUseId)
|
|
1027
|
+
this.openPermissionPrompts.delete(event.toolUseId);
|
|
1028
|
+
// Closed by the server (it stopped waiting, or another surface answered):
|
|
1029
|
+
// there is nothing left to answer, and a late "no" would read as a person's.
|
|
1030
|
+
if (prompt.signal.aborted)
|
|
1031
|
+
return;
|
|
989
1032
|
const approved = /^y(es)?$/i.test(answer.trim());
|
|
990
1033
|
this.socket.emit(approved ? 'permission:approve' : 'permission:deny', {
|
|
991
1034
|
sessionId: this.options.sessionId,
|
|
@@ -1041,10 +1084,36 @@ class AgenticStreamClient {
|
|
|
1041
1084
|
...(interactive ? {} : { nonInteractive: true }),
|
|
1042
1085
|
});
|
|
1043
1086
|
}
|
|
1044
|
-
|
|
1087
|
+
/**
|
|
1088
|
+
* The server closed a permission request (OSK-12197): it expired unanswered,
|
|
1089
|
+
* the pre-execution step gave up, or another surface answered it. Say so,
|
|
1090
|
+
* and close the terminal prompt if this client is still showing it.
|
|
1091
|
+
*/
|
|
1092
|
+
handlePermissionCancel(event) {
|
|
1093
|
+
const toolUseId = typeof event.toolUseId === 'string' ? event.toolUseId : undefined;
|
|
1094
|
+
const open = toolUseId ? this.openPermissionPrompts.get(toolUseId) : undefined;
|
|
1095
|
+
// Every answer is echoed to the whole session as `answered` — including
|
|
1096
|
+
// this client's own. Only one that closes a prompt still open HERE is news.
|
|
1097
|
+
if (event.reason === 'answered' && !open)
|
|
1098
|
+
return;
|
|
1099
|
+
if (!this.firstTimeSeen('permission-cancel', toolUseId))
|
|
1100
|
+
return;
|
|
1101
|
+
if (toolUseId)
|
|
1102
|
+
this.openPermissionPrompts.delete(toolUseId);
|
|
1103
|
+
open?.abort();
|
|
1104
|
+
this.emit({
|
|
1105
|
+
...event,
|
|
1106
|
+
type: 'permission.cancelled',
|
|
1107
|
+
promptClosed: Boolean(open),
|
|
1108
|
+
...(event.reason === 'answered' && !event.message
|
|
1109
|
+
? { message: 'answered on another surface' }
|
|
1110
|
+
: {}),
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
ask(prompt, signal) {
|
|
1045
1114
|
// A prompt raised after close has nobody to answer it, and re-creating the
|
|
1046
1115
|
// interface here would resurrect stdin for a run that is already over.
|
|
1047
|
-
if (this.closed)
|
|
1116
|
+
if (this.closed || signal?.aborted)
|
|
1048
1117
|
return Promise.resolve('');
|
|
1049
1118
|
if (!this.readline) {
|
|
1050
1119
|
this.readline = (0, node_readline_1.createInterface)({
|
|
@@ -1052,7 +1121,15 @@ class AgenticStreamClient {
|
|
|
1052
1121
|
output: process.stderr,
|
|
1053
1122
|
});
|
|
1054
1123
|
}
|
|
1055
|
-
|
|
1124
|
+
if (!signal) {
|
|
1125
|
+
return new Promise((resolve) => this.readline.question(prompt, resolve));
|
|
1126
|
+
}
|
|
1127
|
+
return new Promise((resolve) => {
|
|
1128
|
+
// readline does not call back for a question aborted by its signal, so
|
|
1129
|
+
// the abort settles the prompt itself.
|
|
1130
|
+
signal.addEventListener('abort', () => resolve(''), { once: true });
|
|
1131
|
+
this.readline.question(prompt, { signal }, resolve);
|
|
1132
|
+
});
|
|
1056
1133
|
}
|
|
1057
1134
|
/**
|
|
1058
1135
|
* True the first time this (kind, identity) pair is seen; false afterwards.
|
|
@@ -1126,7 +1203,7 @@ class AgenticStreamClient {
|
|
|
1126
1203
|
this.renderedText = this.accumulated;
|
|
1127
1204
|
}
|
|
1128
1205
|
else if (event.type === 'tool.completed') {
|
|
1129
|
-
output.write(
|
|
1206
|
+
output.write(`${describeToolEnd(event.toolName, event.status)}\n`);
|
|
1130
1207
|
this.renderedText = this.accumulated;
|
|
1131
1208
|
}
|
|
1132
1209
|
else if (event.type === 'stream.gap') {
|
|
@@ -1153,6 +1230,15 @@ class AgenticStreamClient {
|
|
|
1153
1230
|
output.write(`\n[${event.type}] ${describePermission(event)}\n`);
|
|
1154
1231
|
this.renderedText = this.accumulated;
|
|
1155
1232
|
}
|
|
1233
|
+
else if (event.type === 'permission.cancelled') {
|
|
1234
|
+
// A request the run stopped waiting for (OSK-12197). Without this line
|
|
1235
|
+
// an expired approval looked exactly like one still being asked.
|
|
1236
|
+
const why = typeof event.message === 'string' && event.message
|
|
1237
|
+
? event.message
|
|
1238
|
+
: `closed (${String(event.reason ?? 'cancelled')})`;
|
|
1239
|
+
output.write(`\n[permission] ${String(event.toolName ?? 'tool')}: ${why}\n`);
|
|
1240
|
+
this.renderedText = this.accumulated;
|
|
1241
|
+
}
|
|
1156
1242
|
else if (event.type === 'question.requested') {
|
|
1157
1243
|
output.write(`\n[${event.type}]\n`);
|
|
1158
1244
|
this.renderedText = this.accumulated;
|
|
@@ -17,6 +17,26 @@
|
|
|
17
17
|
* says so (OSK-12135).
|
|
18
18
|
*/
|
|
19
19
|
export declare function plainCredentialEnvelopeWarning(args: readonly unknown[]): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* The one line an EXPECTED credential-key change prints (OSK-12186).
|
|
22
|
+
*
|
|
23
|
+
* When the key that wraps this profile's credential key changes underneath it
|
|
24
|
+
* — a Dedicated guest moved to a new image gets a new `/etc/machine-id`, which
|
|
25
|
+
* the Linux key is derived from; a new Mac or a reset Keychain does the same —
|
|
26
|
+
* auth-core cannot unwrap the old key, enrolls a new one and carries on. That
|
|
27
|
+
* is recovery, not damage, but it reached the terminal as four warnings: the
|
|
28
|
+
* unwrap failure, the rotation notice, and one decrypt failure per credential
|
|
29
|
+
* still sealed under the old key, each ending in a raw crypto string. The
|
|
30
|
+
* second run was clean, which is exactly what corruption does not look like.
|
|
31
|
+
*
|
|
32
|
+
* It is distinct from a GENUINELY unreadable credential (OSK-12180): there the
|
|
33
|
+
* key unwraps and one stored value still cannot be opened, nothing will fix it
|
|
34
|
+
* by itself, and that line names `skrr login`. Here the old values are
|
|
35
|
+
* unreadable BECAUSE the key was replaced, and the next step happens on its
|
|
36
|
+
* own when a local daemon can hand a credential over.
|
|
37
|
+
*/
|
|
38
|
+
export declare const CREDENTIAL_KEY_CHANGED_NOTICE: string;
|
|
39
|
+
export declare function createCredentialEnvelopeWarningPresenter(): (args: readonly unknown[]) => string | null;
|
|
20
40
|
type DedicatedGenerationPreparationOptions = {
|
|
21
41
|
machineUuid?: string;
|
|
22
42
|
configDir?: string;
|