@skrr-ai/cli 0.1.50 → 0.1.52

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.
Files changed (131) hide show
  1. package/dist/base-command.d.ts +7 -0
  2. package/dist/base-command.js +95 -8
  3. package/dist/commands/agents/chat.d.ts +14 -0
  4. package/dist/commands/agents/chat.js +37 -1
  5. package/dist/commands/agents/create.js +8 -4
  6. package/dist/commands/agents/show.d.ts +9 -0
  7. package/dist/commands/agents/show.js +53 -0
  8. package/dist/commands/agents/update.d.ts +32 -0
  9. package/dist/commands/agents/update.js +81 -9
  10. package/dist/commands/balance/show.d.ts +34 -0
  11. package/dist/commands/balance/show.js +92 -2
  12. package/dist/commands/commitments/doctor.d.ts +1 -0
  13. package/dist/commands/commitments/doctor.js +19 -4
  14. package/dist/commands/commitments/effective-policy.js +15 -1
  15. package/dist/commands/commitments/next-tick.js +12 -2
  16. package/dist/commands/commitments/preflight.js +10 -0
  17. package/dist/commands/commitments/show.js +11 -1
  18. package/dist/commands/context.js +2 -1
  19. package/dist/commands/daemon/config.d.ts +15 -0
  20. package/dist/commands/daemon/config.js +53 -0
  21. package/dist/commands/daemon/index.js +1 -0
  22. package/dist/commands/daemon/status.js +15 -1
  23. package/dist/commands/doctor.js +8 -1
  24. package/dist/commands/harnesses/models.d.ts +54 -0
  25. package/dist/commands/harnesses/models.js +100 -7
  26. package/dist/commands/harnesses/show.d.ts +11 -0
  27. package/dist/commands/harnesses/show.js +35 -0
  28. package/dist/commands/harnesses/update.d.ts +28 -1
  29. package/dist/commands/harnesses/update.js +73 -10
  30. package/dist/commands/login.d.ts +2 -1
  31. package/dist/commands/login.js +26 -7
  32. package/dist/commands/logout.js +3 -0
  33. package/dist/commands/machines/dedicated/show.js +25 -2
  34. package/dist/commands/outbox/list.d.ts +12 -1
  35. package/dist/commands/outbox/list.js +31 -2
  36. package/dist/commands/revoke-daemon.js +9 -0
  37. package/dist/commands/spaces/create.d.ts +1 -0
  38. package/dist/commands/spaces/create.js +17 -0
  39. package/dist/commands/spaces/show.js +5 -0
  40. package/dist/commands/spaces/update.d.ts +1 -0
  41. package/dist/commands/spaces/update.js +16 -0
  42. package/dist/commands/tasks/create.d.ts +6 -0
  43. package/dist/commands/tasks/create.js +31 -6
  44. package/dist/commands/tasks/show.js +45 -0
  45. package/dist/commands/tasks/start.d.ts +14 -0
  46. package/dist/commands/tasks/start.js +31 -1
  47. package/dist/commands/tasks/update.d.ts +1 -0
  48. package/dist/commands/tasks/update.js +29 -1
  49. package/dist/commands/tasks/upsert.d.ts +1 -0
  50. package/dist/commands/tasks/upsert.js +16 -0
  51. package/dist/commands/whoami.js +5 -3
  52. package/dist/lib/agent-config.d.ts +2 -0
  53. package/dist/lib/agent-config.js +14 -10
  54. package/dist/lib/agentic-stream.d.ts +88 -1
  55. package/dist/lib/agentic-stream.js +330 -9
  56. package/dist/lib/auth-core-init.d.ts +2 -10
  57. package/dist/lib/auth-core-init.js +20 -67
  58. package/dist/lib/auth-failure.d.ts +33 -0
  59. package/dist/lib/auth-failure.js +68 -0
  60. package/dist/lib/auth-storage.d.ts +24 -0
  61. package/dist/lib/auth-storage.js +342 -11
  62. package/dist/lib/cli-id.d.ts +22 -0
  63. package/dist/lib/cli-id.js +56 -0
  64. package/dist/lib/cli-identity.d.ts +49 -0
  65. package/dist/lib/cli-identity.js +159 -0
  66. package/dist/lib/commitments.d.ts +25 -1
  67. package/dist/lib/commitments.js +46 -1
  68. package/dist/lib/config.d.ts +17 -0
  69. package/dist/lib/config.js +15 -3
  70. package/dist/lib/daemonBroker.d.ts +37 -1
  71. package/dist/lib/daemonBroker.js +146 -22
  72. package/dist/lib/daemonBrokerRefusal.d.ts +51 -0
  73. package/dist/lib/daemonBrokerRefusal.js +127 -0
  74. package/dist/lib/dedicated-machines.d.ts +33 -2
  75. package/dist/lib/dedicated-machines.js +50 -1
  76. package/dist/lib/delegated-cli.js +20 -1
  77. package/dist/lib/exec-runtime-binary.d.ts +3 -1
  78. package/dist/lib/exec-runtime-binary.js +4 -2
  79. package/dist/lib/first-party-harness-broker.js +10 -5
  80. package/dist/lib/harnesses.d.ts +10 -0
  81. package/dist/lib/keychain.d.ts +39 -0
  82. package/dist/lib/keychain.js +228 -39
  83. package/dist/lib/label-ref.d.ts +34 -0
  84. package/dist/lib/label-ref.js +68 -1
  85. package/dist/lib/live-runtime-binary.d.ts +40 -0
  86. package/dist/lib/live-runtime-binary.js +193 -0
  87. package/dist/lib/login.d.ts +23 -0
  88. package/dist/lib/login.js +67 -21
  89. package/dist/lib/message-intent.js +3 -1
  90. package/dist/lib/oauthLogin.js +2 -8
  91. package/dist/lib/refresh.js +6 -2
  92. package/dist/lib/tasks.d.ts +20 -0
  93. package/dist/lib/tasks.js +87 -8
  94. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
  95. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
  96. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +28 -4
  97. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +23 -5
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
  108. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +8 -1
  109. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
  110. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
  111. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
  112. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
  113. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +28 -4
  114. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +22 -4
  115. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
  116. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  117. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
  118. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
  119. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
  120. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
  121. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
  122. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
  123. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
  124. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
  125. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +8 -1
  126. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
  127. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
  128. package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
  129. package/dist/node_modules/@skrr-ai/data-provider/index.js +3139 -3097
  130. package/oclif.manifest.json +28591 -28438
  131. package/package.json +1 -1
