@intentius/chant-lexicon-fly 0.89.0 → 0.90.0

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 (37) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/integrity.json +3 -3
  4. package/dist/manifest.json +1 -1
  5. package/dist/op/activities/emulator-images.d.ts +13 -0
  6. package/dist/op/activities/emulator-images.d.ts.map +1 -1
  7. package/dist/op/activities/index.d.ts +4 -2
  8. package/dist/op/activities/index.d.ts.map +1 -1
  9. package/dist/op/activities/sprite-services.d.ts +158 -0
  10. package/dist/op/activities/sprite-services.d.ts.map +1 -0
  11. package/dist/op/activities/sprites-contract.d.ts +17 -1
  12. package/dist/op/activities/sprites-contract.d.ts.map +1 -1
  13. package/dist/op/activities/sprites-emulator.d.ts +12 -0
  14. package/dist/op/activities/sprites-emulator.d.ts.map +1 -1
  15. package/dist/op/activities/sprites-fake.d.ts +4 -0
  16. package/dist/op/activities/sprites-fake.d.ts.map +1 -1
  17. package/dist/op/activities/sprites.d.ts +53 -0
  18. package/dist/op/activities/sprites.d.ts.map +1 -1
  19. package/dist/op/builders.d.ts +20 -1
  20. package/dist/op/builders.d.ts.map +1 -1
  21. package/dist/skills/chant-fly-sprites.md +4 -2
  22. package/package.json +2 -2
  23. package/src/index.ts +9 -0
  24. package/src/op/activities/emulator-images.ts +14 -0
  25. package/src/op/activities/index.ts +34 -0
  26. package/src/op/activities/sprite-config.test.ts +19 -0
  27. package/src/op/activities/sprite-services.docker.integration.test.ts +87 -0
  28. package/src/op/activities/sprite-services.test.ts +263 -0
  29. package/src/op/activities/sprite-services.ts +311 -0
  30. package/src/op/activities/sprites-contract.test.ts +15 -4
  31. package/src/op/activities/sprites-contract.ts +24 -6
  32. package/src/op/activities/sprites-emulator.ts +52 -1
  33. package/src/op/activities/sprites-fake.ts +57 -8
  34. package/src/op/activities/sprites.test.ts +102 -0
  35. package/src/op/activities/sprites.ts +108 -7
  36. package/src/op/builders.ts +28 -1
  37. package/src/skills/chant-fly-sprites.md +4 -2
@@ -1,6 +1,7 @@
1
1
  import { describe, test, expect } from "vitest";
