@skrr-ai/cli 0.1.52 → 0.1.53
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/commands/agents/chat.js +2 -1
- package/dist/commands/agents/create.js +13 -4
- package/dist/commands/agents/update.d.ts +32 -0
- package/dist/commands/agents/update.js +65 -14
- package/dist/commands/tasks/approvals/list.d.ts +22 -0
- package/dist/commands/tasks/approvals/list.js +80 -3
- package/dist/commands/tasks/create.js +2 -1
- package/dist/commands/tasks/start.js +1 -1
- package/dist/commands/tasks/update.js +23 -3
- package/dist/lib/agent-config.d.ts +13 -0
- package/dist/lib/agent-config.js +32 -0
- package/dist/lib/agentic-stream.d.ts +1 -1
- package/dist/lib/agentic-stream.js +1 -1
- package/dist/lib/auth-storage.js +7 -3
- package/dist/lib/cred-envelope.d.ts +12 -1
- package/dist/lib/cred-envelope.js +20 -3
- package/dist/lib/dedicated-machines.d.ts +20 -2
- package/dist/lib/dedicated-machines.js +46 -5
- package/dist/lib/keychain.js +26 -1
- package/dist/lib/label-ref.d.ts +1 -1
- package/dist/lib/label-ref.js +7 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +13 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +9 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +13 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +8 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/oclif.manifest.json +35912 -35900
- package/package.json +1 -1
|
@@ -269,7 +269,8 @@ class AgentsChat extends base_command_1.BaseCommand {
|
|
|
269
269
|
'calls arrive as `tool.started` / `tool.completed`, once per `toolUseId`, on cloud ' +
|
|
270
270
|
'and local-harness runs alike. A `session.notice` with `notice: ' +
|
|
271
271
|
'"harness-session-reset"` means the local harness could not resume its previous ' +
|
|
272
|
-
'session and started a new one
|
|
272
|
+
'session and started a new one, which does not see earlier turns of the ' +
|
|
273
|
+
'conversation; `message` says why. A `permission.resolved` says ' +
|
|
273
274
|
'how a local harness permission request was decided: `decidedBy` in words, and ' +
|
|
274
275
|
'`basis` — `person` (with `surface`), `mode` (with `mode`), `policy` or ' +
|
|
275
276
|
'`session-grant` — beside the wire `by`.',
|
|
@@ -252,19 +252,23 @@ class AgentsCreate extends base_command_1.BaseCommand {
|
|
|
252
252
|
description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
|
|
253
253
|
options: [...agent_config_1.REASONING_TOOL_ESCAPES],
|
|
254
254
|
multiple: true,
|
|
255
|
-
deprecated: { message: agent_config_1.
|
|
255
|
+
deprecated: { message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('reasoning-tool-escape') },
|
|
256
256
|
hidden: true,
|
|
257
257
|
}),
|
|
258
258
|
'commitment-responsibility': core_1.Flags.string({
|
|
259
259
|
description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
|
|
260
260
|
options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
|
|
261
|
-
deprecated: {
|
|
261
|
+
deprecated: {
|
|
262
|
+
message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('commitment-responsibility'),
|
|
263
|
+
},
|
|
262
264
|
hidden: true,
|
|
263
265
|
}),
|
|
264
266
|
'commitment-responsibility-ceiling': core_1.Flags.string({
|
|
265
267
|
description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
|
|
266
268
|
options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
|
|
267
|
-
deprecated: {
|
|
269
|
+
deprecated: {
|
|
270
|
+
message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('commitment-responsibility-ceiling'),
|
|
271
|
+
},
|
|
268
272
|
hidden: true,
|
|
269
273
|
}),
|
|
270
274
|
'max-turns': core_1.Flags.integer({ description: 'Maximum turns in one agent run (1-500)' }),
|
|
@@ -393,7 +397,12 @@ class AgentsCreate extends base_command_1.BaseCommand {
|
|
|
393
397
|
// relationshipLoops is an input-only provisioning control. The server
|
|
394
398
|
// applies it by deciding which system commitments to seed, so it is not an
|
|
395
399
|
// Agent document field and is intentionally absent from the response.
|
|
396
|
-
const unapplied = unappliedPayloadKeys(payload, created, [
|
|
400
|
+
const unapplied = unappliedPayloadKeys(payload, created, [
|
|
401
|
+
'relationshipLoops',
|
|
402
|
+
...(0, agent_config_1.deprecatedFieldNames)(created),
|
|
403
|
+
]);
|
|
404
|
+
for (const warning of (0, agent_config_1.deprecatedFieldsWarnings)(created))
|
|
405
|
+
this.warn(warning);
|
|
397
406
|
if (unapplied.length > 0) {
|
|
398
407
|
this.warn(unappliedPayloadKeysWarning(unapplied));
|
|
399
408
|
}
|
|
@@ -18,6 +18,7 @@ export default class AgentsUpdate extends BaseCommand {
|
|
|
18
18
|
'next-target': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
19
19
|
'working-directory': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
20
20
|
'default-permission-mode': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
21
|
+
'clear-default-permission-mode': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
21
22
|
tools: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
22
23
|
'model-parameters': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
23
24
|
'agent-ids': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
@@ -55,6 +56,27 @@ export default class AgentsUpdate extends BaseCommand {
|
|
|
55
56
|
};
|
|
56
57
|
run(): Promise<void>;
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The warnings an `agents update` prints for what the server did with the
|
|
61
|
+
* PATCH, in order: one line per field the server reported as ignored, with the
|
|
62
|
+
* SERVER's reason (`deprecatedFields`, OSK-12068), then one line for keys that
|
|
63
|
+
* came back missing for no stated reason.
|
|
64
|
+
*
|
|
65
|
+
* That last check is the one `agents create` makes. Without it, a key the
|
|
66
|
+
* server does not model, a typo of one that it does (`max_turns` for
|
|
67
|
+
* `maxTurns`), or a field only the server sets was discarded with the same
|
|
68
|
+
* "Updated agent" line as a real change. It skips:
|
|
69
|
+
* - `removeProjectIds`, an instruction rather than a stored field;
|
|
70
|
+
* - every field the server already explained in `deprecatedFields`, so it is
|
|
71
|
+
* not ALSO called "not an agent field" with the generic guess;
|
|
72
|
+
* - every key sent as `null`. `null` is a clear, and a cleared field coming
|
|
73
|
+
* back absent (or `null`) is the clear working. `defaultPermissionMode: null`
|
|
74
|
+
* through `--from-json` was reported as "not an agent field" after the server
|
|
75
|
+
* had cleared it on purpose (OSK-12167).
|
|
76
|
+
*
|
|
77
|
+
* Exported for tests.
|
|
78
|
+
*/
|
|
79
|
+
export declare function agentUpdateWarnings(payload: Record<string, unknown>, updated: unknown): string[];
|
|
58
80
|
/**
|
|
59
81
|
* The per-Agent mutation fence's "another request holds this right now"
|
|
60
82
|
* refusals. `updateAgent` takes a short lease on the Agent before it writes, so
|
|
@@ -78,6 +100,16 @@ export declare const AGENT_FENCE_RETRY_CODES: ReadonlySet<string>;
|
|
|
78
100
|
export declare function retryOnceOnAgentFence<T>(operation: () => Promise<T>, { delayMs }?: {
|
|
79
101
|
delayMs?: number;
|
|
80
102
|
}): Promise<T>;
|
|
103
|
+
/**
|
|
104
|
+
* The `defaultPermissionMode` a PATCH carries for these flags. `null` is the
|
|
105
|
+
* server's clear, the same spelling `harnesses update --clear-default-permission-mode`
|
|
106
|
+
* sends; before it, a mode once set could not be removed from the CLI at all —
|
|
107
|
+
* `""` fails the flag's options and `null` failed the server (OSK-12070).
|
|
108
|
+
*/
|
|
109
|
+
export declare function defaultPermissionModePatch(flags: {
|
|
110
|
+
'default-permission-mode'?: string;
|
|
111
|
+
'clear-default-permission-mode'?: boolean;
|
|
112
|
+
}): string | null | undefined;
|
|
81
113
|
/**
|
|
82
114
|
* What changing the agent's default permission mode actually changed
|
|
83
115
|
* (OSK-12124). It is the AGENT's default, so it governs every conversation,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AGENT_FENCE_RETRY_CODES = void 0;
|
|
4
|
+
exports.agentUpdateWarnings = agentUpdateWarnings;
|
|
4
5
|
exports.retryOnceOnAgentFence = retryOnceOnAgentFence;
|
|
6
|
+
exports.defaultPermissionModePatch = defaultPermissionModePatch;
|
|
5
7
|
exports.defaultPermissionModeNote = defaultPermissionModeNote;
|
|
6
8
|
const core_1 = require("@oclif/core");
|
|
7
9
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
@@ -58,6 +60,12 @@ class AgentsUpdate extends base_command_1.BaseCommand {
|
|
|
58
60
|
'One-shot harnesses (gemini, opencode, cursor, copilot, goose, pi, openclaw) can only run as ' +
|
|
59
61
|
'bypassPermissions; any other mode is refused for them.',
|
|
60
62
|
options: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
|
|
63
|
+
exclusive: ['clear-default-permission-mode'],
|
|
64
|
+
}),
|
|
65
|
+
'clear-default-permission-mode': core_1.Flags.boolean({
|
|
66
|
+
description: "Remove this agent's permission-mode override, so its harness's default (then the daemon's) " +
|
|
67
|
+
'applies. Allowed on every harness, including one-shot ones.',
|
|
68
|
+
exclusive: ['default-permission-mode'],
|
|
61
69
|
}),
|
|
62
70
|
tools: core_1.Flags.string({
|
|
63
71
|
description: 'Tools JSON array of plugin keys, e.g. \'["calculator"]\'. `skrr agents tools` lists the keys, ' +
|
|
@@ -107,19 +115,23 @@ class AgentsUpdate extends base_command_1.BaseCommand {
|
|
|
107
115
|
description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
|
|
108
116
|
options: [...agent_config_1.REASONING_TOOL_ESCAPES],
|
|
109
117
|
multiple: true,
|
|
110
|
-
deprecated: { message: agent_config_1.
|
|
118
|
+
deprecated: { message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('reasoning-tool-escape') },
|
|
111
119
|
hidden: true,
|
|
112
120
|
}),
|
|
113
121
|
'commitment-responsibility': core_1.Flags.string({
|
|
114
122
|
description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
|
|
115
123
|
options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
|
|
116
|
-
deprecated: {
|
|
124
|
+
deprecated: {
|
|
125
|
+
message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('commitment-responsibility'),
|
|
126
|
+
},
|
|
117
127
|
hidden: true,
|
|
118
128
|
}),
|
|
119
129
|
'commitment-responsibility-ceiling': core_1.Flags.string({
|
|
120
130
|
description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
|
|
121
131
|
options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
|
|
122
|
-
deprecated: {
|
|
132
|
+
deprecated: {
|
|
133
|
+
message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('commitment-responsibility-ceiling'),
|
|
134
|
+
},
|
|
123
135
|
hidden: true,
|
|
124
136
|
}),
|
|
125
137
|
'max-turns': core_1.Flags.integer({ description: 'Maximum turns in one agent run (1-500)' }),
|
|
@@ -177,7 +189,7 @@ class AgentsUpdate extends base_command_1.BaseCommand {
|
|
|
177
189
|
category: flags.category,
|
|
178
190
|
executionPolicy,
|
|
179
191
|
workingDirectory: flags['working-directory'],
|
|
180
|
-
defaultPermissionMode: flags
|
|
192
|
+
defaultPermissionMode: defaultPermissionModePatch(flags),
|
|
181
193
|
});
|
|
182
194
|
// `mergeAgentConfigFlags` parses the typed flags (avatar, scene background,
|
|
183
195
|
// working hours, every JSON flag). A parse failure is the caller's input, so
|
|
@@ -206,16 +218,8 @@ class AgentsUpdate extends base_command_1.BaseCommand {
|
|
|
206
218
|
catch (err) {
|
|
207
219
|
this.handleApiError(err);
|
|
208
220
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// field only the server sets was discarded with the same "Updated agent"
|
|
212
|
-
// line as a real change.
|
|
213
|
-
// `removeProjectIds` is an instruction, not a stored field, so it never
|
|
214
|
-
// comes back and is not reported.
|
|
215
|
-
const unapplied = (0, create_1.unappliedPayloadKeys)(payload, updated, ['removeProjectIds']);
|
|
216
|
-
if (unapplied.length > 0) {
|
|
217
|
-
this.warn((0, create_1.unappliedPayloadKeysWarning)(unapplied));
|
|
218
|
-
}
|
|
221
|
+
for (const warning of agentUpdateWarnings(payload, updated))
|
|
222
|
+
this.warn(warning);
|
|
219
223
|
await (0, runtime_tool_support_1.warnPluginsNotLoaded)(payload.tools, (message) => this.warn(message));
|
|
220
224
|
if (flags.json) {
|
|
221
225
|
this.log(JSON.stringify((0, web_url_1.withAgentUrl)(this.cliConfig.baseURL, updated), null, 2));
|
|
@@ -227,9 +231,45 @@ class AgentsUpdate extends base_command_1.BaseCommand {
|
|
|
227
231
|
const modeNote = defaultPermissionModeNote(flags['default-permission-mode'], a);
|
|
228
232
|
if (modeNote)
|
|
229
233
|
this.log(modeNote);
|
|
234
|
+
if (flags['clear-default-permission-mode']) {
|
|
235
|
+
this.log("Permission: cleared — this agent now follows its harness's default permission mode " +
|
|
236
|
+
"(then the daemon's), starting with each conversation's next turn.");
|
|
237
|
+
}
|
|
230
238
|
}
|
|
231
239
|
}
|
|
232
240
|
exports.default = AgentsUpdate;
|
|
241
|
+
/**
|
|
242
|
+
* The warnings an `agents update` prints for what the server did with the
|
|
243
|
+
* PATCH, in order: one line per field the server reported as ignored, with the
|
|
244
|
+
* SERVER's reason (`deprecatedFields`, OSK-12068), then one line for keys that
|
|
245
|
+
* came back missing for no stated reason.
|
|
246
|
+
*
|
|
247
|
+
* That last check is the one `agents create` makes. Without it, a key the
|
|
248
|
+
* server does not model, a typo of one that it does (`max_turns` for
|
|
249
|
+
* `maxTurns`), or a field only the server sets was discarded with the same
|
|
250
|
+
* "Updated agent" line as a real change. It skips:
|
|
251
|
+
* - `removeProjectIds`, an instruction rather than a stored field;
|
|
252
|
+
* - every field the server already explained in `deprecatedFields`, so it is
|
|
253
|
+
* not ALSO called "not an agent field" with the generic guess;
|
|
254
|
+
* - every key sent as `null`. `null` is a clear, and a cleared field coming
|
|
255
|
+
* back absent (or `null`) is the clear working. `defaultPermissionMode: null`
|
|
256
|
+
* through `--from-json` was reported as "not an agent field" after the server
|
|
257
|
+
* had cleared it on purpose (OSK-12167).
|
|
258
|
+
*
|
|
259
|
+
* Exported for tests.
|
|
260
|
+
*/
|
|
261
|
+
function agentUpdateWarnings(payload, updated) {
|
|
262
|
+
const cleared = Object.keys(payload).filter((key) => payload[key] === null);
|
|
263
|
+
const unapplied = (0, create_1.unappliedPayloadKeys)(payload, updated, [
|
|
264
|
+
'removeProjectIds',
|
|
265
|
+
...cleared,
|
|
266
|
+
...(0, agent_config_1.deprecatedFieldNames)(updated),
|
|
267
|
+
]);
|
|
268
|
+
return [
|
|
269
|
+
...(0, agent_config_1.deprecatedFieldsWarnings)(updated),
|
|
270
|
+
...(unapplied.length > 0 ? [(0, create_1.unappliedPayloadKeysWarning)(unapplied)] : []),
|
|
271
|
+
];
|
|
272
|
+
}
|
|
233
273
|
/**
|
|
234
274
|
* The per-Agent mutation fence's "another request holds this right now"
|
|
235
275
|
* refusals. `updateAgent` takes a short lease on the Agent before it writes, so
|
|
@@ -267,6 +307,17 @@ async function retryOnceOnAgentFence(operation, { delayMs = 750 } = {}) {
|
|
|
267
307
|
return operation();
|
|
268
308
|
}
|
|
269
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* The `defaultPermissionMode` a PATCH carries for these flags. `null` is the
|
|
312
|
+
* server's clear, the same spelling `harnesses update --clear-default-permission-mode`
|
|
313
|
+
* sends; before it, a mode once set could not be removed from the CLI at all —
|
|
314
|
+
* `""` fails the flag's options and `null` failed the server (OSK-12070).
|
|
315
|
+
*/
|
|
316
|
+
function defaultPermissionModePatch(flags) {
|
|
317
|
+
if (flags['clear-default-permission-mode'])
|
|
318
|
+
return null;
|
|
319
|
+
return flags['default-permission-mode'];
|
|
320
|
+
}
|
|
270
321
|
/**
|
|
271
322
|
* What changing the agent's default permission mode actually changed
|
|
272
323
|
* (OSK-12124). It is the AGENT's default, so it governs every conversation,
|
|
@@ -14,6 +14,28 @@ export declare function approvalRow(a: Record<string, unknown>): {
|
|
|
14
14
|
approver: string;
|
|
15
15
|
created: string;
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* OSK-12165 — the task's latest run, when it ended blocked on a permission
|
|
19
|
+
* nobody could grant.
|
|
20
|
+
*
|
|
21
|
+
* An unattended run (`tasks start`) cannot ask anyone: the permission bridge
|
|
22
|
+
* declines every ask at once, so no approval row is ever created, and this
|
|
23
|
+
* command used to answer "No approvals." in exactly the case `tasks start`
|
|
24
|
+
* sends people here for. The evidence lives on the run, not in the approvals
|
|
25
|
+
* collection: the leaf finalizer settles it with `error.code`
|
|
26
|
+
* `permission_blocked`, which `GET /tasks/:id/runs` projects as `failureCode`
|
|
27
|
+
* alongside the sentence (what was refused, and the fix) as `failureReason`.
|
|
28
|
+
*
|
|
29
|
+
* Only the LATEST run counts (the route sorts newest first): a permission block
|
|
30
|
+
* on an older run that a later run got past is history, not the current state.
|
|
31
|
+
*/
|
|
32
|
+
export declare function permissionBlockedRun(runs: Array<Record<string, unknown>>): Record<string, unknown> | null;
|
|
33
|
+
/**
|
|
34
|
+
* What to print for such a run: why it is blocked, what needed approval (the
|
|
35
|
+
* run's own recorded sentence, which names the refused tool), that an
|
|
36
|
+
* unattended run cannot prompt, and the two ways forward with exact commands.
|
|
37
|
+
*/
|
|
38
|
+
export declare function permissionBlockLines(run: Record<string, unknown>, bin?: string, taskRef?: string): string[];
|
|
17
39
|
export default class TasksApprovalsList extends BaseCommand {
|
|
18
40
|
static description: string;
|
|
19
41
|
static examples: string[];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.approvalRow = approvalRow;
|
|
4
|
+
exports.permissionBlockedRun = permissionBlockedRun;
|
|
5
|
+
exports.permissionBlockLines = permissionBlockLines;
|
|
4
6
|
const core_1 = require("@oclif/core");
|
|
5
7
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
6
8
|
const base_command_1 = require("../../../base-command");
|
|
@@ -22,6 +24,52 @@ function approvalRow(a) {
|
|
|
22
24
|
created: a.createdAt ? (0, format_1.localTime)(a.createdAt) : '-',
|
|
23
25
|
};
|
|
24
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* OSK-12165 — the task's latest run, when it ended blocked on a permission
|
|
29
|
+
* nobody could grant.
|
|
30
|
+
*
|
|
31
|
+
* An unattended run (`tasks start`) cannot ask anyone: the permission bridge
|
|
32
|
+
* declines every ask at once, so no approval row is ever created, and this
|
|
33
|
+
* command used to answer "No approvals." in exactly the case `tasks start`
|
|
34
|
+
* sends people here for. The evidence lives on the run, not in the approvals
|
|
35
|
+
* collection: the leaf finalizer settles it with `error.code`
|
|
36
|
+
* `permission_blocked`, which `GET /tasks/:id/runs` projects as `failureCode`
|
|
37
|
+
* alongside the sentence (what was refused, and the fix) as `failureReason`.
|
|
38
|
+
*
|
|
39
|
+
* Only the LATEST run counts (the route sorts newest first): a permission block
|
|
40
|
+
* on an older run that a later run got past is history, not the current state.
|
|
41
|
+
*/
|
|
42
|
+
function permissionBlockedRun(runs) {
|
|
43
|
+
const latest = runs[0];
|
|
44
|
+
if (!latest)
|
|
45
|
+
return null;
|
|
46
|
+
const code = latest.failureCode ?? latest.failureClass;
|
|
47
|
+
return code === data_provider_1.PERMISSION_BLOCKED_FAILURE_CODE ? latest : null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* What to print for such a run: why it is blocked, what needed approval (the
|
|
51
|
+
* run's own recorded sentence, which names the refused tool), that an
|
|
52
|
+
* unattended run cannot prompt, and the two ways forward with exact commands.
|
|
53
|
+
*/
|
|
54
|
+
function permissionBlockLines(run, bin = 'skrr', taskRef = '<task-id>') {
|
|
55
|
+
const runId = String(run.runId ?? run.id ?? '-');
|
|
56
|
+
const reason = typeof run.failureReason === 'string' ? run.failureReason.trim() : '';
|
|
57
|
+
const executor = run.executor;
|
|
58
|
+
const agent = executor && typeof executor.agentId === 'string' && executor.agentId
|
|
59
|
+
? executor.agentId
|
|
60
|
+
: '<agent>';
|
|
61
|
+
return [
|
|
62
|
+
`The latest run (${runId}) is blocked on a permission, not on an approval request.`,
|
|
63
|
+
...(reason ? [` ${reason}`] : []),
|
|
64
|
+
'It ran unattended (`tasks start`), and an unattended run cannot prompt anyone to approve a',
|
|
65
|
+
'tool call, so the ask was refused on the spot and no approval request was recorded here.',
|
|
66
|
+
'',
|
|
67
|
+
'To continue, either:',
|
|
68
|
+
` ${bin} agents chat ${agent} --approve-tools --prompt "..." do the work interactively, approving each call`,
|
|
69
|
+
` ${bin} agents update ${agent} --default-permission-mode bypassPermissions let unattended runs use the tool`,
|
|
70
|
+
` then: ${bin} tasks start ${taskRef} (or pick an agent/harness whose mode allows it)`,
|
|
71
|
+
];
|
|
72
|
+
}
|
|
25
73
|
class TasksApprovalsList extends base_command_1.BaseCommand {
|
|
26
74
|
static description = 'List approvals for a task';
|
|
27
75
|
static examples = [
|
|
@@ -44,14 +92,38 @@ class TasksApprovalsList extends base_command_1.BaseCommand {
|
|
|
44
92
|
catch (err) {
|
|
45
93
|
this.handleApiError(err);
|
|
46
94
|
}
|
|
95
|
+
const r = response;
|
|
96
|
+
const items = r?.data ?? [];
|
|
97
|
+
// OSK-12165 — an unattended run blocked on a permission leaves no approval
|
|
98
|
+
// row, so the empty list alone would say "nothing to act on" about a task
|
|
99
|
+
// that is waiting on exactly that. Read the latest run, best-effort: the
|
|
100
|
+
// approvals list is still the answer if the runs read fails.
|
|
101
|
+
let blockedRun = null;
|
|
102
|
+
try {
|
|
103
|
+
const runs = (await data_provider_1.dataService.getTaskRuns(args.id));
|
|
104
|
+
blockedRun = permissionBlockedRun(runs?.data ?? []);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
blockedRun = null;
|
|
108
|
+
}
|
|
109
|
+
const blockLines = blockedRun ? permissionBlockLines(blockedRun, this.config.bin, args.id) : [];
|
|
47
110
|
if (flags.json) {
|
|
111
|
+
// The server payload stays byte-identical; the pointer goes to stderr so
|
|
112
|
+
// `--json` remains parseable and a script still learns the task is stuck.
|
|
113
|
+
if (blockedRun) {
|
|
114
|
+
this.warn(`The latest run is blocked on a permission an unattended run could not ask for; ` +
|
|
115
|
+
`\`${this.config.bin} tasks approvals list ${args.id}\` (without --json) says what to do.`);
|
|
116
|
+
}
|
|
48
117
|
this.log(JSON.stringify(response, null, 2));
|
|
49
118
|
return;
|
|
50
119
|
}
|
|
51
|
-
const r = response;
|
|
52
|
-
const items = r?.data ?? [];
|
|
53
120
|
if (items.length === 0) {
|
|
54
|
-
this.log('No
|
|
121
|
+
this.log('No approval requests.');
|
|
122
|
+
if (blockLines.length > 0) {
|
|
123
|
+
this.log('');
|
|
124
|
+
for (const line of blockLines)
|
|
125
|
+
this.log(line);
|
|
126
|
+
}
|
|
55
127
|
return;
|
|
56
128
|
}
|
|
57
129
|
const rows = items.map(approvalRow);
|
|
@@ -62,6 +134,11 @@ class TasksApprovalsList extends base_command_1.BaseCommand {
|
|
|
62
134
|
{ key: 'approver', header: 'APPROVER', maxWidth: 30 },
|
|
63
135
|
{ key: 'created', header: 'CREATED' },
|
|
64
136
|
], (line) => this.log(line));
|
|
137
|
+
if (blockLines.length > 0) {
|
|
138
|
+
this.log('');
|
|
139
|
+
for (const line of blockLines)
|
|
140
|
+
this.log(line);
|
|
141
|
+
}
|
|
65
142
|
}
|
|
66
143
|
}
|
|
67
144
|
exports.default = TasksApprovalsList;
|
|
@@ -238,7 +238,8 @@ class TasksCreate extends base_command_1.BaseCommand {
|
|
|
238
238
|
'label-id': core_1.Flags.string({
|
|
239
239
|
aliases: ['label'],
|
|
240
240
|
description: 'Attach a task label (repeatable) — id, or a name from `skrr labels list --applicable-to task`. ' +
|
|
241
|
-
'An unknown name is refused, never created.
|
|
241
|
+
'An unknown name is refused, never created. Also accepted as --label. ' +
|
|
242
|
+
'Manage labels with `skrr labels`.',
|
|
242
243
|
multiple: true,
|
|
243
244
|
}),
|
|
244
245
|
'working-directory': core_1.Flags.string({
|
|
@@ -137,7 +137,7 @@ function startFollowUpLines(bin, taskRef) {
|
|
|
137
137
|
'Follow it:',
|
|
138
138
|
` ${bin} tasks watch ${taskRef} status until it finishes or fails`,
|
|
139
139
|
` ${bin} tasks transcript ${taskRef} what the agent has said so far`,
|
|
140
|
-
` ${bin} tasks approvals list ${taskRef}
|
|
140
|
+
` ${bin} tasks approvals list ${taskRef} approvals waiting on you, or why a run is blocked on one`,
|
|
141
141
|
'On a local machine, the run may add its instruction file (AGENTS.md / CLAUDE.md) to the',
|
|
142
142
|
'working directory while it runs; files the run wrote are removed afterwards unless changed.',
|
|
143
143
|
];
|
|
@@ -163,7 +163,8 @@ class TasksUpdate extends base_command_1.BaseCommand {
|
|
|
163
163
|
}),
|
|
164
164
|
'label-id': core_1.Flags.string({
|
|
165
165
|
aliases: ['label'],
|
|
166
|
-
description: 'Replace the task labels
|
|
166
|
+
description: 'Replace the task labels (repeatable, whole-list) — id, or a name from ' +
|
|
167
|
+
'`skrr labels list --applicable-to task`. Also accepted as --label. ' +
|
|
167
168
|
'Use `skrr tasks labels attach/detach` for incremental changes.',
|
|
168
169
|
multiple: true,
|
|
169
170
|
}),
|
|
@@ -322,6 +323,25 @@ class TasksUpdate extends base_command_1.BaseCommand {
|
|
|
322
323
|
}
|
|
323
324
|
}
|
|
324
325
|
}
|
|
326
|
+
// Same resolution as `tasks create`: the `--tag` warning below suggests
|
|
327
|
+
// `--label-id <name>`, so this flag has to accept the name it suggests
|
|
328
|
+
// (OSK-12166). Ids pass through untouched.
|
|
329
|
+
let resolvedLabelIds;
|
|
330
|
+
if (flags['label-id'] !== undefined) {
|
|
331
|
+
try {
|
|
332
|
+
resolvedLabelIds = await (0, label_ref_1.resolveLabelRefs)(flags['label-id'], {
|
|
333
|
+
applicableTo: 'task',
|
|
334
|
+
...(process.env.OVERSKY_WORKSPACE_ID
|
|
335
|
+
? { workspaceId: process.env.OVERSKY_WORKSPACE_ID }
|
|
336
|
+
: {}),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
if (err instanceof label_ref_1.LabelRefError)
|
|
341
|
+
this.error(err.message, { exit: 1 });
|
|
342
|
+
this.handleApiError(err);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
325
345
|
const payload = (0, prompt_1.mergeDefined)((0, task_resolver_1.stripTaskIdentityFields)(body), {
|
|
326
346
|
title: flags.title,
|
|
327
347
|
expectations: expectationsPatch === null ? [] : (expectationsPatch ?? undefined),
|
|
@@ -353,7 +373,7 @@ class TasksUpdate extends base_command_1.BaseCommand {
|
|
|
353
373
|
addBlocksIds: flags['add-blocks'],
|
|
354
374
|
removeBlocksIds: flags['remove-blocks'],
|
|
355
375
|
tags: flags.tag,
|
|
356
|
-
labelIds:
|
|
376
|
+
labelIds: resolvedLabelIds,
|
|
357
377
|
// Worktree isolation override. --worktree ⇒ 'ephemeral', --no-worktree ⇒
|
|
358
378
|
// 'shared', omitted ⇒ unchanged. (Clear-to-inherit via --from-json.)
|
|
359
379
|
executionIsolation: (0, tasks_2.executionIsolationFromWorktreeFlag)(flags.worktree),
|
|
@@ -448,7 +468,7 @@ class TasksUpdate extends base_command_1.BaseCommand {
|
|
|
448
468
|
const estimateWarning = (0, create_1.droppedEstimateWarning)(flags.estimate, updated, updated?.spaceId);
|
|
449
469
|
if (estimateWarning)
|
|
450
470
|
this.warn(estimateWarning);
|
|
451
|
-
const tagWarning = await (0, create_1.taskTagLabelWarning)(flags.tag,
|
|
471
|
+
const tagWarning = await (0, create_1.taskTagLabelWarning)(flags.tag, resolvedLabelIds);
|
|
452
472
|
if (tagWarning)
|
|
453
473
|
this.warn(tagWarning);
|
|
454
474
|
if (flags.json) {
|
|
@@ -4,6 +4,19 @@ export declare const REASONING_TOOL_ESCAPES: readonly ["bash", "browser"];
|
|
|
4
4
|
export declare const COMMITMENT_PERMISSION_FLAG_RETIRED = "This setting no longer has any effect: Commitment runs use the agent's own permissions.";
|
|
5
5
|
/** The three retired flags themselves: accepted at parse, warned on, never sent. */
|
|
6
6
|
export declare const RETIRED_COMMITMENT_PERMISSION_FLAGS: readonly ["reasoning-tool-escape", "commitment-responsibility", "commitment-responsibility-ceiling"];
|
|
7
|
+
/**
|
|
8
|
+
* The deprecation warning ONE retired flag prints. oclif prints a
|
|
9
|
+
* `deprecated.message` verbatim, so a shared message printed the same line
|
|
10
|
+
* once per flag with nothing saying which flag each one was about (OSK-12068).
|
|
11
|
+
*/
|
|
12
|
+
export declare function retiredCommitmentPermissionFlagMessage(flag: (typeof RETIRED_COMMITMENT_PERMISSION_FLAGS)[number]): string;
|
|
13
|
+
/**
|
|
14
|
+
* The fields the server ignored and why, from a write's `deprecatedFields`
|
|
15
|
+
* notice, one line each. Empty when the response carries none.
|
|
16
|
+
*/
|
|
17
|
+
export declare function deprecatedFieldsWarnings(response: unknown): string[];
|
|
18
|
+
/** The payload keys the server reported as ignored, so they are not also called "not kept". */
|
|
19
|
+
export declare function deprecatedFieldNames(response: unknown): string[];
|
|
7
20
|
export declare const RELEASE_INSTALL_CHANNELS: readonly ["latest", "manual"];
|
|
8
21
|
/**
|
|
9
22
|
* `09:00-18:00` or `09:00-18:00@America/Los_Angeles`, or a JSON object.
|
package/dist/lib/agent-config.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RELEASE_INSTALL_CHANNELS = exports.RETIRED_COMMITMENT_PERMISSION_FLAGS = exports.COMMITMENT_PERMISSION_FLAG_RETIRED = exports.REASONING_TOOL_ESCAPES = exports.COMMITMENT_RESPONSIBILITY_LEVELS = void 0;
|
|
4
|
+
exports.retiredCommitmentPermissionFlagMessage = retiredCommitmentPermissionFlagMessage;
|
|
5
|
+
exports.deprecatedFieldsWarnings = deprecatedFieldsWarnings;
|
|
6
|
+
exports.deprecatedFieldNames = deprecatedFieldNames;
|
|
4
7
|
exports.parseWorkingHoursFlag = parseWorkingHoursFlag;
|
|
5
8
|
exports.mergeAgentConfigFlags = mergeAgentConfigFlags;
|
|
6
9
|
const prompt_1 = require("./prompt");
|
|
@@ -15,6 +18,35 @@ exports.RETIRED_COMMITMENT_PERMISSION_FLAGS = [
|
|
|
15
18
|
'commitment-responsibility',
|
|
16
19
|
'commitment-responsibility-ceiling',
|
|
17
20
|
];
|
|
21
|
+
/**
|
|
22
|
+
* The deprecation warning ONE retired flag prints. oclif prints a
|
|
23
|
+
* `deprecated.message` verbatim, so a shared message printed the same line
|
|
24
|
+
* once per flag with nothing saying which flag each one was about (OSK-12068).
|
|
25
|
+
*/
|
|
26
|
+
function retiredCommitmentPermissionFlagMessage(flag) {
|
|
27
|
+
return `--${flag} is retired and was not sent. ${exports.COMMITMENT_PERMISSION_FLAG_RETIRED}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The fields the server ignored and why, from a write's `deprecatedFields`
|
|
31
|
+
* notice, one line each. Empty when the response carries none.
|
|
32
|
+
*/
|
|
33
|
+
function deprecatedFieldsWarnings(response) {
|
|
34
|
+
const entries = response?.deprecatedFields;
|
|
35
|
+
if (!Array.isArray(entries))
|
|
36
|
+
return [];
|
|
37
|
+
return entries
|
|
38
|
+
.filter((entry) => Boolean(entry) && typeof entry.field === 'string')
|
|
39
|
+
.map((entry) => `${entry.field}: ${entry.reason || 'ignored by the server'}`);
|
|
40
|
+
}
|
|
41
|
+
/** The payload keys the server reported as ignored, so they are not also called "not kept". */
|
|
42
|
+
function deprecatedFieldNames(response) {
|
|
43
|
+
const entries = response?.deprecatedFields;
|
|
44
|
+
return Array.isArray(entries)
|
|
45
|
+
? entries
|
|
46
|
+
.map((entry) => entry?.field)
|
|
47
|
+
.filter((field) => typeof field === 'string')
|
|
48
|
+
: [];
|
|
49
|
+
}
|
|
18
50
|
exports.RELEASE_INSTALL_CHANNELS = ['latest', 'manual'];
|
|
19
51
|
/**
|
|
20
52
|
* `09:00-18:00` or `09:00-18:00@America/Los_Angeles`, or a JSON object.
|
|
@@ -299,7 +299,7 @@ export type SeqGap = {
|
|
|
299
299
|
* `service` envelopes that carry a `notice` — words for the person, not
|
|
300
300
|
* diagnostics — projected as `session.notice` events. Today the only notice is
|
|
301
301
|
* `harness-session-reset`: the local harness could not resume its previous
|
|
302
|
-
* session and began a new one,
|
|
302
|
+
* session and began a new one, which does not see earlier turns (OSK-12155).
|
|
303
303
|
*/
|
|
304
304
|
export declare function projectSessionNotices(envelopes: unknown[]): AgenticStreamEvent[];
|
|
305
305
|
export declare function detectSeqGap(highestApplied: number, incoming: number): SeqGap | null;
|
|
@@ -426,7 +426,7 @@ function jitteredRetryDelay(retryAfterMs, random = Math.random) {
|
|
|
426
426
|
* `service` envelopes that carry a `notice` — words for the person, not
|
|
427
427
|
* diagnostics — projected as `session.notice` events. Today the only notice is
|
|
428
428
|
* `harness-session-reset`: the local harness could not resume its previous
|
|
429
|
-
* session and began a new one,
|
|
429
|
+
* session and began a new one, which does not see earlier turns (OSK-12155).
|
|
430
430
|
*/
|
|
431
431
|
function projectSessionNotices(envelopes) {
|
|
432
432
|
const out = [];
|
package/dist/lib/auth-storage.js
CHANGED
|
@@ -288,7 +288,12 @@ function readFileAt(p, upgradeInPlace) {
|
|
|
288
288
|
// L12 — route the secret string fields through the envelope. Mixed
|
|
289
289
|
// bundles (e.g., legacy plaintext token + freshly-rotated wrapped
|
|
290
290
|
// refresh) decrypt independently so partial migrations stay safe.
|
|
291
|
-
|
|
291
|
+
// Read the (plaintext) owner first, so a value another config root sealed
|
|
292
|
+
// under its own key fails with a sentence that says so (OSK-12135).
|
|
293
|
+
const owner = parseOwner(parsed.owner);
|
|
294
|
+
const ownerRoot = owner?.configRoot ? path.resolve(owner.configRoot) : undefined;
|
|
295
|
+
const decryptOptions = ownerRoot && ownerRoot !== path.resolve((0, config_1.configRoot)()) ? { foreignOwnerRoot: ownerRoot } : {};
|
|
296
|
+
const tokenResult = (0, cred_envelope_1.maybeDecryptOnRead)(parsed.token, decryptOptions);
|
|
292
297
|
if (tokenResult.plaintext === null) {
|
|
293
298
|
// Envelope on disk but no DEK to unwrap → treat as missing so the
|
|
294
299
|
// caller falls through to a clean `skrr login`.
|
|
@@ -300,7 +305,7 @@ function readFileAt(p, upgradeInPlace) {
|
|
|
300
305
|
bundle.expiresAt = parsed.expiresAt;
|
|
301
306
|
}
|
|
302
307
|
if (typeof parsed.refreshToken === 'string' && parsed.refreshToken.length > 0) {
|
|
303
|
-
const refreshResult = (0, cred_envelope_1.maybeDecryptOnRead)(parsed.refreshToken);
|
|
308
|
+
const refreshResult = (0, cred_envelope_1.maybeDecryptOnRead)(parsed.refreshToken, decryptOptions);
|
|
304
309
|
if (refreshResult.plaintext !== null) {
|
|
305
310
|
bundle.refreshToken = refreshResult.plaintext;
|
|
306
311
|
if (refreshResult.needsMigration)
|
|
@@ -316,7 +321,6 @@ function readFileAt(p, upgradeInPlace) {
|
|
|
316
321
|
if (typeof parsed.serverOrigin === 'string' && parsed.serverOrigin.length > 0) {
|
|
317
322
|
bundle.serverOrigin = parsed.serverOrigin;
|
|
318
323
|
}
|
|
319
|
-
const owner = parseOwner(parsed.owner);
|
|
320
324
|
if (owner)
|
|
321
325
|
bundle.owner = owner;
|
|
322
326
|
// Best-effort upgrade: re-write the bundle so legacy plaintext fields
|
|
@@ -8,7 +8,18 @@
|
|
|
8
8
|
* and file backends and avoids a detect-then-read TOCTOU race.
|
|
9
9
|
*/
|
|
10
10
|
import { type ReadTransformResult } from '@skrr-ai/auth-core';
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The `decrypt_failed` hint for a value whose owner record names ANOTHER
|
|
13
|
+
* config root. Each root seals under its own key, so that value is unreadable
|
|
14
|
+
* here by construction: "signing in again rebuilds it" would send someone to
|
|
15
|
+
* re-login over a credential that is not broken and not theirs (OSK-12135).
|
|
16
|
+
*/
|
|
17
|
+
export declare function foreignCredentialHint(ownerConfigRoot: string): string;
|
|
18
|
+
export interface CliDecryptOnReadOptions {
|
|
19
|
+
/** The config root the stored value's owner record names, when not ours. */
|
|
20
|
+
foreignOwnerRoot?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function maybeDecryptOnRead(stored: string, options?: CliDecryptOnReadOptions): ReadTransformResult;
|
|
12
23
|
/** Consume and clear the per-process blocked-read observation. */
|
|
13
24
|
export declare function consumeEncryptedCredentialReadWhileInactive(): boolean;
|
|
14
25
|
/** Consume any unreadable envelope observation, including tamper/decrypt failure. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.__wrappedDekFilePathForTest = exports.maybeEncryptForWrite = exports.describeCredEnvelopeState = exports.isCredEnvelopeActive = exports.shutdownCredEnvelope = exports.resetCredEnvelope = exports.initCredEnvelope = void 0;
|
|
4
|
+
exports.foreignCredentialHint = foreignCredentialHint;
|
|
4
5
|
exports.maybeDecryptOnRead = maybeDecryptOnRead;
|
|
5
6
|
exports.consumeEncryptedCredentialReadWhileInactive = consumeEncryptedCredentialReadWhileInactive;
|
|
6
7
|
exports.consumeUnreadableEncryptedCredentialRead = consumeUnreadableEncryptedCredentialRead;
|
|
@@ -16,10 +17,26 @@ exports.consumeUnreadableEncryptedCredentialRead = consumeUnreadableEncryptedCre
|
|
|
16
17
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
17
18
|
let encryptedReadWhileInactive = false;
|
|
18
19
|
let unreadableEncryptedRead = false;
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The `decrypt_failed` hint for a value whose owner record names ANOTHER
|
|
22
|
+
* config root. Each root seals under its own key, so that value is unreadable
|
|
23
|
+
* here by construction: "signing in again rebuilds it" would send someone to
|
|
24
|
+
* re-login over a credential that is not broken and not theirs (OSK-12135).
|
|
25
|
+
*/
|
|
26
|
+
function foreignCredentialHint(ownerConfigRoot) {
|
|
27
|
+
return (`stored credential belongs to another skrr config (config root ${ownerConfigRoot}) and is ` +
|
|
28
|
+
"sealed under that config's key, so this config cannot read it. Nothing is wrong with " +
|
|
29
|
+
'either credential and it was left untouched; signing in here does not change it');
|
|
30
|
+
}
|
|
31
|
+
function maybeDecryptOnRead(stored, options = {}) {
|
|
32
|
+
const result = (0, auth_core_1.maybeDecryptOnRead)(stored, options.foreignOwnerRoot
|
|
33
|
+
? { unreadableHint: foreignCredentialHint(options.foreignOwnerRoot) }
|
|
34
|
+
: {});
|
|
21
35
|
if (result.plaintext === null && (0, auth_core_1.isCredEnvelopeString)(stored)) {
|
|
22
|
-
|
|
36
|
+
// Another config's value is not an unreadable credential of OURS, so it
|
|
37
|
+
// must not tell `logout` there is local encrypted state to erase.
|
|
38
|
+
if (!options.foreignOwnerRoot)
|
|
39
|
+
unreadableEncryptedRead = true;
|
|
23
40
|
if ((0, auth_core_1.describeCredEnvelopeState)().kind !== 'active') {
|
|
24
41
|
encryptedReadWhileInactive = true;
|
|
25
42
|
}
|