@skrr-ai/cli 0.1.49 → 0.1.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +5 -4
  2. package/dist/base-command.js +75 -6
  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 +14 -7
  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 +9 -0
  9. package/dist/commands/agents/update.js +38 -5
  10. package/dist/commands/balance/show.d.ts +34 -0
  11. package/dist/commands/balance/show.js +92 -2
  12. package/dist/commands/code/index.d.ts +1 -0
  13. package/dist/commands/code/index.js +49 -5
  14. package/dist/commands/code/run.js +5 -1
  15. package/dist/commands/commitments/cycles.d.ts +19 -0
  16. package/dist/commands/commitments/cycles.js +46 -0
  17. package/dist/commands/commitments/doctor.d.ts +1 -0
  18. package/dist/commands/commitments/doctor.js +19 -4
  19. package/dist/commands/commitments/effective-policy.js +40 -1
  20. package/dist/commands/commitments/explain.d.ts +17 -0
  21. package/dist/commands/commitments/explain.js +40 -0
  22. package/dist/commands/commitments/next-tick.js +3 -1
  23. package/dist/commands/commitments/preflight.js +10 -0
  24. package/dist/commands/commitments/show.js +11 -1
  25. package/dist/commands/daemon/config.d.ts +15 -0
  26. package/dist/commands/daemon/config.js +38 -0
  27. package/dist/commands/daemon/index.js +1 -0
  28. package/dist/commands/daemon/status.js +15 -1
  29. package/dist/commands/goals/create.d.ts +1 -0
  30. package/dist/commands/goals/create.js +22 -0
  31. package/dist/commands/harnesses/models.d.ts +54 -0
  32. package/dist/commands/harnesses/models.js +100 -7
  33. package/dist/commands/harnesses/show.d.ts +11 -0
  34. package/dist/commands/harnesses/show.js +35 -0
  35. package/dist/commands/harnesses/update.d.ts +28 -1
  36. package/dist/commands/harnesses/update.js +73 -10
  37. package/dist/commands/logout.js +12 -1
  38. package/dist/commands/machines/dedicated/index.js +1 -1
  39. package/dist/commands/machines/dedicated/sign-in.js +4 -1
  40. package/dist/commands/machines/dedicated/terminal/kill.d.ts +21 -0
  41. package/dist/commands/machines/dedicated/terminal/kill.js +55 -0
  42. package/dist/commands/machines/dedicated/terminal/ls.d.ts +22 -0
  43. package/dist/commands/machines/dedicated/terminal/ls.js +70 -0
  44. package/dist/commands/machines/dedicated/terminal/rename.d.ts +23 -0
  45. package/dist/commands/machines/dedicated/terminal/rename.js +64 -0
  46. package/dist/commands/machines/dedicated/terminal.d.ts +9 -0
  47. package/dist/commands/machines/dedicated/terminal.js +27 -2
  48. package/dist/commands/machines/hosted/connect.js +2 -2
  49. package/dist/commands/machines/hosted/destroy.js +1 -1
  50. package/dist/commands/machines/hosted/exec.js +1 -1
  51. package/dist/commands/machines/hosted/list.js +1 -1
  52. package/dist/commands/machines/hosted/pause.js +1 -1
  53. package/dist/commands/machines/hosted/pull.js +1 -1
  54. package/dist/commands/machines/hosted/resume.js +1 -1
  55. package/dist/commands/machines/hosted/start.js +2 -2
  56. package/dist/commands/machines/hosted/status.js +2 -1
  57. package/dist/commands/outbox/list.d.ts +12 -1
  58. package/dist/commands/outbox/list.js +31 -2
  59. package/dist/commands/revoke-daemon.js +9 -0
  60. package/dist/commands/spaces/create.d.ts +1 -0
  61. package/dist/commands/spaces/create.js +17 -0
  62. package/dist/commands/spaces/show.js +5 -0
  63. package/dist/commands/spaces/update.d.ts +1 -0
  64. package/dist/commands/spaces/update.js +16 -0
  65. package/dist/commands/tasks/create.d.ts +1 -0
  66. package/dist/commands/tasks/create.js +6 -0
  67. package/dist/commands/tasks/list.js +1 -0
  68. package/dist/commands/tasks/show.js +45 -0
  69. package/dist/commands/tasks/update.d.ts +1 -0
  70. package/dist/commands/tasks/update.js +22 -0
  71. package/dist/commands/tasks/upsert.d.ts +1 -0
  72. package/dist/commands/tasks/upsert.js +16 -0
  73. package/dist/lib/agent-config.d.ts +2 -0
  74. package/dist/lib/agent-config.js +3 -1
  75. package/dist/lib/agentic-stream.d.ts +68 -1
  76. package/dist/lib/agentic-stream.js +294 -7
  77. package/dist/lib/api-fetch.js +19 -0
  78. package/dist/lib/auth-core-init.d.ts +2 -10
  79. package/dist/lib/auth-core-init.js +20 -67
  80. package/dist/lib/auth-failure.d.ts +33 -0
  81. package/dist/lib/auth-failure.js +65 -0
  82. package/dist/lib/auth-storage.d.ts +14 -0
  83. package/dist/lib/auth-storage.js +14 -0
  84. package/dist/lib/commitments.d.ts +37 -2
  85. package/dist/lib/commitments.js +148 -0
  86. package/dist/lib/daemonBroker.d.ts +134 -31
  87. package/dist/lib/daemonBroker.js +376 -31
  88. package/dist/lib/dedicated-lease-command.d.ts +14 -0
  89. package/dist/lib/dedicated-lease-command.js +30 -1
  90. package/dist/lib/dedicated-machines.js +8 -25
  91. package/dist/lib/dedicated-service-command.d.ts +11 -3
  92. package/dist/lib/dedicated-service-command.js +20 -3
  93. package/dist/lib/dedicated-service.d.ts +46 -0
  94. package/dist/lib/dedicated-service.js +85 -7
  95. package/dist/lib/dedicated-terminal.d.ts +21 -0
  96. package/dist/lib/dedicated-terminal.js +104 -9
  97. package/dist/lib/delegated-cli.js +4 -1
  98. package/dist/lib/exec-runtime-binary.d.ts +3 -1
  99. package/dist/lib/exec-runtime-binary.js +4 -2
  100. package/dist/lib/first-party-harness-broker.d.ts +10 -0
  101. package/dist/lib/first-party-harness-broker.js +13 -1
  102. package/dist/lib/first-party-harness-doctor.js +41 -1
  103. package/dist/lib/first-party-harness-managed.d.ts +4 -3
  104. package/dist/lib/first-party-harness-project-trust.d.ts +125 -0
  105. package/dist/lib/first-party-harness-project-trust.js +364 -0
  106. package/dist/lib/first-party-harness.d.ts +9 -2
  107. package/dist/lib/first-party-harness.js +6 -5
  108. package/dist/lib/harnesses.d.ts +10 -0
  109. package/dist/lib/hosted-machines.d.ts +10 -1
  110. package/dist/lib/hosted-machines.js +36 -2
  111. package/dist/lib/live-runtime-binary.d.ts +40 -0
  112. package/dist/lib/live-runtime-binary.js +193 -0
  113. package/dist/lib/login.d.ts +23 -0
  114. package/dist/lib/login.js +60 -10
  115. package/dist/lib/machine-spend-cap.d.ts +23 -0
  116. package/dist/lib/machine-spend-cap.js +68 -0
  117. package/dist/lib/message-intent.js +3 -1
  118. package/dist/lib/node-adapter.js +15 -3
  119. package/dist/lib/tasks.d.ts +20 -0
  120. package/dist/lib/tasks.js +54 -0
  121. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +90 -0
  122. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +113 -0
  123. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
  124. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
  125. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.d.ts +47 -0
  126. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +69 -0
  127. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +133 -0
  128. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +189 -0
  129. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
  130. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  131. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
  132. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
  133. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
  134. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
  135. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
  136. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
  137. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
  138. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +8 -3
  139. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +9 -4
  140. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
  141. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +16 -10
  142. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.d.ts +6 -0
  143. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.js +32 -9
  144. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.d.ts +81 -0
  145. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.js +87 -0
  146. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
  147. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
  148. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +6 -1
  149. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +90 -0
  150. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +105 -0
  151. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
  152. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
  153. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.d.ts +47 -0
  154. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +66 -0
  155. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +133 -0
  156. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +182 -0
  157. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
  158. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  159. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
  160. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
  161. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
  162. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
  163. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
  164. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
  165. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
  166. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +8 -3
  167. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +9 -4
  168. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
  169. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +16 -10
  170. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.d.ts +6 -0
  171. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.js +31 -9
  172. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.d.ts +81 -0
  173. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.js +80 -0
  174. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
  175. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
  176. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +6 -1
  177. package/dist/node_modules/@skrr-ai/auth-core/package.json +61 -1
  178. package/dist/node_modules/@skrr-ai/data-provider/index.js +3418 -3344
  179. package/oclif.manifest.json +24813 -24242
  180. package/package.json +4 -1