2
2
  import {
3
3
  resolveSpritesEndpoint,
4
+ resolveSpritesToken,
4
5
  DEFAULT_SPRITES_BASE_URL,
5
6
  accumulateExecFrames,
6
7
  parseCheckpointNdjson,
@@ -13,6 +14,8 @@ import {
13
14
  spriteRestore,
14
15
  listCheckpoints,
15
16
  spriteDestroy,
17
+ spriteDelete,
18
+ spriteUrl,
16
19
  type SpritesHttp,
17
20
  type Checkpoint,
18
21
  } from "./sprites";
@@ -141,6 +144,50 @@ describe("resolveSpritesEndpoint (S3)", () => {
141
144
  test("default is real Sprites", () => {
142
145
  expect(resolveSpritesEndpoint({}, {} as NodeJS.ProcessEnv)).toBe(DEFAULT_SPRITES_BASE_URL);
143
146
  });
147
+
148
+ // #2711 — the studio and wisp call it SPRITES_API_URL.
149
+ test("SPRITES_API_URL alias when SPRITES_BASE_URL is unset", () => {
150
+ expect(resolveSpritesEndpoint({}, { SPRITES_API_URL: "http://alias:9000" } as NodeJS.ProcessEnv)).toBe(
151
+ "http://alias:9000",
152
+ );
153
+ });
154
+
155
+ test("SPRITES_BASE_URL wins over SPRITES_API_URL", () => {
156
+ expect(
157
+ resolveSpritesEndpoint(
158
+ {},
159
+ { SPRITES_BASE_URL: "http://base:1", SPRITES_API_URL: "http://alias:2" } as NodeJS.ProcessEnv,
160
+ ),
161
+ ).toBe("http://base:1");
162
+ });
163
+ });
164
+
165
+ describe("resolveSpritesToken (#2711)", () => {
166
+ test("explicit arg wins", () => {
167
+ expect(resolveSpritesToken("arg-token", { SPRITES_API_TOKEN: "env" } as NodeJS.ProcessEnv)).toBe("arg-token");
168
+ });
169
+
170
+ test("SPRITES_API_TOKEN env when no arg", () => {
171
+ expect(resolveSpritesToken(undefined, { SPRITES_API_TOKEN: "env-token" } as NodeJS.ProcessEnv)).toBe(
172
+ "env-token",
173
+ );
174
+ });
175
+
176
+ test("SPRITE_TOKEN alias when SPRITES_API_TOKEN is unset", () => {
177
+ expect(resolveSpritesToken(undefined, { SPRITE_TOKEN: "alias-token" } as NodeJS.ProcessEnv)).toBe(
178
+ "alias-token",
179
+ );
180
+ });
181
+
182
+ test("SPRITES_API_TOKEN wins over SPRITE_TOKEN", () => {
183
+ expect(
184
+ resolveSpritesToken(undefined, { SPRITES_API_TOKEN: "orig", SPRITE_TOKEN: "alias" } as NodeJS.ProcessEnv),
185
+ ).toBe("orig");
186
+ });
187
+
188
+ test("undefined when nothing is set", () => {
189
+ expect(resolveSpritesToken(undefined, {} as NodeJS.ProcessEnv)).toBeUndefined();
190
+ });
144
191
  });
145
192
 
146
193
  // ── Command tokenizing + exec WS url ──────────────────────────────────────────
@@ -263,6 +310,61 @@ describe("spriteDestroy", () => {
263
310
  });
264
311
  });
265
312
 
313
+ describe("spriteDelete (#2711)", () => {
314
+ test("is spriteDestroy under another name — same DELETE, same idempotence", async () => {
315
+ expect(spriteDelete).toBe(spriteDestroy);
316
+ const { http, calls } = recorder(() => ({ status: 200, text: "{}" }));
317
+ await spriteDelete({ id: "task-1", endpoint: "http://x" }, undefined, http);
318
+ expect(calls[0].method).toBe("DELETE");
319
+ expect(calls[0].url).toBe("http://x/v1/sprites/task-1");
320
+ });
321
+ });
322
+
323
+ describe("spriteUrl (#2711)", () => {
324
+ test("GETs /v1/sprites/{id} and returns its url", async () => {
325
+ const { http, calls } = recorder(() => ({ status: 200, text: JSON.stringify({ url: "http://h/s/task-1" }) }));
326
+ const res = await spriteUrl({ id: "task-1", endpoint: "http://x" }, undefined, http);
327
+ expect(res).toEqual({ url: "http://h/s/task-1" });
328
+ expect(calls[0].method).toBe("GET");
329
+ expect(calls[0].url).toBe("http://x/v1/sprites/task-1");
330
+ });
331
+
332
+ test("throws when the sprite has no url", async () => {
333
+ const { http } = recorder(() => ({ status: 200, text: "{}" }));
334
+ await expect(spriteUrl({ id: "task-1", endpoint: "http://x" }, undefined, http)).rejects.toThrow(/no url/);
335
+ });
336
+
337
+ test("with a path: returns immediately once the check answers 2xx", async () => {
338
+ let checks = 0;
339
+ const { http, calls } = recorder((method, url) => {
340
+ if (method === "GET" && url === "http://x/v1/sprites/task-1") {
341
+ return { status: 200, text: JSON.stringify({ url: "http://h/s/task-1" }) };
342
+ }
343
+ checks += 1;
344
+ return { status: checks < 2 ? 503 : 200, text: "" };
345
+ });
346
+ const res = await spriteUrl(
347
+ { id: "task-1", endpoint: "http://x", path: "/", intervalMs: 1 },
348
+ undefined,
349
+ http,
350
+ );
351
+ expect(res).toEqual({ url: "http://h/s/task-1" });
352
+ expect(checks).toBe(2); // one 503, then 200
353
+ expect(calls.at(-1)?.url).toBe("http://h/s/task-1/");
354
+ });
355
+
356
+ test("with a path: throws once timeoutMs elapses without a match", async () => {
357
+ const { http } = recorder((method, url) =>
358
+ url === "http://x/v1/sprites/task-1"
359
+ ? { status: 200, text: JSON.stringify({ url: "http://h/s/task-1" }) }
360
+ : { status: 503, text: "nothing is answering" },
361
+ );
362
+ await expect(
363
+ spriteUrl({ id: "task-1", endpoint: "http://x", path: "/", timeoutMs: 5, intervalMs: 1 }, undefined, http),
364
+ ).rejects.toThrow(/did not answer/);
365
+ });
366
+ });
367
+
266
368
  // ── Bearer header on the default client ───────────────────────────────────────