@@ -4,6 +4,7 @@ import { type AssigneeResolution, type MemberKind, type SpaceMember } from './li
4
4
  import { type CliConfig } from './lib/config';
5
5
  import { type OutboxMethod } from './lib/outbox';
6
6
  import { type ResolvedCredential } from './lib/credential-resolver';
7
+ import { type BrokerRefusalDescription } from './lib/daemonBrokerRefusal';
7
8
  import { type DefaultAgentResolution } from './lib/tasks';
8
9
  export declare function assigneePayloadNeedsResolution(payload: Record<string, unknown>): boolean;
9
10
  /**
@@ -68,6 +69,12 @@ export declare abstract class BaseCommand extends Command {
68
69
  */
69
70
  protected resolvedCredential: ResolvedCredential;
70
71
  protected bareMode: boolean;
72
+ /**
73
+ * Set when the auto-broker reached a live daemon and it REFUSED the hand-off.
74
+ * `requireAuth()` reports it instead of a bare "Not signed in", because the
75
+ * daemon's reason decides the repair (OSK-12149, OSK-12151).
76
+ */
77
+ protected brokerRefusal: BrokerRefusalDescription | null;
71
78
  init(): Promise<void>;
72
79
  /**
73
80
  * Read only the two global base flags. A narrow parser — oclif's
@@ -29,11 +29,13 @@ var keychain_1 = require("./lib/keychain");
29
29
  Object.defineProperty(exports, "resolveProfileFromArgv", { enumerable: true, get: function () { return keychain_1.resolveProfileFromArgv; } });
30
30
  const data_provider_1 = require("@skrr-ai/data-provider");
31
31
  const auth_core_1 = require("@skrr-ai/auth-core");
32
+ const auth_failure_1 = require("./lib/auth-failure");
32
33
  const assignee_resolver_1 = require("./lib/assignee-resolver");
33
34
  const task_ref_resolver_1 = require("./lib/task-ref-resolver");
34
35
  const space_resolver_1 = require("./lib/space-resolver");
35
36
  const projects_1 = require("./lib/projects");
36
37
  const config_1 = require("./lib/config");
38
+ const cli_id_1 = require("./lib/cli-id");
37
39
  const keychain_2 = require("./lib/keychain");
38
40
  const outbox_1 = require("./lib/outbox");
39
41
  const node_adapter_1 = require("./lib/node-adapter");
@@ -41,6 +43,7 @@ const migrate_1 = require("./lib/migrate");
41
43
  const auth_core_init_1 = require("./lib/auth-core-init");
42
44
  const credential_resolver_1 = require("./lib/credential-resolver");
43
45
  const daemonBroker_1 = require("./lib/daemonBroker");
46
+ const daemonBrokerRefusal_1 = require("./lib/daemonBrokerRefusal");
44
47
  const refresh_1 = require("./lib/refresh");
45
48
  const auth_storage_1 = require("./lib/auth-storage");
46
49
  const update_check_1 = require("./lib/update-check");
@@ -130,6 +133,12 @@ class BaseCommand extends core_1.Command {
130
133
  */
131
134
  resolvedCredential;
132
135
  bareMode = false;