@@ -576,16 +576,17 @@ function strippedProviderCredentialNames(env = process.env) {
576
576
  * under that name, rather than rely on inheritance.
577
577
  *
578
578
  * The layering, in full: sanitized parent → the user's own model credential (BYOK
579
- * only) → {@link engineOwnedEnv} → `extraEnv`.
579
+ * only) → {@link engineOwnedEnv} → the project policy (OSK-11958) → `extraEnv`.
580
580
  */
581
581
  function engineSpawnEnv(env, extraEnv,
582
582
  // Required, not defaulted: the safe value differs per call and a parameter that
583
583
  // can be forgotten on a security boundary will be.
584
- mode) {
584
+ mode, policyEnv = {}) {
585
585
  const spawnEnv = {
586
586
  ...(0, auth_core_1.sanitizeSpawnEnv)(env),
587
587
  ...(mode === 'managed' ? {} : inheritedProviderCredentials(env)),
588
588
  ...engineOwnedEnv(env),
589
+ ...policyEnv,
589
590
  ...extraEnv,
590
591
  };
591
592
  if (mode !== 'managed')
@@ -671,7 +672,7 @@ async function execEngine(args, env = process.env, options = {}) {
671
672
  const spawnArgs = options.finalizeArgs ? options.finalizeArgs([...args]) : args;
672
673
  // Read AFTER `prepare`, because that is when the hook knows what it acquired.
673
674
  const mode = options.credentialMode?.() ?? 'byok';
674
- return await spawnEngine(bin, spawnArgs, env, extraEnv, mode, outcome);
675
+ return await spawnEngine(bin, spawnArgs, env, extraEnv, mode, outcome, options.policyEnv);
675
676
  }
676
677
  finally {
677
678
  // In a `finally` rather than on `exit`, so a spawn error and a Ctrl-C both
@@ -683,12 +684,12 @@ async function execEngine(args, env = process.env, options = {}) {
683
684
  function spawnEngine(bin, args, env, extraEnv, mode,
684
685
  // Written into rather than returned, so the `finally` above can read it on
685
686
  // every path — including one where this promise rejects.
686
- outcome) {
687
+ outcome, policyEnv) {
687
688
  return new Promise((resolve, reject) => {
688
689
  const isWinShim = process.platform === 'win32' && /\.(cmd|bat)$/i.test(bin);
689
690
  const child = (0, node_child_process_1.spawn)(bin, args, {
690
691
  stdio: 'inherit',
691
- env: engineSpawnEnv(env, extraEnv, mode),
692
+ env: engineSpawnEnv(env, extraEnv, mode, policyEnv),
692
693
  ...(isWinShim ? { shell: true } : {}),
693
694
  });
694
695
  // `stdio: 'inherit'` puts the child in this process group, so Ctrl-C reaches
@@ -64,6 +64,16 @@ export interface Harness {
64
64
  version?: number;
65
65
  claudeEngine?: 'cli' | 'sdk';
66
66
  lastSeenAt?: string;
67
+ /** This harness's own stored permission-mode fallback, when set. */
68
+ defaultPermissionMode?: string | null;
69
+ /**
70
+ * The permission modes this harness can honour, and where the answer came
71
+ * from — sent by `GET /api/harnesses/:id` for a local row (OSK-12130).
72
+ */
73
+ permissionModes?: {
74
+ modes: string[];
75
+ source: 'daemon' | 'daemon_record' | 'static' | 'vocabulary' | string;
76
+ };
67
77
  /**
68
78
  * Registration metadata. A managed machine's rows carry `machineProduct` and
69
79
  * `machineLeaseId`, which is the only link from a lease to the harness rows its
@@ -124,13 +124,22 @@ export declare function hostedMachineStatus(session: HostedMachineSession | unde
124
124
  export declare function hostedMachineDisplayName(session: HostedMachineSession | undefined | null): string;
125
125
  export declare function hostedMachineRepoSummary(session: HostedMachineSession | undefined | null): string;
126
126
  export declare function hostedMachineErrorSummary(session: HostedMachineSession | undefined | null): string;
127
+ /**
128
+ * Whether tenant code on this machine can read the daemon's credentials
129
+ * (OSK-11981). The server always sends a mode — a machine with no record is
130
+ * reported as `shared_uid` — so a missing field here means an older server,
131
+ * and is printed as unknown rather than as the reassuring answer.
132
+ */
133
+ export declare function hostedMachineIsolationSummary(session: HostedMachineSession | undefined | null): string;
127
134
  /** Stable, user-facing snapshot for long provisioning waits. */
128
135
  export declare function hostedMachineProgressSummary(session: HostedMachineSession | undefined | null): string;
129
136
  export declare function isHostedMachineReady(session: HostedMachineSession | undefined | null): boolean;
130
137
  export declare function isHostedMachineTerminal(session: HostedMachineSession | undefined | null): boolean;
131
138
  export declare function parseDurationMs(value: string | undefined, flagName: string): number | undefined;
132
139
  export declare function sleep(ms: number): Promise<void>;
133
- export declare function formatHostedMachineApiError(err: unknown): string | null;
140
+ export declare function formatHostedMachineApiError(err: unknown, context?: {
141
+ bin?: string;
142
+ }): string | null;
134
143
  export declare function startHostedMachine(input: StartHostedMachineInput): Promise<HostedMachineResponse>;
135
144
  export declare function listHostedMachines(scope?: HostedMachineScope & {
136
145
  limit?: number;
@@ -8,6 +8,7 @@ exports.hostedMachineStatus = hostedMachineStatus;
8
8
  exports.hostedMachineDisplayName = hostedMachineDisplayName;
9
9
  exports.hostedMachineRepoSummary = hostedMachineRepoSummary;
10
10
  exports.hostedMachineErrorSummary = hostedMachineErrorSummary;
11
+ exports.hostedMachineIsolationSummary = hostedMachineIsolationSummary;
11
12
  exports.hostedMachineProgressSummary = hostedMachineProgressSummary;
12
13
  exports.isHostedMachineReady = isHostedMachineReady;
13
14
  exports.isHostedMachineTerminal = isHostedMachineTerminal;
@@ -26,6 +27,7 @@ exports.waitForHostedMachineReady = waitForHostedMachineReady;
26
27
  exports.offlineForLabel = offlineForLabel;
27
28
  const api_fetch_1 = require("./api-fetch");
28
29
  const edge_error_page_1 = require("./edge-error-page");
30
+ const machine_spend_cap_1 = require("./machine-spend-cap");
29
31
  function curateHostedMachineList(response) {
30
32
  return {
31
33
  object: 'list',
@@ -107,6 +109,21 @@ function hostedMachineErrorSummary(session) {
107
109
  return error;
108
110
  return error.message || error.class || '';
109
111
  }
112
+ /**
113
+ * Whether tenant code on this machine can read the daemon's credentials
114
+ * (OSK-11981). The server always sends a mode — a machine with no record is
115
+ * reported as `shared_uid` — so a missing field here means an older server,
116
+ * and is printed as unknown rather than as the reassuring answer.
117
+ */
118
+ function hostedMachineIsolationSummary(session) {
119
+ const isolation = session?.identityIsolation;
120
+ if (!isolation?.mode)
121
+ return 'unknown (server did not report it)';
122
+ if (isolation.mode === 'daemon_workload_split') {
123
+ return `daemon credentials isolated from tenant code (contract ${isolation.contract || '?'})`;
124
+ }
125
+ return `shared uid — tenant code can read the daemon's credentials (${isolation.reason || 'unspecified'})`;
126
+ }
110
127
  /** Stable, user-facing snapshot for long provisioning waits. */
111
128
  function hostedMachineProgressSummary(session) {
112
129
  const status = hostedMachineStatus(session);
@@ -142,7 +159,7 @@ function parseDurationMs(value, flagName) {
142
159
  function sleep(ms) {
143
160
  return new Promise((resolve) => setTimeout(resolve, ms));
144
161
  }
145
- function formatHostedMachineApiError(err) {
162
+ function formatHostedMachineApiError(err, context = {}) {
146
163
  if (!(err instanceof api_fetch_1.ApiFetchError) || !err.body) {
147
164
  return null;
148
165
  }
@@ -155,13 +172,30 @@ function formatHostedMachineApiError(err) {
155
172
  if (edge) {
156
173
  return edge.message;
157
174
  }
175
+ let message;
158
176
  try {
159
177
  const parsed = JSON.parse(err.body);
160
- return parsed.message || parsed.error || parsed.code || null;
178
+ message = parsed.message || parsed.error || parsed.code || null;
161
179
  }
162
180
  catch {
163
181
  return err.body.trim() || null;
164
182
  }
183
+ // A monthly-cap refusal carries its figures in `details`; print them, and say
184
+ // this is not the plan or the credits (OSK-11946). The dedicated CLI has done
185
+ // the figures since OSK-9552; the hosted one dropped them.
186
+ if (message && (0, machine_spend_cap_1.isMonthlyCapRefusal)(err.body)) {
187
+ const figures = (0, machine_spend_cap_1.describeMonthlyCapFigures)(err.body);
188
+ return [
189
+ message,
190
+ figures ? `${figures}.` : '',
191
+ machine_spend_cap_1.MONTHLY_CAP_IS_SEPARATE,
192
+ "Start with a higher --spending-limit-cents, or raise an existing machine's cap with " +
193
+ `\`${context.bin || 'skrr'} harnesses leases billing <lease-id> --spending-limit-cents <n>\`.`,
194
+ ]
195
+ .filter(Boolean)
196
+ .join(' ');
197
+ }
198
+ return message;
165
199
  }
166
200
  async function startHostedMachine(input) {
167
201
  return (0, api_fetch_1.apiFetch)('/api/agentic/sandbox-sessions', {
@@ -0,0 +1,40 @@
1
+ export interface LiveProgram {
2
+ /** The executable the OS reports for the live pid. */
3
+ exec: string;
4
+ /** For an interpreter-launched daemon, the script it runs. */
5
+ script: string | null;
6
+ }
7
+ export interface LiveRuntimeDeps {
8
+ /** pid of the live daemon for `profile`, or null. */
9
+ livePid(profile: string): number | null;
10
+ /** The live pid's program, or null when the OS will not say. */
11
+ programOf(pid: number): LiveProgram | null;
12
+ realpath(p: string): string;
13
+ isExecutable(p: string): boolean;
14
+ }
15
+ export type StatusBinaryDecision = {
16
+ kind: 'same';
17
+ binary: string;
18
+ } | {
19
+ kind: 'unknown';
20
+ binary: string;
21
+ } | {
22
+ kind: 'forward-live';
23
+ binary: string;
24
+ pathBinary: string;
25
+ } | {
26
+ kind: 'mismatch';
27
+ binary: string;
28
+ live: string;
29
+ };
30
+ /** The profile a forwarded argv names, `default` otherwise. */
31
+ export declare function profileFromArgs(args: readonly string[]): string;
32
+ /**
33
+ * Decide which binary renders `status`. Never throws: a probe that cannot
34
+ * answer leaves the PATH binary in place (`unknown`).
35
+ */
36
+ export declare function decideStatusBinary(pathBinary: string, profile: string, deps: LiveRuntimeDeps): StatusBinaryDecision;
37
+ /** One stderr line naming the binary that rendered status, when it is not obvious. */
38
+ export declare function describeStatusBinary(decision: StatusBinaryDecision): string | null;
39
+ export declare function scriptFromArgs(exec: string, args: string): string | null;
40
+ export declare function defaultLiveRuntimeDeps(): LiveRuntimeDeps;
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.profileFromArgs = profileFromArgs;
7
+ exports.decideStatusBinary = decideStatusBinary;
8
+ exports.describeStatusBinary = describeStatusBinary;
9
+ exports.scriptFromArgs = scriptFromArgs;
10
+ exports.defaultLiveRuntimeDeps = defaultLiveRuntimeDeps;
11
+ /**
12
+ * Which runtime binary is the LIVE daemon running, and is it the one
13
+ * `skrr daemon status` is about to forward to? (OSK-12102)
14
+ *
15
+ * `execRuntimeBinary` forwards to the first `skrrd` on PATH. That is right for
16
+ * lifecycle verbs, where PATH order is the person's choice of install. It is
17
+ * wrong for `status`, whose whole subject is the daemon that is running: with
18
+ * an old dev build early on PATH and the release daemon live, the header named
19
+ * `.../releases/0.8.62/skrrd` while the body was rendered by the dev build —
20
+ * its old layout, no per-category permission table — and nothing on screen
21
+ * said two binaries were involved.
22
+ *
23
+ * The live daemon's pid comes from its island bootstrap file (the same
24
+ * discovery the CLI's login broker uses); the program from the OS. When the
25
+ * live program is itself a runtime executable, status is forwarded to it.
26
+ * When it cannot be exec'd directly (a `node .../dist/index.js` dev daemon),
27
+ * status stays on the PATH binary and says so.
28
+ */
29
+ const node_child_process_1 = require("node:child_process");
30
+ const node_fs_1 = require("node:fs");
31
+ const node_path_1 = __importDefault(require("node:path"));
32
+ const daemonBroker_1 = require("./daemonBroker");
33
+ const exec_runtime_binary_1 = require("./exec-runtime-binary");
34
+ function safeRealpath(deps, p) {
35
+ try {
36
+ return deps.realpath(p);
37
+ }
38
+ catch {
39
+ return p;
40
+ }
41
+ }
42
+ function isRuntimeName(p) {
43
+ const base = node_path_1.default
44
+ .basename(p)
45
+ .toLowerCase()
46
+ .replace(/\.(exe|cmd)$/, '');
47
+ return exec_runtime_binary_1.RUNTIME_BINARY_NAMES.some((name) => base === name || base.startsWith(`${name}-`));
48
+ }
49
+ /** The profile a forwarded argv names, `default` otherwise. */
50
+ function profileFromArgs(args) {
51
+ for (let i = 0; i < args.length; i += 1) {
52
+ const arg = args[i];
53
+ if (arg === '--')
54
+ break;
55
+ if (arg === '--profile' && args[i + 1] && !args[i + 1].startsWith('-'))
56
+ return args[i + 1];
57
+ if (arg.startsWith('--profile=') && arg.length > '--profile='.length) {
58
+ return arg.slice('--profile='.length);
59
+ }
60
+ }
61
+ return 'default';
62
+ }
63
+ /**
64
+ * Decide which binary renders `status`. Never throws: a probe that cannot
65
+ * answer leaves the PATH binary in place (`unknown`).
66
+ */
67
+ function decideStatusBinary(pathBinary, profile, deps) {
68
+ let program = null;
69
+ try {
70
+ const pid = deps.livePid(profile);
71
+ program = pid ? deps.programOf(pid) : null;
72
+ }
73
+ catch {
74
+ program = null;
75
+ }
76
+ if (!program)
77
+ return { kind: 'unknown', binary: pathBinary };
78
+ const pathReal = safeRealpath(deps, pathBinary);
79
+ const execReal = safeRealpath(deps, program.exec);
80
+ const scriptReal = program.script ? safeRealpath(deps, program.script) : null;
81
+ if (pathReal === execReal || (scriptReal !== null && pathReal === scriptReal)) {
82
+ return { kind: 'same', binary: pathBinary };
83
+ }
84
+ // Only a runtime executable is forwarded to. A `node`/Electron-hosted daemon
85
+ // has an interpreter as its exec, and running the interpreter with status
86
+ // argv would be nonsense.
87
+ const directlyRunnable = program.script === null &&
88
+ (isRuntimeName(program.exec) || isRuntimeName(execReal)) &&
89
+ deps.isExecutable(program.exec);
90
+ if (directlyRunnable) {
91
+ return { kind: 'forward-live', binary: program.exec, pathBinary };
92
+ }
93
+ return { kind: 'mismatch', binary: pathBinary, live: program.script ?? program.exec };
94
+ }
95
+ /** One stderr line naming the binary that rendered status, when it is not obvious. */
96
+ function describeStatusBinary(decision) {
97
+ switch (decision.kind) {
98
+ case 'forward-live':
99
+ return (`Rendered by the live daemon's binary ${decision.binary} ` +
100
+ `(the first skrrd on PATH is ${decision.pathBinary}, a different build).`);
101
+ case 'mismatch':
102
+ return (`Rendered by ${decision.binary}, the first skrrd on PATH — NOT the live daemon's ` +
103
+ `program (${decision.live}). This layout and these checks are that build's, not the live one's.`);
104
+ default:
105
+ return null;
106
+ }
107
+ }
108
+ function darwinProgramOf(pid) {
109
+ const out = (args) => (0, node_child_process_1.execFileSync)('ps', ['-p', String(pid), '-o', ...args], {
110
+ encoding: 'utf8',
111
+ timeout: 2_000,
112
+ stdio: ['ignore', 'pipe', 'ignore'],
113
+ }).trim();
114
+ const exec = out(['comm=']);
115
+ if (!exec || !node_path_1.default.isAbsolute(exec))
116
+ return null;
117
+ return { exec, script: scriptFromArgs(exec, out(['args='])) };
118
+ }
119
+ function linuxProgramOf(pid) {
120
+ const exec = (0, node_fs_1.readlinkSync)(`/proc/${pid}/exe`);
121
+ let script = null;
122
+ try {
123
+ const cmdline = (0, node_fs_1.readFileSync)(`/proc/${pid}/cmdline`, 'utf8');
124
+ script = scriptFromArgs(exec, cmdline.split('\0').filter(Boolean).join(' '));
125
+ }
126
+ catch {
127
+ script = null;
128
+ }
129
+ return { exec, script };
130
+ }
131
+ /**
132
+ * For an interpreter-hosted daemon (`node --import x.mjs /x/dist/index.js
133
+ * start …`), the script: the first argument after the exec that is neither an
134
+ * option nor the value of a module-loading option. `ps -o args=` is
135
+ * space-joined, so a path containing a space is not recovered — the result is
136
+ * only ever used for display and an equality check, where a miss degrades to
137
+ * the "mismatch" note rather than to a wrong forward.
138
+ */
139
+ const INTERPRETER_VALUE_OPTIONS = new Set([
140
+ '-r',
141
+ '--require',
142
+ '--import',
143
+ '--loader',
144
+ '--experimental-loader',
145
+ '-e',
146
+ '--eval',
147
+ ]);
148
+ function scriptFromArgs(exec, args) {
149
+ if (isRuntimeName(exec))
150
+ return null;
151
+ const rest = args.startsWith(exec) ? args.slice(exec.length) : args.replace(/^\S+/, '');
152
+ const tokens = rest.split(/\s+/).filter(Boolean);
153
+ for (let i = 0; i < tokens.length; i += 1) {
154
+ const token = tokens[i];
155
+ if (INTERPRETER_VALUE_OPTIONS.has(token)) {
156
+ i += 1;
157
+ continue;
158
+ }
159
+ if (token.startsWith('-'))
160
+ continue;
161
+ return token;
162
+ }
163
+ return null;
164
+ }
165
+ function defaultLiveRuntimeDeps() {
166
+ return {
167
+ livePid(profile) {
168
+ for (const candidate of (0, daemonBroker_1.resolveExactProfileBootstrapPaths)(profile)) {
169
+ const bootstrap = (0, daemonBroker_1.readBootstrap)(candidate);
170
+ if (typeof bootstrap?.pid === 'number' && bootstrap.pid > 0)
171
+ return bootstrap.pid;
172
+ }
173
+ return null;
174
+ },
175
+ programOf(pid) {
176
+ if (process.platform === 'darwin')
177
+ return darwinProgramOf(pid);
178
+ if (process.platform === 'linux')
179
+ return linuxProgramOf(pid);
180
+ return null;
181
+ },
182
+ realpath: (p) => (0, node_fs_1.realpathSync)(p),
183
+ isExecutable(p) {
184
+ try {
185
+ (0, node_fs_1.accessSync)(p, node_fs_1.constants.X_OK);
186
+ return true;
187
+ }
188
+ catch {
189
+ return false;
190
+ }
191
+ },
192
+ };
193
+ }
@@ -38,6 +38,29 @@ export interface CliLoginWithRefreshTokenOptions {
38
38
  */
39
39
  cliId: string;
40
40
  }
41
+ /**
42
+ * What a shell with no terminal can do instead (OSK-12117). The refusal used to
43
+ * name only CI tokens, which is not a sign-in: an agent session asking for one
44
+ * could only hand the job to a person typing `! skrr login`.
45
+ */
46
+ export declare const HEADLESS_LOGIN_HINT: string;
47
+ /**
48
+ * The line printed when the daemon descriptor this CLI found is for another
49
+ * server (OSK-12117).
50
+ *
51
+ * It used to assert "the skrr background service on this computer is logged in
52
+ * to <url>" and recommend repointing the CLI there. What the CLI actually knows
53
+ * is narrower: ONE descriptor file it read names that server. That file can
54
+ * belong to another profile's daemon (observed: a localhost:3080 descriptor
55
+ * while this machine's default daemon was connected to skrr.ai), and in that
56
+ * case both the claim and the advice were wrong — the advice by moving a
57
+ * production CLI onto a development server. So say what was read, from where,
58
+ * and how to check the daemon itself.
59
+ */
60
+ export declare function describeBrokerBaseUrlMismatch(failure: {
61
+ daemonServerUrl?: string;
62
+ descriptorPath?: string;
63
+ }, serverUrl: string): string;
41
64
  /**
42
65
  * Top-level dispatcher. Picks PKCE localhost vs device-code per the
43
66
  * decision tree in the roadmap and persists the result.
package/dist/lib/login.js CHANGED
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HEADLESS_LOGIN_HINT = void 0;
4
+ exports.describeBrokerBaseUrlMismatch = describeBrokerBaseUrlMismatch;
3
5
  exports.cliLogin = cliLogin;
4
6
  exports.cliLoginWithRefreshToken = cliLoginWithRefreshToken;
5
7
  /**
@@ -35,6 +37,33 @@ const device_code_1 = require("./device-code");
35
37
  const device_identity_1 = require("./device-identity");
36
38
  const oauthLogin_1 = require("./oauthLogin");
37
39
  const daemonBroker_1 = require("./daemonBroker");
40
+ /**
41
+ * What a shell with no terminal can do instead (OSK-12117). The refusal used to
42
+ * name only CI tokens, which is not a sign-in: an agent session asking for one
43
+ * could only hand the job to a person typing `! skrr login`.
44
+ */
45
+ exports.HEADLESS_LOGIN_HINT = '`skrr login --device-code` works without a terminal: it prints a URL and a code for a ' +
46
+ 'person to approve in any browser, and finishes when they do. For CI, use ' +
47
+ '`skrr create-token` and set OVERSKY_TOKEN=osk_ci_...';
48
+ /**
49
+ * The line printed when the daemon descriptor this CLI found is for another
50
+ * server (OSK-12117).
51
+ *
52
+ * It used to assert "the skrr background service on this computer is logged in
53
+ * to <url>" and recommend repointing the CLI there. What the CLI actually knows
54
+ * is narrower: ONE descriptor file it read names that server. That file can
55
+ * belong to another profile's daemon (observed: a localhost:3080 descriptor
56
+ * while this machine's default daemon was connected to skrr.ai), and in that
57
+ * case both the claim and the advice were wrong — the advice by moving a
58
+ * production CLI onto a development server. So say what was read, from where,
59
+ * and how to check the daemon itself.
60
+ */
61
+ function describeBrokerBaseUrlMismatch(failure, serverUrl) {
62
+ const where = failure.descriptorPath ? ` (${failure.descriptorPath})` : '';
63
+ return (` Skipped the daemon hand-off: the daemon descriptor found${where} is for ` +
64
+ `${failure.daemonServerUrl}, not ${serverUrl}. It may belong to a different profile's ` +
65
+ 'daemon; `skrr daemon status` shows which server this profile’s daemon is on.');
66
+ }
38
67
  /**
39
68
  * Top-level dispatcher. Picks PKCE localhost vs device-code per the
40
69
  * decision tree in the roadmap and persists the result.
@@ -83,6 +112,22 @@ async function cliLogin(opts = {}) {
83
112
  baseURL: serverUrl,
84
113
  });
85
114
  if (brokerResult.ok) {
115
+ if (brokerResult.brokered) {
116
+ // Broker-mode hand-off: the daemon served a short-lived access token
117
+ // from a family IT owns. Nothing credential-shaped is persisted here
118
+ // — the token lives for this process and the next `skrr` re-redeems
119
+ // over loopback. An explicit PKCE/device-code login further down
120
+ // still writes a real user-owned family, unchanged.
121
+ console.log('');
122
+ console.log(" This machine is signed in through the platform's managed hand-off.");
123
+ return {
124
+ token: brokerResult.accessToken,
125
+ ...(brokerResult.accessExpiresAt !== undefined
126
+ ? { expiresAt: brokerResult.accessExpiresAt }
127
+ : {}),
128
+ flow: 'daemon-broker',
129
+ };
130
+ }
86
131
  console.log('');
87
132
  console.log(' Authorized via the skrr background service on this computer (no browser needed).');
88
133
  return {
@@ -132,25 +177,30 @@ async function cliLogin(opts = {}) {
132
177
  }
133
178
  if (brokerResult.reason === 'base_url_mismatch' && brokerResult.daemonServerUrl) {
134
179
  console.log('');
135
- console.log(` The skrr background service on this computer is logged in to ${brokerResult.daemonServerUrl}, but skrr is configured for ${serverUrl}.`);
136
- console.log(` Skipping the daemon-broker handoff. Run \`skrr login --base-url ${brokerResult.daemonServerUrl}\` to use the daemon instead.`);
180
+ console.log(describeBrokerBaseUrlMismatch(brokerResult, serverUrl));
137
181
  }
138
182
  }
139
- if ((0, ssh_detect_1.isNonInteractive)()) {
140
- throw new Error(brokerRateLimited
141
- ? 'The skrr background service on this computer is being rate-limited while brokering logins, and this ' +
142
- 'shell cannot run the browser flow instead. The limit is per 15-minute window — ' +
143
- 'retry shortly. For CI, use `skrr create-token` and set OVERSKY_TOKEN=osk_ci_...'
144
- : 'skrr login requires an interactive terminal or an SSH session. ' +
145
- 'For CI, use `skrr create-token` and set OVERSKY_TOKEN=osk_ci_...');
146
- }
147
183
  // --- Decision tree (first match wins) ---
184
+ //
185
+ // Device code BEFORE the terminal check (OSK-12117). It needs no terminal of
186
+ // its own: it prints a URL and a code, a person approves them in any browser,
187
+ // and this process polls until they do. That is the one way an agent session
188
+ // or any other shell without a TTY can finish a login, and the refusal below
189
+ // used to fire first — so `--device-code` was refused for lacking the very
190
+ // thing it does not use.
148
191
  if (opts.forceDeviceCode) {
149
192
  return runDeviceCode(serverUrl, cliId, 'user_requested', ssh);
150
193
  }
151
194
  if (envForce) {
152
195
  return runDeviceCode(serverUrl, cliId, 'env_force', ssh);
153
196
  }
197
+ if ((0, ssh_detect_1.isNonInteractive)()) {
198
+ throw new Error(brokerRateLimited
199
+ ? 'The skrr background service on this computer is being rate-limited while brokering logins, and this ' +
200
+ 'shell cannot run the browser flow instead. The limit is per 15-minute window — ' +
201
+ `retry shortly, or sign in now with ${exports.HEADLESS_LOGIN_HINT}`
202
+ : `skrr login opens a browser, and this shell has no terminal to run it from. ${exports.HEADLESS_LOGIN_HINT}`);
203
+ }
154
204
  if (ssh.detected) {
155
205
  // Device-code's "open this URL on your laptop" UX is the correct
156
206
  // move under SSH — the localhost callback would bind on the remote
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The figures behind a machine monthly-cap refusal, for every machine product.
3
+ *
4
+ * `RUNTIME_SPENDING_LIMIT_EXCEEDED` carries the cap, this month's spend and
5
+ * where the refused action would take it, as integer cents in `details`. The
6
+ * dedicated CLI learned to print them in OSK-9552; the hosted CLI kept printing
7
+ * the bare sentence, "Projected Hosted Machine usage would exceed the monthly
8
+ * cap." A person who had just watched `skrr balance show` report a large
9
+ * balance with overage on then read that as "you have money but we won't take
10
+ * it" (OSK-11946). The machine cap is a SEPARATE ceiling from the plan
11
+ * allowance and the credits, and overage does not raise it; without figures and
12
+ * that sentence nothing on the screen says so.
13
+ */
14
+ export declare function formatCapCents(cents: number): string;
15
+ /**
16
+ * `Cap $500.00; spent this month $12.22; this would bring it to $512.22`, or ''
17
+ * when the body is not a cap refusal or carries no figures.
18
+ */
19
+ export declare function describeMonthlyCapFigures(body: string | undefined): string;
20
+ /** Whether `body` is a machine monthly-cap refusal at all. */
21
+ export declare function isMonthlyCapRefusal(body: string | undefined): boolean;
22
+ /** The one sentence that says which ceiling this is — and which it is not. */
23
+ export declare const MONTHLY_CAP_IS_SEPARATE = "This is the machine monthly cap, separate from your plan allowance and credits; overage does not raise it.";
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ /**
3
+ * The figures behind a machine monthly-cap refusal, for every machine product.
4
+ *
5
+ * `RUNTIME_SPENDING_LIMIT_EXCEEDED` carries the cap, this month's spend and
6
+ * where the refused action would take it, as integer cents in `details`. The
7
+ * dedicated CLI learned to print them in OSK-9552; the hosted CLI kept printing
8
+ * the bare sentence, "Projected Hosted Machine usage would exceed the monthly
9
+ * cap." A person who had just watched `skrr balance show` report a large
10
+ * balance with overage on then read that as "you have money but we won't take
11
+ * it" (OSK-11946). The machine cap is a SEPARATE ceiling from the plan
12
+ * allowance and the credits, and overage does not raise it; without figures and
13
+ * that sentence nothing on the screen says so.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.MONTHLY_CAP_IS_SEPARATE = void 0;
17
+ exports.formatCapCents = formatCapCents;
18
+ exports.describeMonthlyCapFigures = describeMonthlyCapFigures;
19
+ exports.isMonthlyCapRefusal = isMonthlyCapRefusal;
20
+ function formatCapCents(cents) {
21
+ const sign = cents < 0 ? '-' : '';
22
+ return `${sign}$${(Math.abs(cents) / 100).toFixed(2)}`;
23
+ }
24
+ /** The refusal's `code` and `details`, when the body is JSON; null otherwise. */
25
+ function parseRefusal(body) {
26
+ if (!body)
27
+ return null;
28
+ try {
29
+ const parsed = JSON.parse(body);
30
+ const details = parsed?.details && typeof parsed.details === 'object' && !Array.isArray(parsed.details)
31
+ ? parsed.details
32
+ : undefined;
33
+ return { code: typeof parsed?.code === 'string' ? parsed.code : undefined, details };
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ /**
40
+ * `Cap $500.00; spent this month $12.22; this would bring it to $512.22`, or ''
41
+ * when the body is not a cap refusal or carries no figures.
42
+ */
43
+ function describeMonthlyCapFigures(body) {
44
+ const refusal = parseRefusal(body);
45
+ if (refusal?.code !== 'RUNTIME_SPENDING_LIMIT_EXCEEDED' || !refusal.details)
46
+ return '';
47
+ const details = refusal.details;
48
+ const cents = (key) => {
49
+ const value = details[key];
50
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
51
+ };
52
+ const limit = cents('limitCents');
53
+ const spent = cents('currentSpendCents');
54
+ const projected = cents('projectedCents');
55
+ return [
56
+ limit !== null ? `Cap ${formatCapCents(limit)}` : '',
57
+ spent !== null ? `spent this month ${formatCapCents(spent)}` : '',
58
+ projected !== null ? `this would bring it to ${formatCapCents(projected)}` : '',
59
+ ]
60
+ .filter(Boolean)
61
+ .join('; ');
62
+ }
63
+ /** Whether `body` is a machine monthly-cap refusal at all. */
64
+ function isMonthlyCapRefusal(body) {
65
+ return parseRefusal(body)?.code === 'RUNTIME_SPENDING_LIMIT_EXCEEDED';
66
+ }
67
+ /** The one sentence that says which ceiling this is — and which it is not. */
68
+ exports.MONTHLY_CAP_IS_SEPARATE = 'This is the machine monthly cap, separate from your plan allowance and credits; overage does not raise it.';
@@ -11,6 +11,7 @@ exports.canonicalSlackChannelRequest = canonicalSlackChannelRequest;
11
11
  exports.canonicalSlackOwnerDmRequest = canonicalSlackOwnerDmRequest;
12
12
  exports.rejectAutonomousSlackTarget = rejectAutonomousSlackTarget;
13
13
  const auth_core_1 = require("@skrr-ai/auth-core");
14
+ const loopback_http_1 = require("@skrr-ai/auth-core/loopback-http");
14
15
  const api_fetch_1 = require("./api-fetch");
15
16
  const daemonBroker_1 = require("./daemonBroker");
16
17
  const runtime_context_1 = require("./runtime-context");
@@ -152,7 +153,8 @@ async function submitDaemonLoopbackMessageIntent({ context, request, env, bootst
152
153
  });
153
154
  try {
154
155
  const response = await Promise.race([
155
- (fetchImpl || globalThis.fetch)(`http://${bootstrap.host}:${bootstrap.port}/v1/agent/message-intents`, {
156
+ // The daemon secret and session token stay on loopback (OSK-12134).
157
+ (fetchImpl || loopback_http_1.loopbackFetch)(`http://${bootstrap.host}:${bootstrap.port}/v1/agent/message-intents`, {
156
158
  method: 'POST',
157
159
  headers: {
158
160
  Authorization: `Bearer ${bootstrap.secret}`,