267
369
 
268
370
  describe("defaultSpritesHttp bearer header", () => {
@@ -20,6 +20,13 @@
20
20
  * S3: endpoint override via `SPRITES_BASE_URL` (an explicit `endpoint` arg wins,
21
21
  * then the env, then the real Sprites base), so the same Op targets real Sprites
22
22
  * or the in-process fake with no code change.
23
+ *
24
+ * #2711: the studio and wisp's own docs call these `SPRITES_API_URL` and
25
+ * `SPRITE_TOKEN` rather than `SPRITES_BASE_URL`/`SPRITES_API_TOKEN`. Both
26
+ * pairs are accepted — `resolveSpritesEndpoint`/`resolveSpritesToken` read the
27
+ * alias when the original name is unset, with the original always winning
28
+ * when both are present, so an existing deployment is never surprised by the
29
+ * alias taking over.
23
30
  */
24
31
 
25
32
  // `ws` is loaded inside spriteExec, not at the top. It is a CommonJS package
@@ -27,6 +34,8 @@
27
34
  // ESM, where that require throws. The top-level import put it on the path of
28
35
  // every project that imports the lexicon and needs the run fallback (#2613).
29
36
 
37
+ import { sleep } from "@intentius/chant/op";
38
+
30
39
  export const DEFAULT_SPRITES_BASE_URL = "https://api.sprites.dev";
31
40
 
32
41
  // ── Pure helpers (unit-testable without http/ws) ──────────────────────────────
@@ -41,15 +50,27 @@ export function resolveSpritesEndpoint(
41
50
  args: { endpoint?: string } = {},
42
51
  env: NodeJS.ProcessEnv = process.env,
43
52
  ): string {
44
- const base = args.endpoint || env.SPRITES_BASE_URL || DEFAULT_SPRITES_BASE_URL;
53
+ const base = args.endpoint || env.SPRITES_BASE_URL || env.SPRITES_API_URL || DEFAULT_SPRITES_BASE_URL;
45
54
  return base.replace(/\/$/, "");
46
55
  }
47
56
 
57
+ /**
58
+ * Resolve the bearer token (#2711): an explicit `token` arg wins, then
59
+ * `SPRITES_API_TOKEN`, then its alias `SPRITE_TOKEN`. Pure — mirrors
60
+ * `resolveSpritesEndpoint`.
61
+ */
62
+ export function resolveSpritesToken(
63
+ token: string | undefined = undefined,
64
+ env: NodeJS.ProcessEnv = process.env,
65
+ ): string | undefined {
66
+ return token ?? env.SPRITES_API_TOKEN ?? env.SPRITE_TOKEN;
67
+ }
68
+
48
69
  const spritesUrl = (base: string): string => `${base}/v1/sprites`;
49
- const spriteUrl = (base: string, id: string): string => `${spritesUrl(base)}/${encodeURIComponent(id)}`;
70
+ const spriteResourceUrl = (base: string, id: string): string => `${spritesUrl(base)}/${encodeURIComponent(id)}`;
50
71
  // Create uses REST JSON; exec is the control WebSocket below; checkpoints are NDJSON.
51
- const spriteCheckpointUrl = (base: string, id: string): string => `${spriteUrl(base, id)}/checkpoint`;
52
- const spriteCheckpointsUrl = (base: string, id: string): string => `${spriteUrl(base, id)}/checkpoints`;
72
+ const spriteCheckpointUrl = (base: string, id: string): string => `${spriteResourceUrl(base, id)}/checkpoint`;
73
+ const spriteCheckpointsUrl = (base: string, id: string): string => `${spriteResourceUrl(base, id)}/checkpoints`;
53
74
  const spriteCheckpointRestoreUrl = (base: string, id: string, cp: string): string =>
54
75
  `${spriteCheckpointsUrl(base, id)}/${encodeURIComponent(cp)}/restore`;
55
76
 
@@ -354,7 +375,7 @@ export function defaultSpritesHttp(token?: string, fetchImpl: typeof fetch = fet
354
375
  return async (method, url, body, headers, signal) => {
355
376
  const h: Record<string, string> = { ...headers };
356
377
  if (body !== undefined) h["content-type"] = "application/json";
357
- const tok = token ?? process.env.SPRITES_API_TOKEN;
378
+ const tok = resolveSpritesToken(token);
358
379
  if (tok) h["authorization"] = `Bearer ${tok}`;
359
380
  const res = await fetchImpl(url, {
360
381
  method,
@@ -393,7 +414,7 @@ export async function spriteCreate(
393
414
  export async function spriteExec(args: SpriteExecArgs, signal?: AbortSignal): Promise<SpriteExecResult> {
394
415
  const base = resolveSpritesEndpoint(args);
395
416
  const url = spriteExecWsUrl(base, args.id, args.cmd);
396
- const token = args.token ?? process.env.SPRITES_API_TOKEN;
417
+ const token = resolveSpritesToken(args.token);
397
418
  const headers: Record<string, string> = {};
398
419
  if (token) headers.Authorization = `Bearer ${token}`;
399
420
  const { default: WebSocket } = await import("ws");
@@ -522,10 +543,90 @@ export async function spriteDestroy(
522
543
  http: SpritesHttp = defaultSpritesHttp(args.token),
523
544
  ): Promise<Record<string, never>> {
524
545
  const base = resolveSpritesEndpoint(args);
525
- const res = await http("DELETE", spriteUrl(base, args.id), undefined, undefined, signal);
546
+ const res = await http("DELETE", spriteResourceUrl(base, args.id), undefined, undefined, signal);
526
547
  if (res.status >= 300 && res.status !== 404) {
527
548
  throw new Error(`sprite ${args.id} destroy failed (${res.status}): ${res.text}`);
528
549
  }
529
550
  console.log(`destroyed: sprite/${args.id} (${base})`);
530
551
  return {};
531
552
  }
553
+
554
+ /**
555
+ * Delete the sprite (#2711) — the same idempotent `DELETE /v1/sprites/{id}` as
556
+ * `spriteDestroy`, under the name spritzer, wisp and the other lexicons'
557
+ * delete activities (`awsDelete`, `gcpDelete`, `azDelete`) use. A literal
558
+ * alias, not a reimplementation, so the two names can never drift: an Op
559
+ * authored against either resolves to the same call. `spriteDestroy` stays —
560
+ * it is the name every existing Op, example and the `op-verb-class` registry
561
+ * already depend on.
562
+ */
563
+ export const spriteDelete = spriteDestroy;
564
+ export type SpriteDeleteArgs = SpriteDestroyArgs;
565
+
566
+ export interface SpriteUrlArgs {
567
+ /** Target sprite id. */
568
+ id: string;
569
+ /**
570
+ * Wait until this path on the sprite's URL answers, instead of returning
571
+ * the URL immediately. Unset (default): no wait, just resolve the URL.
572
+ */
573
+ path?: string;
574
+ /** Expected status while waiting. Default: any 2xx. */
575
+ status?: number;
576
+ /** Max time to wait for `path` to answer, ms. Default: `10000`. */
577
+ timeoutMs?: number;
578
+ /** Delay between polls, ms. Default: `500`. */
579
+ intervalMs?: number;
580
+ endpoint?: string;
581
+ token?: string;
582
+ }
583
+
584
+ export interface SpriteUrlResult {
585
+ url: string;
586
+ }
587
+
588
+ /**
589
+ * Resolve the sprite's URL (#2711) — `spriteCreate` returns it too, but
590
+ * nothing until now exposed it on its own (for a later phase that only has
591
+ * the sprite `id`) or waited on it. `GET /v1/sprites/{id}` for the URL; with
592
+ * `path` set, polls `GET {url}{path}` until `status` (default any 2xx) or
593
+ * `timeoutMs` runs out — the box's door/hud/chud services take a moment to
594
+ * bind their `http_port` after `spriteServiceStart`.
595
+ */
596
+ export async function spriteUrl(
597
+ args: SpriteUrlArgs,
598
+ signal?: AbortSignal,
599
+ http: SpritesHttp = defaultSpritesHttp(args.token),
600
+ ): Promise<SpriteUrlResult> {
601
+ const base = resolveSpritesEndpoint(args);
602
+ const res = await http("GET", spriteResourceUrl(base, args.id), undefined, undefined, signal);
603
+ if (res.status >= 300) throw new Error(`sprite ${args.id} url lookup failed (${res.status}): ${res.text}`);
604
+ const url = (safeJson(res.text) as { url?: string } | undefined)?.url;
605
+ if (!url) throw new Error(`sprite ${args.id} has no url`);
606
+
607
+ if (args.path !== undefined) {
608
+ const timeoutMs = args.timeoutMs ?? 10_000;
609
+ const intervalMs = args.intervalMs ?? 500;
610
+ const target = `${url}${args.path}`;
611
+ const deadline = Date.now() + timeoutMs;
612
+ let lastErr = "";
613
+ for (;;) {
614
+ if (signal?.aborted) throw new Error(`sprite ${args.id} url wait aborted`);
615
+ try {
616
+ const check = await http("GET", target, undefined, undefined, signal);
617
+ const ok = args.status !== undefined ? check.status === args.status : check.status >= 200 && check.status < 300;
618
+ if (ok) {
619
+ console.log(`url: sprite/${args.id} answers on ${target} (${check.status})`);
620
+ return { url };
621
+ }
622
+ lastErr = `status ${check.status} (want ${args.status ?? "2xx"})`;
623
+ } catch (e) {
624
+ lastErr = e instanceof Error ? e.message : String(e);
625
+ }
626
+ if (Date.now() >= deadline) throw new Error(`sprite ${args.id} url ${target} did not answer: ${lastErr}`);
627
+ await sleep(intervalMs, signal);
628
+ }
629
+ }
630
+
631
+ return { url };
632
+ }
@@ -28,9 +28,18 @@
28
28
 
29
29
  import { activity, type NamedActivityStep, type WithStepRefs } from "@intentius/chant/op";
30
30
  import type { ActivityStep } from "@intentius/chant/op";
31
- import type { SpriteCreateArgs, SpriteExecArgs, SpriteCheckpointArgs, SpriteRestoreArgs, ListCheckpointsArgs, SpriteDestroyArgs } from "./activities/sprites";
31
+ import type { SpriteCreateArgs, SpriteExecArgs, SpriteCheckpointArgs, SpriteRestoreArgs, ListCheckpointsArgs, SpriteDestroyArgs, SpriteDeleteArgs, SpriteUrlArgs } from "./activities/sprites";
32
32
  import type { SpriteWriteFileArgs, SpriteReadFileArgs, SpriteListDirArgs, SpriteRemoveArgs } from "./activities/sprite-fs";
33
33
  import type { SpriteApplyNetworkPolicyArgs, SpriteApplyServicesArgs } from "./activities/sprite-config";
34
+ import type {
35
+ SpriteServiceCreateArgs,
36
+ SpriteServiceGetArgs,
37
+ SpriteServiceListArgs,
38
+ SpriteServiceStartArgs,
39
+ SpriteServiceStopArgs,
40
+ SpriteServiceDeleteArgs,
41
+ SpriteServiceLogsArgs,
42
+ } from "./activities/sprite-services";
34
43
  import type { SpriteTaskCreateArgs, SpriteTaskRefreshArgs, SpriteTaskReleaseArgs } from "./activities/sprite-tasks";
35
44
  import type { SpritesUpArgs, SpritesDownArgs } from "./activities/sprites-emulator";
36
45
 
@@ -75,6 +84,10 @@ export const spriteRestore = spriteStep<SpriteRestoreArgs>("spriteRestore", "lon
75
84
  export const listCheckpoints = spriteStep<ListCheckpointsArgs>("listCheckpoints", "fastIdempotent");
76
85
  /** Destroy a sprite — the fully typed twin of core's `spriteDestroy`. Defaults to the `fastIdempotent` profile. */
77
86
  export const spriteDestroy = spriteStep<SpriteDestroyArgs>("spriteDestroy", "fastIdempotent");
87
+ /** Delete a sprite (#2711; alias of `spriteDestroy`). Defaults to the `fastIdempotent` profile. */
88
+ export const spriteDelete = spriteStep<SpriteDeleteArgs>("spriteDelete", "fastIdempotent");
89
+ /** Resolve a sprite's URL, optionally waiting until a path on it answers (#2711). Defaults to the `fastIdempotent` profile. */
90
+ export const spriteUrl = spriteStep<SpriteUrlArgs>("spriteUrl", "fastIdempotent");
78
91
  /** Write a file into a sprite — the fully typed twin of core's `spriteWriteFile`. Defaults to the `fastIdempotent` profile. */
79
92
  export const spriteWriteFile = spriteStep<SpriteWriteFileArgs>("spriteWriteFile", "fastIdempotent");
80
93
  /** Read a file from a sprite — the fully typed twin of core's `spriteReadFile`. Defaults to the `fastIdempotent` profile. */
@@ -87,6 +100,20 @@ export const spriteRemove = spriteStep<SpriteRemoveArgs>("spriteRemove", "fastId
87
100
  export const spriteApplyNetworkPolicy = spriteStep<SpriteApplyNetworkPolicyArgs>("spriteApplyNetworkPolicy", "fastIdempotent");
88
101
  /** Reconcile a sprite's background services — the fully typed twin of core's `spriteApplyServices`. Defaults to the `fastIdempotent` profile. */
89
102
  export const spriteApplyServices = spriteStep<SpriteApplyServicesArgs>("spriteApplyServices", "fastIdempotent");
103
+ /** Create-and-start one background service (#2711) — the single-service primitive underneath `spriteApplyServices`. Defaults to the `longInfra` profile (the create+start NDJSON round trip). */
104
+ export const spriteServiceCreate = spriteStep<SpriteServiceCreateArgs>("spriteServiceCreate", "longInfra");
105
+ /** Get one background service's definition and live state (#2711). Defaults to the `fastIdempotent` profile. */
106
+ export const spriteServiceGet = spriteStep<SpriteServiceGetArgs>("spriteServiceGet", "fastIdempotent");
107
+ /** List a sprite's background services (#2711). Defaults to the `fastIdempotent` profile. */
108
+ export const spriteServiceList = spriteStep<SpriteServiceListArgs>("spriteServiceList", "fastIdempotent");
109
+ /** Start a stopped background service (#2711). Defaults to the `longInfra` profile (the NDJSON round trip). */
110
+ export const spriteServiceStart = spriteStep<SpriteServiceStartArgs>("spriteServiceStart", "longInfra");
111
+ /** Stop a running background service (#2711). Defaults to the `fastIdempotent` profile. */
112
+ export const spriteServiceStop = spriteStep<SpriteServiceStopArgs>("spriteServiceStop", "fastIdempotent");
113
+ /** Delete a background service, idempotent (#2711). Defaults to the `fastIdempotent` profile. */
114
+ export const spriteServiceDelete = spriteStep<SpriteServiceDeleteArgs>("spriteServiceDelete", "fastIdempotent");
115
+ /** Read a background service's log tail (#2711). Defaults to the `fastIdempotent` profile. */
116
+ export const spriteServiceLogs = spriteStep<SpriteServiceLogsArgs>("spriteServiceLogs", "fastIdempotent");
90
117
  /** Create a keep-alive task — the fully typed twin of core's `spriteTaskCreate`. Defaults to the `fastIdempotent` profile. */
91
118
  export const spriteTaskCreate = spriteStep<SpriteTaskCreateArgs>("spriteTaskCreate", "fastIdempotent");
92
119
  /** Refresh a keep-alive task's expiry — the fully typed twin of core's `spriteTaskRefresh`. Defaults to the `fastIdempotent` profile. */
@@ -78,7 +78,7 @@ When the `Run` phase's command exits non-zero, `spriteExec` throws, the phase fa
78
78
 
79
79
  ## Targeting the emulator or real Sprites
80
80
 
81
- The activities resolve their endpoint in this order: an explicit `endpoint` arg, then `SPRITES_BASE_URL`, then the real Sprites base. The same Op targets an emulator or real Sprites with no code change. The default `fetch` client adds `Authorization: Bearer ${SPRITES_API_TOKEN}` when a token is set; the emulator ignores it.
81
+ The activities resolve their endpoint in this order: an explicit `endpoint` arg, then `SPRITES_BASE_URL`, then its alias `SPRITES_API_URL` (the name the studio and wisp's own docs use), then the real Sprites base. The same Op targets an emulator or real Sprites with no code change. The default `fetch` client adds `Authorization: Bearer <token>` when a token is set — `SPRITES_API_TOKEN`, or its alias `SPRITE_TOKEN` — the emulator ignores it. Either alias is read only when the original name is unset, so an existing deployment using `SPRITES_BASE_URL`/`SPRITES_API_TOKEN` is unaffected.
82
82
 
83
83
  ```bash
84
84
  # Point at a self-hosted or in-process emulator.
@@ -105,9 +105,11 @@ The same lexicon ships more Sprite primitives, all imported from `@intentius/cha
105
105
  |--------|-----------|-----|
106
106
  | Filesystem (#848) | `spriteWriteFile` / `spriteReadFile` / `spriteListDir` / `spriteRemove` | stage an input file and read a result out without shelling `spriteExec` + `cat` |
107
107
  | Config reconcile (#849) | `spriteApplyNetworkPolicy` / `spriteApplyServices` | reconcile a Sprite's egress allowlist and background services against typed config (validated before any HTTP; a whole-object replace for policy, create-or-update by name for services) |
108
+ | Services (#2711) | `spriteServiceCreate` / `spriteServiceGet` / `spriteServiceList` / `spriteServiceStart` / `spriteServiceStop` / `spriteServiceDelete` / `spriteServiceLogs` | the single-service primitives underneath `spriteApplyServices` — create-and-start one long-lived service (a box's door, hud or chud), inspect or list what's running, stop/start or delete one by name, read its log tail |
109
+ | Sprite URL / delete (#2711) | `spriteUrl` / `spriteDelete` | resolve a Sprite's URL, optionally waiting until a path on it answers; delete the Sprite (`spriteDelete` is `spriteDestroy` under the name spritzer/wisp and the other lexicons' delete activities use — same call, either name) |
108
110
  | Keep-alive (#847) | `spriteTaskCreate` / `spriteTaskRefresh` / `spriteTaskRelease` | hold a Sprite active for a session so it will not pause; a session past the 1-hour task cap refreshes on an interval |
109
111
 
110
- These are still runtime-orchestration primitives, not declarable resources — a Sprite has no desired-state create body to reconcile.
112
+ These are still runtime-orchestration primitives, not declarable resources — a Sprite has no desired-state create body to reconcile. Services created directly with `spriteServiceCreate` are what a box's `spritzer` preset provisions door/hud/chud on (arugula-salad/studio#27) — the same wire surface as `sprite-env services` inside the sprite, spritzer 0.6.0's container mode, and wisp.
111
113
 
112
114
  ## Where it fits
113
115