136
+ /**
137
+ * Set when the auto-broker reached a live daemon and it REFUSED the hand-off.
138
+ * `requireAuth()` reports it instead of a bare "Not signed in", because the
139
+ * daemon's reason decides the repair (OSK-12149, OSK-12151).
140
+ */
141
+ brokerRefusal = null;
133
142
  async init() {
134
143
  await super.init();
135
144
  // The profile is resolved from raw argv, BEFORE oclif parses anything.
@@ -268,11 +277,13 @@ class BaseCommand extends core_1.Command {
268
277
  // skrr command reads it through the normal resolver chain — no
269
278
  // per-command broker round-trip.
270
279
  //
271
- // Fail-quiet: any broker failure leaves resolvedCredential null and
272
- // the command's own `requireAuth()` will surface the "Not signed
273
- // in" error a moment later. Mismatched baseURL (a real foot-gun the
274
- // user might not notice) is the one exception — we print a one-
275
- // line hint so they know how to fix it.
280
+ // A broker failure leaves resolvedCredential null and the command's own
281
+ // `requireAuth()` surfaces the error a moment later. It is quiet only when
282
+ // no daemon was there to ask. A daemon that ANSWERED and refused is kept
283
+ // in `brokerRefusal`, and `requireAuth()` reports its reason and the
284
+ // repair that reason implies — "Not signed in. Run `skrr login`" was true
285
+ // and pointed at the wrong fix (OSK-12149). Rate limiting and a mismatched
286
+ // baseURL keep their own one-line hints below.
276
287
  if (!this.resolvedCredential.token) {
277
288
  const autoResult = await (0, daemonBroker_1.maybeAutoBroker)({
278
289
  cliConfig: this.cliConfig,
@@ -317,6 +328,9 @@ class BaseCommand extends core_1.Command {
317
328
  kind: (0, auth_core_1.classifyTokenKind)(accessToken),
318
329
  };
319
330
  }
331
+ else if ((this.brokerRefusal = (0, daemonBrokerRefusal_1.describeBrokerRefusal)(autoResult.outcome, this.config.bin))) {
332
+ // Reported by requireAuth(); nothing to print here.
333
+ }
320
334
  else if (autoResult.outcome.reason === 'rate_limited') {
321
335
  // Otherwise the only thing the user sees is "Not signed in", which is
322
336
  // true and points at the wrong repair: they are signed in, the local
@@ -339,7 +353,12 @@ class BaseCommand extends core_1.Command {
339
353
  }
340
354
  }
341
355
  if (!this.resolvedCredential.token && ignoredStoredCredential) {
342
- if (ignoredStoredCredential.originStatus === 'mismatch') {
356
+ const foreign = (0, auth_storage_1.describeForeignCredential)(ignoredStoredCredential);
357
+ if (foreign) {
358
+ process.stderr.write(`[skrr] ${foreign} Run \`skrr login\` to sign this configuration in; the other ` +
359
+ `configuration keeps its own credential.\n`);
360
+ }
361
+ else if (ignoredStoredCredential.originStatus === 'mismatch') {
343
362
  process.stderr.write(`[skrr] Ignoring stored credentials issued by ${ignoredStoredCredential.storedServerOrigin}; ` +
344
363
  `skrr is configured for ${this.cliConfig.baseURL}. Run \`skrr login --base-url ${this.cliConfig.baseURL}\` ` +
345
364
  `or switch back to the issuing server.\n`);
@@ -615,6 +634,18 @@ class BaseCommand extends core_1.Command {
615
634
  /** Abort the command with a helpful message if the user isn't signed in. */
616
635
  requireAuth() {
617
636
  if (!this.resolvedCredential?.token) {
637
+ const refusal = this.brokerRefusal;
638
+ if (refusal) {
639
+ // Same code as the plain case — scripts branch on NOT_SIGNED_IN — with
640
+ // the daemon's answer in the message and in `details.broker`.
641
+ this.failWithCliError({
642
+ message: `Not signed in. ${refusal.headline}\n→ ${refusal.remedy}`,
643
+ code: 'NOT_SIGNED_IN',
644
+ exit: 2,
645
+ retryable: refusal.transient,
646
+ details: { broker: refusal.broker, suggestion: refusal.remedy },
647
+ });
648
+ }
618
649
  this.failWithCliError({
619
650
  message: `Not signed in. Run \`${this.config.bin} login\` first.`,
620
651
  code: 'NOT_SIGNED_IN',
@@ -1002,16 +1033,29 @@ class BaseCommand extends core_1.Command {
1002
1033
  // `login` sends a CI job to do the one thing it cannot, and hides the
1003
1034
  // real mistake (dogfood OSK-4693). Name the credential instead.
1004
1035
  const presentedCiToken = Boolean(this.resolvedCredential?.token?.startsWith('osk_ci_'));
1036
+ // OSK-12116 — the 401 is usually the SECOND failure: the token expired
1037
+ // (routine) and renewing it was refused (the fault). The server's code
1038
+ // for the retry is `TOKEN_EXPIRED`, which names the routine half; the
1039
+ // latched refresh refusal names the half the reader can act on.
1040
+ const refreshRefusal = presentedCiToken
1041
+ ? null
1042
+ : (0, auth_failure_1.describeRefreshRefusal)(readRefreshRefusal(), {
1043
+ bin: this.config.bin,
1044
+ cliId: this.cliConfig ? (0, cli_id_1.resolveCliId)(this.cliConfig) : undefined,
1045
+ });
1005
1046
  this.failWithCliError({
1006
1047
  message: say(presentedCiToken
1007
1048
  ? `Authentication failed: this is an \`osk_ci_*\` token, which only bootstraps a ` +
1008
1049
  `daemon (\`OVERSKY_TOKEN=<token> skrrd start\`). ${this.config.bin} commands do ` +
1009
1050
  `not accept it — sign in with \`${this.config.bin} login\` instead.`
1010
- : `Authentication failed. Run \`${this.config.bin} login\` to sign in again.`),
1011
- code: bodyCode || 'AUTHENTICATION_FAILED',
1051
+ : refreshRefusal
1052
+ ? `Authentication failed. ${refreshRefusal.message}`
1053
+ : `Authentication failed. Run \`${this.config.bin} login\` to sign in again.`),
1054
+ code: refreshRefusal?.code || bodyCode || 'AUTHENTICATION_FAILED',
1012
1055
  status: e.status,
1013
1056
  exit: exitCode,
1014
1057
  retryable: true,
1058
+ ...(refreshRefusal && bodyCode ? { details: { accessTokenCode: bodyCode } } : {}),
1015
1059
  });
1016
1060
  }
1017
1061
  else if ((e.status === 403 || e.status === 504) &&
@@ -1287,6 +1331,36 @@ class BaseCommand extends core_1.Command {
1287
1331
  // attempt is assumed absent.
1288
1332
  const outage = (0, outbox_1.isOutageFailure)(err);
1289
1333
  const statusSuffix = e.status ? ` (HTTP ${e.status})` : '';
1334
+ // OSK-12121 — a 401 is queued (the payload is authored, and it is worth
1335
+ // keeping until someone signs in again), but it is an AUTHENTICATION
1336
+ // failure, and saying "did not accept this write" left the reader to guess
1337
+ // that. Lead with the sign-in, name the refused refresh when one is latched,
1338
+ // and give the order the fix has to happen in: sign in, THEN replay.
1339
+ if (e.status === 401) {
1340
+ const refusal = (0, auth_failure_1.describeRefreshRefusal)(readRefreshRefusal(), {
1341
+ bin: this.config.bin,
1342
+ cliId: this.cliConfig ? (0, cli_id_1.resolveCliId)(this.cliConfig) : undefined,
1343
+ });
1344
+ const why = refusal
1345
+ ? refusal.message.replace(/\s*Run `[^`]+` to sign in again\.$/, '')
1346
+ : 'The server did not accept this sign-in.';
1347
+ this.failWithCliError({
1348
+ message: `Authentication failed${statusSuffix}. ${why} Your ${write.summary} was not applied; it ` +
1349
+ `was saved locally as ${queuedId} so it is not lost. Sign in with ` +
1350
+ `\`${this.config.bin} login\`, then replay it with \`${this.config.bin} outbox retry\` ` +
1351
+ `(or discard it with \`${this.config.bin} outbox drop ${queuedId}\`).`,
1352
+ code: refusal?.code || 'AUTHENTICATION_FAILED',
1353
+ status: e.status,
1354
+ exit: this.bareMode ? 75 : 2,
1355
+ retryable: true,
1356
+ details: {
1357
+ queued: true,
1358
+ outboxId: queuedId,
1359
+ idempotencyKey: write.idempotencyKey,
1360
+ ...(e.code ? { accessTokenCode: e.code } : {}),
1361
+ },
1362
+ });
1363
+ }
1290
1364
  const lead = outage
1291
1365
  ? `${this.cliConfig.baseURL} is not serving requests right now${statusSuffix}. ` +
1292
1366
  `Your ${write.summary} was queued locally as ${queuedId}; whether the server already ` +
@@ -1419,6 +1493,19 @@ function parseGlobalFlags(argv) {
1419
1493
  }
1420
1494
  return { bare, token, workspace };
1421
1495
  }
1496
+ /**
1497
+ * The latched permanent refresh refusal, if any. Never throws: a surface that
1498
+ * cannot read the latch falls back to its generic wording (OSK-12116).
1499
+ */
1500
+ function readRefreshRefusal() {
1501
+ try {
1502
+ const state = (0, auth_core_1.needsReauthState)();
1503
+ return state.needed ? { reason: state.reason, message: state.message } : null;
1504
+ }
1505
+ catch {
1506
+ return null;
1507
+ }
1508
+ }
1422
1509
  function parseErrorBody(body) {
1423
1510
  if (!body)
1424
1511
  return null;
@@ -1,3 +1,4 @@
1
+ import { dataService } from '@skrr-ai/data-provider';
1
2
  import { BaseCommand } from '../../base-command';
2
3
  import { type AgenticStreamResult, type ReconciledTerminal } from '../../lib/agentic-stream';
3
4
  /**
@@ -141,6 +142,17 @@ export declare function daemonPinFields(localSkills: {
141
142
  pathHash: string;
142
143
  }>;
143
144
  } | Record<string, never>;
145
+ type ChatPermissionMode = NonNullable<Parameters<typeof dataService.startAgenticChat>[0]['permissionMode']>;
146
+ /**
147
+ * The per-turn permission-mode fragment of the chat-start payload (OSK-12124).
148
+ * It rides the same request layer the web composer's picker sends, which the
149
+ * server already honours above the agent's and the harness's stored defaults —
150
+ * so switching one conversation no longer means changing the agent for every
151
+ * conversation it has.
152
+ */
153
+ export declare function permissionModeFields(mode: string | undefined): {
154
+ permissionMode: ChatPermissionMode;
155
+ } | Record<string, never>;
144
156
  export default class AgentsChat extends BaseCommand {
145
157
  static description: string;
146
158
  static examples: string[];
@@ -157,6 +169,7 @@ export default class AgentsChat extends BaseCommand {
157
169
  jsonl: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
158
170
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
159
171
  'approve-tools': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
172
+ 'permission-mode': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
160
173
  'local-skill': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
161
174
  daemon: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
162
175
  task: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -182,3 +195,4 @@ export default class AgentsChat extends BaseCommand {
182
195
  private resolveLocalSkills;
183
196
  run(): Promise<void>;
184
197
  }
198
+ export {};
@@ -9,6 +9,7 @@ exports.noOutputFailureMessage = noOutputFailureMessage;
9
9
  exports.noStreamJsonPayload = noStreamJsonPayload;
10
10
  exports.chatStreamHeaderLines = chatStreamHeaderLines;
11
11
  exports.daemonPinFields = daemonPinFields;
12
+ exports.permissionModeFields = permissionModeFields;
12
13
  const node_fs_1 = require("node:fs");
13
14
  const core_1 = require("@oclif/core");
14
15
  const data_provider_1 = require("@skrr-ai/data-provider");
@@ -157,6 +158,11 @@ function noStreamJsonPayload(input) {
157
158
  responseMessageId: result.responseMessageId,
158
159
  status: result.status,
159
160
  response: result.text,
161
+ // The model a local harness reported running (OSK-12111). Absent on a
162
+ // cloud turn and on a harness that reports none.
163
+ ...(typeof result.lastEvent?.harnessModel === 'string' && result.lastEvent.harnessModel
164
+ ? { harnessModel: result.lastEvent.harnessModel }
165
+ : {}),
160
166
  ...(result.status === 'failed'
161
167
  ? {
162
168
  failureReason: result.lastEvent?.reason ??
@@ -211,11 +217,22 @@ function daemonPinFields(localSkills, daemonFlag) {
211
217
  }
212
218
  return {};
213
219
  }
220
+ /**
221
+ * The per-turn permission-mode fragment of the chat-start payload (OSK-12124).
222
+ * It rides the same request layer the web composer's picker sends, which the
223
+ * server already honours above the agent's and the harness's stored defaults —
224
+ * so switching one conversation no longer means changing the agent for every
225
+ * conversation it has.
226
+ */
227
+ function permissionModeFields(mode) {
228
+ return mode ? { permissionMode: mode } : {};
229
+ }
214
230
  class AgentsChat extends base_command_1.BaseCommand {
215
231
  static description = 'Run an agent turn with live streaming, tool events, cancellation, and reconnects';
216
232
  static examples = [
217
233
  '<%= config.bin %> agents chat <agent-id> --prompt "Review the open tasks"',
218
234
  '<%= config.bin %> agents chat <agent-id> --prompt "Deploy the fix" --approve-tools',
235
+ '<%= config.bin %> agents chat <agent-id> --conversation <id> --prompt "Now apply it" --permission-mode acceptEdits',
219
236
  '<%= config.bin %> agents chat <agent-id> --prompt "Summarize" --jsonl',
220
237
  '<%= config.bin %> agents chat <agent-id> --prompt "Summarize" --no-stream --json',
221
238
  '<%= config.bin %> agents chat <agent-id> --prompt "Review this diff" --local-skill code-review',
@@ -250,7 +267,12 @@ class AgentsChat extends base_command_1.BaseCommand {
250
267
  '`stream.gap` event means envelopes in that seq range never arrived and the streamed ' +
251
268
  'text is short; `run.result` and the persisted message remain authoritative. Tool ' +
252
269
  'calls arrive as `tool.started` / `tool.completed`, once per `toolUseId`, on cloud ' +
253
- 'and local-harness runs alike.',
270
+ 'and local-harness runs alike. A `session.notice` with `notice: ' +
271
+ '"harness-session-reset"` means the local harness could not resume its previous ' +
272
+ 'session and started a new one; `message` says why. A `permission.resolved` says ' +
273
+ 'how a local harness permission request was decided: `decidedBy` in words, and ' +
274
+ '`basis` — `person` (with `surface`), `mode` (with `mode`), `policy` or ' +
275
+ '`session-grant` — beside the wire `by`.',
254
276
  }),
255
277
  json: core_1.Flags.boolean({
256
278
  description: 'Emit the final one-shot response as JSON (use with --no-stream)',
@@ -260,6 +282,15 @@ class AgentsChat extends base_command_1.BaseCommand {
260
282
  'with no terminal the flag itself is the approval for this turn, and each one is ' +
261
283
  'reported on the stream. Without it every such call is denied.',
262
284
  }),
285
+ 'permission-mode': core_1.Flags.string({
286
+ description: 'Permission mode for THIS turn only, on a local-harness agent. It beats the agent’s and ' +
287
+ 'the harness’s stored defaults for this turn and changes nothing else — pass it again ' +
288
+ 'on each turn that needs it (`agents update --default-permission-mode` changes every ' +
289
+ 'conversation of the agent). A mode the harness cannot honour runs as the one it can, ' +
290
+ 'and run.started says which. --approve-tools is separate: it answers the approval ' +
291
+ 'prompts a mode asks for.',
292
+ options: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
293
+ }),
263
294
  'local-skill': core_1.Flags.string({
264
295
  description: 'Invoke a skill on your machine on this turn, as `skrr skills local` lists it: its ' +
265
296
  'name, or <root>/<name> when two roots share one. The turn runs on that machine. ' +
@@ -354,6 +385,10 @@ class AgentsChat extends base_command_1.BaseCommand {
354
385
  'response and streams no events. Use --quick --json for a machine-readable ' +
355
386
  'one-shot, or drop --quick for a real streamed turn.', { exit: 2 });
356
387
  }
388
+ if (flags.quick && flags['permission-mode']) {
389
+ this.error('--permission-mode has no meaning with --quick: the persona-only summarizer runs no ' +
390
+ 'tools, so there is nothing for a mode to govern. Drop --quick for a real turn.', { exit: 2 });
391
+ }
357
392
  if (flags.quick && flags['local-skill']?.length) {
358
393
  this.error('--local-skill has no meaning with --quick: the persona-only summarizer runs no ' +
359
394
  'runtime, so no skill on your machine can load. Drop --quick for a real turn.', { exit: 2 });
@@ -485,6 +520,7 @@ class AgentsChat extends base_command_1.BaseCommand {
485
520
  // execution policy places it elsewhere keeps that placement, and the
486
521
  // run fails saying why rather than running without the skill.
487
522
  ...daemonPinFields(localSkills, flags.daemon),
523
+ ...permissionModeFields(flags['permission-mode']),
488
524
  });
489
525
  }
490
526
  catch (err) {
@@ -246,22 +246,26 @@ class AgentsCreate extends base_command_1.BaseCommand {
246
246
  '09:00-18:00@America/Los_Angeles',
247
247
  }),
248
248
  // Retired (OSK-11975): a Commitment turn works under the Agent's own
249
- // permissions, so these three are stored but no longer read at runtime.
249
+ // permissions. Hidden, warned on use, and never sent (OSK-12068) — see
250
+ // `mergeAgentConfigFlags`. Kept parseable so an old script still runs.
250
251
  'reasoning-tool-escape': core_1.Flags.string({
251
- description: "Deprecated — no longer has any effect. Commitment runs use the agent's own permissions.",
252
+ description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
252
253
  options: [...agent_config_1.REASONING_TOOL_ESCAPES],
253
254
  multiple: true,
254
255
  deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
256
+ hidden: true,
255
257
  }),
256
258
  'commitment-responsibility': core_1.Flags.string({
257
- description: "Deprecated — no longer has any effect. Commitment runs use the agent's own permissions.",
259
+ description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
258
260
  options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
259
261
  deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
262
+ hidden: true,
260
263
  }),
261
264
  'commitment-responsibility-ceiling': core_1.Flags.string({
262
- description: "Deprecated — no longer has any effect. Commitment runs use the agent's own permissions.",
265
+ description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
263
266
  options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
264
267
  deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
268
+ hidden: true,
265
269
  }),
266
270
  'max-turns': core_1.Flags.integer({ description: 'Maximum turns in one agent run (1-500)' }),
267
271
  'tool-resources': core_1.Flags.string({
@@ -13,6 +13,15 @@ export declare function agentToneLine(agent: {
13
13
  toneMode?: unknown;
14
14
  toneId?: unknown;
15
15
  }): string;
16
+ /**
17
+ * The permission mode this agent runs with on its harness, and the layer that
18
+ * decided it (OSK-12069). The server resolves it (`effectivePermissionMode`) the
19
+ * way the dispatch does; the stored agent field alone is one of four layers, so
20
+ * printing it read "not set" for an agent whose harness default governed.
21
+ * Falls back to the stored field against a server that does not send the
22
+ * resolution, and says that is all it knows.
23
+ */
24
+ export declare function agentPermissionLines(agent: Record<string, unknown>, harness: string): string[];
16
25
  /** Text rendering is deliberately based on the server projection, with a
17
26
  * storage-shape fallback while all stored policies are migrated. */
18
27
  export declare function agentExecutionOrderLines(agent: Record<string, unknown>): string[];
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.agentToneLine = agentToneLine;
4
+ exports.agentPermissionLines = agentPermissionLines;
4
5
  exports.agentExecutionOrderLines = agentExecutionOrderLines;
5
6
  const core_1 = require("@oclif/core");
6
7
  const data_provider_1 = require("@skrr-ai/data-provider");
@@ -31,6 +32,46 @@ function agentToneLine(agent) {
31
32
  return 'inherit — see `skrr agents tone show`';
32
33
  return '-';
33
34
  }
35
+ const PERMISSION_SOURCE_LABELS = {
36
+ agent: "this agent's default",
37
+ harness: "the harness's default",
38
+ };
39
+ /**
40
+ * The permission mode this agent runs with on its harness, and the layer that
41
+ * decided it (OSK-12069). The server resolves it (`effectivePermissionMode`) the
42
+ * way the dispatch does; the stored agent field alone is one of four layers, so
43
+ * printing it read "not set" for an agent whose harness default governed.
44
+ * Falls back to the stored field against a server that does not send the
45
+ * resolution, and says that is all it knows.
46
+ */
47
+ function agentPermissionLines(agent, harness) {
48
+ const effective = agent.effectivePermissionMode;
49
+ const pad = ' ';
50
+ if (!effective || typeof effective.source !== 'string') {
51
+ const stored = typeof agent.defaultPermissionMode === 'string' && agent.defaultPermissionMode
52
+ ? agent.defaultPermissionMode
53
+ : "not set on the agent (the harness's or the daemon's default applies)";
54
+ return [`Permission: ${stored} — enforced by the ${harness} harness`];
55
+ }
56
+ const lines = [];
57
+ if (typeof effective.mode === 'string' && effective.mode) {
58
+ const from = PERMISSION_SOURCE_LABELS[effective.source] ?? effective.source;
59
+ lines.push(`Permission: ${effective.mode} (from ${from}) — enforced by the ${harness} harness`);
60
+ }
61
+ else {
62
+ lines.push(`Permission: set by the daemon's own config on that machine — no agent or harness mode is set`);
63
+ }
64
+ if (typeof effective.coercedFrom === 'string' && effective.coercedFrom) {
65
+ lines.push(`${pad}${effective.coercedFrom} is configured, but ${harness} cannot honour it, so it runs as ${String(effective.mode)}.`);
66
+ }
67
+ const supported = Array.isArray(effective.supportedModes)
68
+ ? effective.supportedModes.filter((m) => typeof m === 'string')
69
+ : [];
70
+ if (supported.length > 0)
71
+ lines.push(`${pad}${harness} accepts: ${supported.join(', ')}`);
72
+ lines.push(`${pad}A turn's own mode (\`agents chat --permission-mode\`) overrides this for that turn.`);
73
+ return lines;
74
+ }
34
75
  /** Text rendering is deliberately based on the server projection, with a
35
76
  * storage-shape fallback while all stored policies are migrated. */
36
77
  function agentExecutionOrderLines(agent) {
@@ -163,6 +204,18 @@ class AgentsShow extends base_command_1.BaseCommand {
163
204
  this.log(line);
164
205
  if (a.harnessName || a.harnessProvider || a.harnessStatus) {
165
206
  this.log(`Harness: ${a.harnessName ?? (typeof a.harnessProvider === 'string' ? (0, harness_provider_input_1.canonicalHarnessInput)(a.harnessProvider) : a.harnessProvider) ?? '-'}${a.harnessStatus ? ` (${a.harnessStatus})` : ''}`);
207
+ // On a harness, the HARNESS decides what runs and which permission modes
208
+ // exist — `Provider` is only the cloud model contract, so an agent read
209
+ // as "anthropic" while running Codex (OSK-12071). And the mode itself was
210
+ // visible only in --json (OSK-12069).
211
+ if (typeof a.harnessProvider === 'string' && a.harnessProvider) {
212
+ const harness = (0, harness_provider_input_1.canonicalHarnessInput)(a.harnessProvider);
213
+ for (const line of agentPermissionLines(a, harness))
214
+ this.log(line);
215
+ if (typeof a.provider === 'string' && a.provider && a.provider !== harness) {
216
+ this.log(` Runs on ${harness}; Provider "${a.provider}" is only the cloud model contract.`);
217
+ }
218
+ }
166
219
  // An offline engine is the difference between "this agent works" and
167
220
  // "this agent silently does nothing", and the display name alone cannot
168
221
  // carry it: two runtimes for the same machine share an identical name,
@@ -55,3 +55,35 @@ export default class AgentsUpdate extends BaseCommand {
55
55
  };
56
56
  run(): Promise<void>;
57
57
  }
58
+ /**
59
+ * The per-Agent mutation fence's "another request holds this right now"
60
+ * refusals. `updateAgent` takes a short lease on the Agent before it writes, so
61
+ * a concurrent writer — or, until OSK-12138, a lagging replica read of a lease
62
+ * row the same request had just created — refuses the update with one of these.
63
+ * Nothing was written, so repeating the identical PATCH is safe.
64
+ */
65
+ export declare const AGENT_FENCE_RETRY_CODES: ReadonlySet<string>;
66
+ /**
67
+ * Run an Agent write, and repeat it ONCE if the fence refused it as transient.
68
+ *
69
+ * Retried only when the server said both things: a fence code AND
70
+ * `retryable: true`. `MARKETPLACE_SOURCE_LIFECYCLE_BUSY` also covers a source
71
+ * that is being deleted or published, where the server answers
72
+ * `retryable: false`, and those must surface rather than be repeated. One retry,
73
+ * after a short pause, because the dogfooded case (OSK-12138) cleared on the
74
+ * first identical retry; anything that survives it is worth showing.
75
+ *
76
+ * Exported for tests.
77
+ */
78
+ export declare function retryOnceOnAgentFence<T>(operation: () => Promise<T>, { delayMs }?: {
79
+ delayMs?: number;
80
+ }): Promise<T>;
81
+ /**
82
+ * What changing the agent's default permission mode actually changed
83
+ * (OSK-12124). It is the AGENT's default, so it governs every conversation,
84
+ * task run and Commitment Run of the agent from its next turn — which the bare
85
+ * "Updated agent" line never said, and the only way to learn that a live
86
+ * conversation picked it up was to spend another turn. Names the per-turn
87
+ * alternative for the case that wanted one conversation changed.
88
+ */
89
+ export declare function defaultPermissionModeNote(requested: string | undefined, updated: Record<string, unknown>): string | null;
@@ -1,5 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AGENT_FENCE_RETRY_CODES = void 0;
4
+ exports.retryOnceOnAgentFence = retryOnceOnAgentFence;
5
+ exports.defaultPermissionModeNote = defaultPermissionModeNote;
3
6
  const core_1 = require("@oclif/core");
4
7
  const data_provider_1 = require("@skrr-ai/data-provider");
5
8
  const agent_visual_refs_1 = require("../../lib/agent-visual-refs");
@@ -50,6 +53,8 @@ class AgentsUpdate extends base_command_1.BaseCommand {
50
53
  }),
51
54
  'default-permission-mode': core_1.Flags.string({
52
55
  description: "Permission mode for this agent's local-harness sessions (every harness, not only Claude Code). " +
56
+ 'It is the default for EVERY conversation, task run and Commitment Run of the agent; for one ' +
57
+ 'turn only use `agents chat --permission-mode`. ' +
53
58
  'One-shot harnesses (gemini, opencode, cursor, copilot, goose, pi, openclaw) can only run as ' +
54
59
  'bypassPermissions; any other mode is refused for them.',
55
60
  options: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
@@ -96,22 +101,26 @@ class AgentsUpdate extends base_command_1.BaseCommand {
96
101
  '09:00-18:00@America/Los_Angeles',
97
102
  }),
98
103
  // Retired (OSK-11975): a Commitment turn works under the Agent's own
99
- // permissions, so these three are stored but no longer read at runtime.
104
+ // permissions. Hidden, warned on use, and never sent (OSK-12068) — see
105
+ // `mergeAgentConfigFlags`. Kept parseable so an old script still runs.
100
106
  'reasoning-tool-escape': core_1.Flags.string({
101
- description: "Deprecated — no longer has any effect. Commitment runs use the agent's own permissions.",
107
+ description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
102
108
  options: [...agent_config_1.REASONING_TOOL_ESCAPES],
103
109
  multiple: true,
104
110
  deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
111
+ hidden: true,
105
112
  }),
106
113
  'commitment-responsibility': core_1.Flags.string({
107
- description: "Deprecated — no longer has any effect. Commitment runs use the agent's own permissions.",
114
+ description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
108
115
  options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
109
116
  deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
117
+ hidden: true,
110
118
  }),
111
119
  'commitment-responsibility-ceiling': core_1.Flags.string({
112
- description: "Deprecated — no longer has any effect. Commitment runs use the agent's own permissions.",
120
+ description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
113
121
  options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
114
122
  deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
123
+ hidden: true,
115
124
  }),
116
125
  'max-turns': core_1.Flags.integer({ description: 'Maximum turns in one agent run (1-500)' }),
117
126
  'tool-resources': core_1.Flags.string({
@@ -181,16 +190,18 @@ class AgentsUpdate extends base_command_1.BaseCommand {
181
190
  this.error(err.message, { exit: 1 });
182
191
  }
183
192
  if (Object.keys(payload).length === 0) {
184
- this.error('Nothing to update. Pass at least one flag (e.g. --name) or --from-json.', {
185
- exit: 1,
186
- });
193
+ const retired = agent_config_1.RETIRED_COMMITMENT_PERMISSION_FLAGS.filter((flag) => flags[flag] !== undefined);
194
+ this.error(retired.length > 0
195
+ ? `Nothing to update: ${retired.map((flag) => `--${flag}`).join(', ')} ` +
196
+ `${retired.length === 1 ? 'is' : 'are'} retired and not sent. ${agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED}`
197
+ : 'Nothing to update. Pass at least one flag (e.g. --name) or --from-json.', { exit: 1 });
187
198
  }
188
199
  let updated;
189
200
  try {
190
- updated = await data_provider_1.dataService.updateAgent({
201
+ updated = await retryOnceOnAgentFence(() => data_provider_1.dataService.updateAgent({
191
202
  agent_id: args.id,
192
203
  data: payload,
193
- });
204
+ }));
194
205
  }
195
206
  catch (err) {
196
207
  this.handleApiError(err);
@@ -213,6 +224,67 @@ class AgentsUpdate extends base_command_1.BaseCommand {
213
224
  const a = updated;
214
225
  this.log(`Updated agent ${a.id ?? a._id ?? args.id} — ${a.name ?? '(no name)'}`);
215
226
  this.log(`URL: ${(0, web_url_1.withAgentUrl)(this.cliConfig.baseURL, { ...a, id: a.id ?? a._id ?? args.id }).url ?? '-'}`);
227
+ const modeNote = defaultPermissionModeNote(flags['default-permission-mode'], a);
228
+ if (modeNote)
229
+ this.log(modeNote);
216
230
  }
217
231
  }
218
232
  exports.default = AgentsUpdate;
233
+ /**
234
+ * The per-Agent mutation fence's "another request holds this right now"
235
+ * refusals. `updateAgent` takes a short lease on the Agent before it writes, so
236
+ * a concurrent writer — or, until OSK-12138, a lagging replica read of a lease
237
+ * row the same request had just created — refuses the update with one of these.
238
+ * Nothing was written, so repeating the identical PATCH is safe.
239
+ */
240
+ exports.AGENT_FENCE_RETRY_CODES = new Set([
241
+ 'MARKETPLACE_SOURCE_LIFECYCLE_RACE',
242
+ 'MARKETPLACE_SOURCE_LIFECYCLE_BUSY',
243
+ 'MARKETPLACE_SOURCE_LIFECYCLE_CAS_LOST',
244
+ ]);
245
+ /**
246
+ * Run an Agent write, and repeat it ONCE if the fence refused it as transient.
247
+ *
248
+ * Retried only when the server said both things: a fence code AND
249
+ * `retryable: true`. `MARKETPLACE_SOURCE_LIFECYCLE_BUSY` also covers a source
250
+ * that is being deleted or published, where the server answers
251
+ * `retryable: false`, and those must surface rather than be repeated. One retry,
252
+ * after a short pause, because the dogfooded case (OSK-12138) cleared on the
253
+ * first identical retry; anything that survives it is worth showing.
254
+ *
255
+ * Exported for tests.
256
+ */
257
+ async function retryOnceOnAgentFence(operation, { delayMs = 750 } = {}) {
258
+ try {
259
+ return await operation();
260
+ }
261
+ catch (err) {
262
+ const { code, body } = (0, base_command_1.apiRefusal)(err);
263
+ if (!code || !exports.AGENT_FENCE_RETRY_CODES.has(code) || body?.retryable !== true)
264
+ throw err;
265
+ if (delayMs > 0)
266
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
267
+ return operation();
268
+ }
269
+ }
270
+ /**
271
+ * What changing the agent's default permission mode actually changed
272
+ * (OSK-12124). It is the AGENT's default, so it governs every conversation,
273
+ * task run and Commitment Run of the agent from its next turn — which the bare
274
+ * "Updated agent" line never said, and the only way to learn that a live
275
+ * conversation picked it up was to spend another turn. Names the per-turn
276
+ * alternative for the case that wanted one conversation changed.
277
+ */
278
+ function defaultPermissionModeNote(requested, updated) {
279
+ if (!requested)
280
+ return null;
281
+ const stored = typeof updated.defaultPermissionMode === 'string' && updated.defaultPermissionMode
282
+ ? updated.defaultPermissionMode
283
+ : requested;
284
+ return [
285
+ `Permission: ${stored} is now this agent's default for EVERY conversation, task run and`,
286
+ ' Commitment Run, starting with each one’s next turn. A turn that names its own',
287
+ ' mode still wins. To change one turn only, leave the default alone and pass',
288
+ ` \`skrr agents chat <agent> --conversation <id> --permission-mode ${requested}\`.`,
289
+ ].join('\n');
290
+ }