runcloud 0.1.117 → 0.1.119

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/diagnose.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
- import { GoogleAuth, Impersonated } from 'google-auth-library';
3
+ import { GoogleAuth } from 'google-auth-library';
4
4
  import { renderSessionDiagnosticTerminal, writeSessionDiagnosticBundle, } from './sessionDiagnostics.js';
5
5
  const execFileAsync = promisify(execFile);
6
6
  const AUTH_ATTEMPT_TIMEOUT_MS = 10_000;
@@ -87,10 +87,8 @@ function extractAuthorization(headers) {
87
87
  const h = headers;
88
88
  return h?.Authorization ?? h?.authorization;
89
89
  }
90
- async function gcloudIdentityToken(audience, serviceAccount) {
91
- const args = ['auth', 'print-identity-token', `--audiences=${audience}`];
92
- if (serviceAccount)
93
- args.push(`--impersonate-service-account=${serviceAccount}`);
90
+ async function gcloudIdentityToken(audience) {
91
+ const args = ['auth', 'print-identity-token', '--audiences=' + audience];
94
92
  const { stdout } = await execFileAsync('gcloud', args, { timeout: 15_000, maxBuffer: 1024 * 1024 });
95
93
  const token = stdout.trim();
96
94
  if (!token)
@@ -123,22 +121,32 @@ function exactAudienceAuthorization(authorization, audience) {
123
121
  }
124
122
  return authorization;
125
123
  }
126
- async function impersonatedIdentityToken(audience, serviceAccount) {
127
- return withDeadline('service-account impersonation', async (signal) => {
124
+ async function serviceAccountIdentityToken(audience, serviceAccount) {
125
+ return withDeadline('service-account ID-token minting', async (signal) => {
128
126
  const sourceClient = await new GoogleAuth({
129
127
  projectId: AUTH_DISCOVERY_PROJECT_ID,
130
128
  scopes: ['https://www.googleapis.com/auth/cloud-platform'],
131
129
  clientOptions: { transporterOptions: { signal, timeout: AUTH_ATTEMPT_TIMEOUT_MS } },
132
130
  }).getClient();
133
- const impersonated = new Impersonated({
134
- sourceClient,
135
- targetPrincipal: serviceAccount,
136
- targetScopes: ['https://www.googleapis.com/auth/cloud-platform'],
137
- lifetime: 600,
138
- transporterOptions: { signal, timeout: AUTH_ATTEMPT_TIMEOUT_MS },
131
+ const sourceToken = await sourceClient.getAccessToken();
132
+ const authorization = typeof sourceToken === 'string' ? sourceToken : sourceToken.token;
133
+ if (!authorization)
134
+ throw new Error('ADC returned an empty source access token');
135
+ const response = await fetch('https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/' +
136
+ encodeURIComponent(serviceAccount) + ':generateIdToken', {
137
+ method: "POST",
138
+ headers: {
139
+ Authorization: 'Bearer ' + authorization,
140
+ 'Content-Type': 'application/json',
141
+ },
142
+ body: JSON.stringify({ audience, includeEmail: true }),
143
+ signal,
139
144
  });
140
- const token = await impersonated.fetchIdToken(audience, { includeEmail: true });
141
- return exactAudienceAuthorization(`Bearer ${token}`, audience);
145
+ const body = await response.json().catch(() => ({}));
146
+ if (!response.ok || typeof body.token !== 'string') {
147
+ throw new Error("generateIdToken failed (" + response.status + "): " + JSON.stringify(body.error ?? body));
148
+ }
149
+ return exactAudienceAuthorization('Bearer ' + body.token, audience);
142
150
  });
143
151
  }
144
152
  export async function resolveOpsAuthHeader(env = process.env, requestedAudience, requestedServiceAccount) {
@@ -158,13 +166,13 @@ export async function resolveOpsAuthHeader(env = process.env, requestedAudience,
158
166
  catch (error) {
159
167
  adcError = error instanceof Error ? error.message : String(error);
160
168
  }
161
- let impersonationError = 'not configured';
169
+ let serviceAccountError = 'not configured';
162
170
  if (serviceAccount && env.DIAGNOSE_DISABLE_IMPERSONATION !== '1') {
163
171
  try {
164
- return await impersonatedIdentityToken(audience, serviceAccount);
172
+ return await serviceAccountIdentityToken(audience, serviceAccount);
165
173
  }
166
174
  catch (error) {
167
- impersonationError = error instanceof Error ? error.message : String(error);
175
+ serviceAccountError = error instanceof Error ? error.message : String(error);
168
176
  }
169
177
  }
170
178
  const token = env.DIAGNOSE_OPS_TOKEN;
@@ -173,13 +181,13 @@ export async function resolveOpsAuthHeader(env = process.env, requestedAudience,
173
181
  }
174
182
  if (env.DIAGNOSE_DISABLE_GCLOUD !== '1') {
175
183
  try {
176
- return exactAudienceAuthorization(`Bearer ${await gcloudIdentityToken(audience, serviceAccount)}`, audience);
184
+ return exactAudienceAuthorization(`Bearer ${await gcloudIdentityToken(audience)}`, audience);
177
185
  }
178
186
  catch (error) {
179
187
  const gcloudError = error instanceof Error ? error.message : String(error);
180
188
  throw new Error(`No usable ops credential for audience ${audience}.\n` +
181
189
  `ADC failed: ${truncate(adcError, 500)}\n` +
182
- `Service-account impersonation failed: ${truncate(impersonationError, 500)}\n` +
190
+ `Service-account ID-token minting failed: ${truncate(serviceAccountError, 500)}\n` +
183
191
  `gcloud failed: ${truncate(gcloudError, 500)}\n\n` +
184
192
  'Repair access by adding the Google identity to control-plane `operators`, applying Terraform, ' +
185
193
  'and running `gcloud auth application-default login`, then rerun. ' +
@@ -188,7 +196,7 @@ export async function resolveOpsAuthHeader(env = process.env, requestedAudience,
188
196
  }
189
197
  throw new Error(`No ops credential available for audience ${audience}. ` +
190
198
  `ADC failed: ${truncate(adcError, 300)}. ` +
191
- `Service-account impersonation failed: ${truncate(impersonationError, 300)}. ` +
199
+ `Service-account ID-token minting failed: ${truncate(serviceAccountError, 300)}. ` +
192
200
  'Run `gcloud auth application-default login` after the operator Terraform has been applied, or export DIAGNOSE_OPS_TOKEN.');
193
201
  }
194
202
  function truncate(value, max = 200) {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.1.117';
1
+ export const CLI_VERSION = '0.1.119';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.1.117",
3
+ "version": "0.1.119",
4
4
  "description": "Create and control run.cloud remote mobile simulators and cloud sandboxes",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
Binary file
@@ -12,11 +12,17 @@ Keep this skill as a router. Do not duplicate operational instructions here.
12
12
  iframe embeds, Metro tunnels, mobile assets, and mobile session cleanup.
13
13
  - Use `$run-cloud-sandboxes` for microVM sandboxes, command execution, files,
14
14
  snapshots, images, SSH, secrets, public ports, desktop automation, resource
15
- sizing, and sandbox cleanup.
15
+ sizing, sandbox cleanup, and investigating a broken sandbox.
16
16
  - Use both focused skills when a workflow genuinely combines mobile sessions
17
17
  and sandboxes. Follow each skill's authentication, security, metering, and
18
18
  cleanup guardrails.
19
19
 
20
+ Symptom phrasing ("is down", "stuck", "hung", "not responding", "why did it
21
+ stop") names no resource, so route on the id in hand: a sandbox id or tag goes to
22
+ `$run-cloud-sandboxes`, a session id to `$run-cloud-ios-simulator`. Each says
23
+ what its own signals do and do not mean, which is the part that cannot be
24
+ guessed from the outside.
25
+
20
26
  If the request is ambiguous, infer the product from the resource noun and
21
27
  desired outcome. Ask only when choosing the wrong product would materially
22
28
  change the work.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: run-cloud-sandboxes
3
- description: Operate run.cloud microVM sandboxes with the CLI or TypeScript SDK. Use for creating isolated compute, running commands, moving files, managing snapshots or images, exposing ports, using SSH or desktop automation, attaching secrets, sizing resources, or cleaning up sandboxes.
3
+ description: Operate and debug run.cloud microVM sandboxes with the CLI or TypeScript SDK. Use for creating isolated compute, running commands, moving files, managing snapshots or images, exposing ports, using SSH or desktop automation, attaching secrets, sizing resources, or cleaning up sandboxes. Also use to investigate a sandbox that is down, stuck, hung, unreachable, or not responding, to find out why one stopped or was destroyed, or to trace a sandbox from a log line by tag.
4
4
  ---
5
5
 
6
6
  # Operate run.cloud Sandboxes
@@ -274,6 +274,55 @@ Key names do not match the env vars, and `sops` needs
274
274
  see the Secrets section of the root `CLAUDE.md`/`AGENTS.md` before concluding
275
275
  you lack access.
276
276
 
277
+ ## What the Signals Mean When a Sandbox Misbehaves
278
+
279
+ `state` describes the microVM lease, not the workload inside it. A sandbox whose
280
+ process died an hour ago still reads `running`, so `running` on its own never
281
+ means healthy.
282
+
283
+ The console log is one file per sandbox carrying boot output, everything `exec`
284
+ printed, and lifecycle markers, interleaved in the order they happened.
285
+ `runcloud sandbox logs <id>` serves it, and it survives destroy: the host renames
286
+ the file rather than deleting it, so a post-mortem read works on a sandbox that
287
+ no longer exists.
288
+
289
+ Two limits that change what you can conclude from it:
290
+
291
+ - It is capped at 10 MiB and trimmed to the newest 5 MiB, so on a chatty workload
292
+ early output is gone, not merely paged out. An empty-looking start is a trim,
293
+ not proof the process printed nothing.
294
+ - `systemd-run` output goes to the journal, not the console, so those units never
295
+ appear here at all. Read `journalctl -u <unit>` inside the sandbox instead.
296
+
297
+ ### Lifecycle markers
298
+
299
+ The host writes a marker into the console on every transition:
300
+
301
+ ```
302
+ --- sandbox destroyed: timeout-sweep at 2026-08-05T11:02:11Z ---
303
+ ```
304
+
305
+ The events are `created`, `paused`, `resumed`, `stopped`, and `destroyed`. The
306
+ token after the colon is the control plane's actor, written verbatim so it can be
307
+ matched rather than parsed; the sentence around it is not a contract. Only stops
308
+ carry a reason, so `created` and `resumed` appear without one.
309
+
310
+ | actor | what it means |
311
+ | --- | --- |
312
+ | `timeout-sweep` | hit its `--timeout` lifetime cap. Not a crash |
313
+ | `idle-sweep` | idle-paused after `--idle-pause` seconds. Resumable, and not a failure |
314
+ | `retention-sweep` | destroyed after its 48h parked window expired |
315
+ | `pause` `resume` `stop` `archive` `restore` | a caller requested exactly this |
316
+ | `api` `api-force` | a caller destroyed it. `api-force` bypassed a wedged guest |
317
+ | `health-sweep` `host-reconcile` `reap-sweep` | the platform reclaimed it |
318
+ | `boot-failure` `placement-timeout` `request-timeout` `scheduler` | it never started. Capacity or image, not the workload |
319
+ | `image-built` `image-build-failure` | an async image build finished or failed |
320
+ | `startup-recover` `volume-lease-invalid` `replica-fork-resume-failure` | platform recovery paths |
321
+
322
+ A sandbox that failed before it ever reached a host has no console, because the
323
+ file is created at boot. Those carry a `Last error` row on
324
+ `runcloud sandbox get` instead, which is then the only record of the cause.
325
+
277
326
  ## Guardrails
278
327
 
279
328
  - Destroy every sandbox created during a task unless the user explicitly asks
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Run Cloud Sandboxes"
3
- short_description: "Operate secure remote microVM sandboxes"
4
- default_prompt: "Use $run-cloud-sandboxes to create, operate, and clean up run.cloud sandboxes."
3
+ short_description: "Operate and debug secure remote microVM sandboxes"
4
+ default_prompt: "Use $run-cloud-sandboxes to create, operate, debug, and clean up run.cloud sandboxes."
Binary file