@orchestraworks/cli 0.5.1 → 0.5.2

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/README.md CHANGED
@@ -20,7 +20,7 @@ The full guide to every construct, secrets, connections and troubleshooting is [
20
20
  ```bash
21
21
  npm install -g @orchestraworks/cli # the CLI, on your path as `orchestra`
22
22
  npm install @orchestraworks/cli # the current release
23
- npm install @orchestraworks/cli@0.5.1 # this exact release
23
+ npm install @orchestraworks/cli@0.5.2 # this exact release
24
24
  ```
25
25
 
26
26
  Then `import { Factory, Agent, … } from "@orchestraworks/cli"` normally — the package `exports` entry resolves
@@ -2421,7 +2421,7 @@
2421
2421
  "required": true,
2422
2422
  "repeatable": false,
2423
2423
  "credential": false,
2424
- "description": "The type of the principal the key signs in. A key of a type that cannot sign in yet is refused 403; today a service account's key alone signs in",
2424
+ "description": "The type of the principal the key signs in. A key of a type that cannot sign in is refused 403; a person's key and a service account's key sign in, and an agent's does not",
2425
2425
  "enumValues": [
2426
2426
  "USER",
2427
2427
  "SERVICE_ACCOUNT",
@@ -2453,6 +2453,19 @@
2453
2453
  "repeatable": false,
2454
2454
  "credential": false,
2455
2455
  "description": "Human-readable key name"
2456
+ },
2457
+ {
2458
+ "name": "expires-at",
2459
+ "names": [
2460
+ "expires-at"
2461
+ ],
2462
+ "source": "body",
2463
+ "field": "expiresAt",
2464
+ "type": "string",
2465
+ "required": false,
2466
+ "repeatable": false,
2467
+ "credential": false,
2468
+ "description": "When the key stops signing its principal in, after the request's time and fixed at create; absent means the key never expires"
2456
2469
  }
2457
2470
  ],
2458
2471
  "emptyDefaults": [],
@@ -2478,6 +2491,11 @@
2478
2491
  "schema": {
2479
2492
  "kind": "scalar"
2480
2493
  }
2494
+ },
2495
+ "expiresAt": {
2496
+ "schema": {
2497
+ "kind": "scalar"
2498
+ }
2481
2499
  }
2482
2500
  }
2483
2501
  },
@@ -2513,6 +2531,11 @@
2513
2531
  "kind": "scalar"
2514
2532
  }
2515
2533
  },
2534
+ "expiresAt": {
2535
+ "schema": {
2536
+ "kind": "scalar"
2537
+ }
2538
+ },
2516
2539
  "createdAt": {
2517
2540
  "schema": {
2518
2541
  "kind": "scalar"
@@ -2614,6 +2637,11 @@
2614
2637
  "kind": "scalar"
2615
2638
  }
2616
2639
  },
2640
+ "expiresAt": {
2641
+ "schema": {
2642
+ "kind": "scalar"
2643
+ }
2644
+ },
2617
2645
  "createdAt": {
2618
2646
  "schema": {
2619
2647
  "kind": "scalar"
@@ -2746,6 +2774,11 @@
2746
2774
  "kind": "scalar"
2747
2775
  }
2748
2776
  },
2777
+ "expiresAt": {
2778
+ "schema": {
2779
+ "kind": "scalar"
2780
+ }
2781
+ },
2749
2782
  "createdAt": {
2750
2783
  "schema": {
2751
2784
  "kind": "scalar"
@@ -45,6 +45,12 @@ export interface ApiKey {
45
45
  * @memberof ApiKey
46
46
  */
47
47
  lastUsedAt?: Date;
48
+ /**
49
+ * When the key stops signing its principal in, fixed at create; absent when it never expires
50
+ * @type {Date}
51
+ * @memberof ApiKey
52
+ */
53
+ expiresAt?: Date;
48
54
  /**
49
55
  * Creation timestamp
50
56
  * @type {Date}
@@ -50,6 +50,7 @@ export function ApiKeyFromJSONTyped(json, ignoreDiscriminator) {
50
50
  'principalId': json['principalId'],
51
51
  'name': json['name'],
52
52
  'lastUsedAt': json['lastUsedAt'] == null ? undefined : (new Date(json['lastUsedAt'])),
53
+ 'expiresAt': json['expiresAt'] == null ? undefined : (new Date(json['expiresAt'])),
53
54
  'createdAt': (new Date(json['createdAt'])),
54
55
  'updatedAt': (new Date(json['updatedAt'])),
55
56
  };
@@ -67,6 +68,7 @@ export function ApiKeyToJSONTyped(value, ignoreDiscriminator = false) {
67
68
  'principalId': value['principalId'],
68
69
  'name': value['name'],
69
70
  'lastUsedAt': value['lastUsedAt'] == null ? value['lastUsedAt'] : value['lastUsedAt'].toISOString(),
71
+ 'expiresAt': value['expiresAt'] == null ? value['expiresAt'] : value['expiresAt'].toISOString(),
70
72
  'createdAt': value['createdAt'].toISOString(),
71
73
  'updatedAt': value['updatedAt'].toISOString(),
72
74
  };
@@ -16,7 +16,7 @@
16
16
  */
17
17
  export interface CreateApiKeyRequest {
18
18
  /**
19
- * The type of the principal the key signs in. A key of a type that cannot sign in yet is refused 403; today a service account's key alone signs in
19
+ * The type of the principal the key signs in. A key of a type that cannot sign in is refused 403; a person's key and a service account's key sign in, and an agent's does not
20
20
  * @type {CreateApiKeyRequestPrincipalTypeEnum}
21
21
  * @memberof CreateApiKeyRequest
22
22
  */
@@ -33,6 +33,12 @@ export interface CreateApiKeyRequest {
33
33
  * @memberof CreateApiKeyRequest
34
34
  */
35
35
  name: string;
36
+ /**
37
+ * When the key stops signing its principal in, after the request's time and fixed at create; absent means the key never expires
38
+ * @type {Date}
39
+ * @memberof CreateApiKeyRequest
40
+ */
41
+ expiresAt?: Date;
36
42
  }
37
43
  /**
38
44
  * @export
@@ -42,6 +42,7 @@ export function CreateApiKeyRequestFromJSONTyped(json, ignoreDiscriminator) {
42
42
  'principalType': json['principalType'],
43
43
  'principalId': json['principalId'],
44
44
  'name': json['name'],
45
+ 'expiresAt': json['expiresAt'] == null ? undefined : (new Date(json['expiresAt'])),
45
46
  };
46
47
  }
47
48
  export function CreateApiKeyRequestToJSON(json) {
@@ -55,5 +56,6 @@ export function CreateApiKeyRequestToJSONTyped(value, ignoreDiscriminator = fals
55
56
  'principalType': value['principalType'],
56
57
  'principalId': value['principalId'],
57
58
  'name': value['name'],
59
+ 'expiresAt': value['expiresAt'] == null ? value['expiresAt'] : value['expiresAt'].toISOString(),
58
60
  };
59
61
  }
@@ -45,6 +45,12 @@ export interface ApiKey {
45
45
  * @memberof ApiKey
46
46
  */
47
47
  lastUsedAt?: Date;
48
+ /**
49
+ * When the key stops signing its principal in, fixed at create; absent when it never expires
50
+ * @type {Date}
51
+ * @memberof ApiKey
52
+ */
53
+ expiresAt?: Date;
48
54
  /**
49
55
  * Creation timestamp
50
56
  * @type {Date}
@@ -58,6 +58,7 @@ function ApiKeyFromJSONTyped(json, ignoreDiscriminator) {
58
58
  'principalId': json['principalId'],
59
59
  'name': json['name'],
60
60
  'lastUsedAt': json['lastUsedAt'] == null ? undefined : (new Date(json['lastUsedAt'])),
61
+ 'expiresAt': json['expiresAt'] == null ? undefined : (new Date(json['expiresAt'])),
61
62
  'createdAt': (new Date(json['createdAt'])),
62
63
  'updatedAt': (new Date(json['updatedAt'])),
63
64
  };
@@ -75,6 +76,7 @@ function ApiKeyToJSONTyped(value, ignoreDiscriminator = false) {
75
76
  'principalId': value['principalId'],
76
77
  'name': value['name'],
77
78
  'lastUsedAt': value['lastUsedAt'] == null ? value['lastUsedAt'] : value['lastUsedAt'].toISOString(),
79
+ 'expiresAt': value['expiresAt'] == null ? value['expiresAt'] : value['expiresAt'].toISOString(),
78
80
  'createdAt': value['createdAt'].toISOString(),
79
81
  'updatedAt': value['updatedAt'].toISOString(),
80
82
  };
@@ -16,7 +16,7 @@
16
16
  */
17
17
  export interface CreateApiKeyRequest {
18
18
  /**
19
- * The type of the principal the key signs in. A key of a type that cannot sign in yet is refused 403; today a service account's key alone signs in
19
+ * The type of the principal the key signs in. A key of a type that cannot sign in is refused 403; a person's key and a service account's key sign in, and an agent's does not
20
20
  * @type {CreateApiKeyRequestPrincipalTypeEnum}
21
21
  * @memberof CreateApiKeyRequest
22
22
  */
@@ -33,6 +33,12 @@ export interface CreateApiKeyRequest {
33
33
  * @memberof CreateApiKeyRequest
34
34
  */
35
35
  name: string;
36
+ /**
37
+ * When the key stops signing its principal in, after the request's time and fixed at create; absent means the key never expires
38
+ * @type {Date}
39
+ * @memberof CreateApiKeyRequest
40
+ */
41
+ expiresAt?: Date;
36
42
  }
37
43
  /**
38
44
  * @export
@@ -50,6 +50,7 @@ function CreateApiKeyRequestFromJSONTyped(json, ignoreDiscriminator) {
50
50
  'principalType': json['principalType'],
51
51
  'principalId': json['principalId'],
52
52
  'name': json['name'],
53
+ 'expiresAt': json['expiresAt'] == null ? undefined : (new Date(json['expiresAt'])),
53
54
  };
54
55
  }
55
56
  function CreateApiKeyRequestToJSON(json) {
@@ -63,5 +64,6 @@ function CreateApiKeyRequestToJSONTyped(value, ignoreDiscriminator = false) {
63
64
  'principalType': value['principalType'],
64
65
  'principalId': value['principalId'],
65
66
  'name': value['name'],
67
+ 'expiresAt': value['expiresAt'] == null ? value['expiresAt'] : value['expiresAt'].toISOString(),
66
68
  };
67
69
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestraworks/client",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Orchestra public API TypeScript client",
5
5
  "author": "Living Computers",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestraworks/cli",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "description": "The Orchestra command line.",
6
6
  "license": "Apache-2.0",
@@ -109,13 +109,13 @@
109
109
  },
110
110
  "dependencies": {
111
111
  "@agentclientprotocol/sdk": "1.3.0",
112
- "@orchestraworks/client": "0.5.1",
113
- "@orchestraworks/worker": "0.0.136",
112
+ "@orchestraworks/client": "0.5.2",
113
+ "@orchestraworks/worker": "0.0.137",
114
114
  "commander": "^14.0.0",
115
115
  "liquidjs": "^10.28.0"
116
116
  },
117
117
  "optionalDependencies": {
118
- "@orchestraworks/local": "0.0.136"
118
+ "@orchestraworks/local": "0.0.137"
119
119
  },
120
120
  "bundleDependencies": [
121
121
  "@orchestraworks/client"
@@ -138,7 +138,7 @@ ports, settings and network rules. A *machine* is one live computer made from it
138
138
  machine carries one agent.
139
139
 
140
140
  - *Size.* CPU and memory, from a fixed ladder: 0.5, 1, 2, 4, 8, 16 or 32 CPU, with 2 GB of memory per CPU.
141
- - *Image.* The disk the machine starts from. The stock image is Debian with Node 22, Java 21 and 25, git,
141
+ - *Image.* The disk the machine starts from. The stock image is Debian with Node 24, Java 21 and 25, git,
142
142
  Docker with Compose, `gh` and `acli`, and all four harnesses.
143
143
  - *Setup script.* A script that runs once, as root, on each new machine, before the agent arrives and before
144
144
  any code is cloned. Bash on Linux, PowerShell on Windows. At most 64 KiB, stopped after 10 minutes. A failed
@@ -516,7 +516,7 @@ const claudeSignIn = new Secret(factory, "claude-sign-in", {
516
516
  });
517
517
 
518
518
  // …in the Environment's props:
519
- env: { CLAUDE_CODE_OAUTH_TOKEN: "placeholder", ANTHROPIC_API_KEY: "" },
519
+ env: { CLAUDE_CODE_OAUTH_TOKEN: "software-factory-egress-placeholder", ANTHROPIC_API_KEY: "" },
520
520
  egress: EgressPolicy.allowAll()
521
521
  .allow("claude-sign-in", { hosts: ["api.anthropic.com"], priority: 300, credential: claudeSignIn }),
522
522
  ```
@@ -604,6 +604,106 @@ which the harness would otherwise send instead.
604
604
  (the recipe supplies the headers), or use `bearer(header, secret)` / `basic(...)` / `header(...)`
605
605
  for one-header bindings, or `inject(secret, recipe)` for a binding-owned recipe.
606
606
 
607
+ ## How the agents reach a model
608
+
609
+ Ask the person which of three ways they want, and walk them through its steps once.
610
+
611
+ **1. Subscription: their own Claude or Codex sign-in.** The person makes the sign-in in a real terminal
612
+ on their own computer; run from your shell, its browser step hangs. Never read or copy a value yourself.
613
+
614
+ - **Claude: a long-lived token in a Secret, the recommended way.** `claude setup-token` prints an
615
+ `sk-ant-oat…` token made for servers. It lasts about a year, and nothing on a machine renews or revokes
616
+ it, so it needs no shared `/home`. The person copies it and saves it to a file only they can read,
617
+ without pasting it into a chat: `umask 077 && pbpaste | tr -d '\n' > <file> && pbcopy </dev/null`.
618
+ At apply, the shell exports it from that file into the variable the Secret reads. Declare the Claude
619
+ sign-in recipe (§ Secrets are recipes, never values): a `Secret`, an egress rule that allows
620
+ `api.anthropic.com` and sets `Authorization: Bearer ${secret}` with `x-api-key` removed, and, in the
621
+ environment's `env`, `CLAUDE_CODE_OAUTH_TOKEN: "software-factory-egress-placeholder"` and an empty
622
+ `ANTHROPIC_API_KEY`. Turn `enableModelGateway` off for the agent. The machine holds only the
623
+ placeholder; the edge adds the token. Copying the renewing credentials file instead is the fallback,
624
+ and follows the ChatGPT sign-in steps below.
625
+ - **Codex: a personal access token, where the workspace allows one.** A ChatGPT Business or Enterprise
626
+ member creates it at chatgpt.com/admin/access-tokens, once the workspace owner allows access tokens,
627
+ and chooses how long it lasts, such as 7 to 90 days. Codex never renews it, so it needs no shared
628
+ `/home`; the person supplies a new one before it expires. It begins `at-` and goes in
629
+ `ORCHESTRA_WORKER_CODEX_PERSONAL_ACCESS_TOKEN`, which outranks a ChatGPT sign-in or an API key beside
630
+ it. On their own computer, `codex login --with-access-token` reads the same token from standard input.
631
+ - **Codex otherwise: a separate ChatGPT sign-in on a shared `/home`.** The other long-lived choice is an
632
+ OpenAI API key, billed by use rather than by the subscription (option 3).
633
+ 1. **Make a separate sign-in, never a copy of the local one:** `mkdir -p <dir> && CODEX_HOME=<dir>
634
+ codex login`, with `--device-auth` only on a machine with no browser. Its tokens in
635
+ `<dir>/auth.json` go in the `ORCHESTRA_WORKER_CODEX_` variables below, or a setup step writes the
636
+ file.
637
+ 2. **Put it on a shared `/home`.** Give the environment a durable `/home` volume, shared by every
638
+ machine that uses the sign-in, so one file renews for all of them. Separate copies fight: each
639
+ renewal replaces the refresh token, so the first copy to renew signs out the others. Two machines
640
+ that renew in the same second can still sign each other out; then make a new separate sign-in.
641
+ Remove the variables after the first machine starts, or each new machine writes the old token back.
642
+ 3. **Never sign in again in the default folder after the copy.** A new `codex login` in `~/.codex`
643
+ revokes the cloud copy, and every machine then answers "refresh token was revoked". If it
644
+ happened, make a new separate sign-in.
645
+ - **Keep a Codex sign-in off an agent that uses the model gateway.** With a ChatGPT sign-in or a personal
646
+ access token, Codex compresses its requests, and the gateway refuses them with `model_not_found`, a
647
+ known issue. Give such an agent the subscription directly, with `enableModelGateway` off, or keep the
648
+ homes apart.
649
+
650
+ **2. Model gateway: Orchestra provides the key.** One endpoint reaches OpenAI, Anthropic and open-source
651
+ models, on the organization's credits. The person can also bring their own provider key to the gateway:
652
+ `orchestra fdk init --model-key anthropic` (or `openai`) declares a `ModelProviderKey`, which the gateway uses for
653
+ that provider's calls. The gateway also routes models: `orchestra model-configs` sets which providers serve a
654
+ model and in what order, so a call falls back to the next one when a provider is down, and
655
+ `orchestra model-aliases` gives a model a name of the organization's own, such as `default`, that one change
656
+ points elsewhere.
657
+
658
+ **3. Own API key via the vault.** Their Anthropic or OpenAI API key is a platform `Secret`, added at the
659
+ network edge on calls straight to the provider, with no gateway in between, and nothing to sync, because
660
+ an API key does not renew. Declare it as the Claude sign-in recipe (§ Secrets are recipes) does, with the provider's own
661
+ header, and leave `enableModelGateway` off in the agent's model settings:
662
+
663
+ ```ts
664
+ const anthropicKey = new Secret(factory, "anthropic-api-key", {
665
+ value: SecretValue.fromEnv("ORCHESTRA_FDK_ANTHROPIC_API_KEY"),
666
+ injection: { headers: [{ name: "x-api-key", value: "${secret}" }] },
667
+ });
668
+ // …in the Environment's props:
669
+ egress: EgressPolicy.allowAll()
670
+ .allow("anthropic", { hosts: ["api.anthropic.com"], priority: 300, credential: anthropicKey }),
671
+ ```
672
+
673
+ For OpenAI, inject `authorization: Bearer ${secret}` on `api.openai.com`. Recommend this over a
674
+ subscription when the person has an API key: there is no file to copy and no shared `/home`.
675
+
676
+ ### The subscription variables
677
+
678
+ The worker writes each value into the file the harness reads, once per machine start, and hands none of
679
+ these variables to the harness. pi can also run on the provider you name.
680
+
681
+ | Variable | claude | codex | pi |
682
+ |---|---|---|---|
683
+ | `ORCHESTRA_WORKER_CLAUDE_OAUTH_TOKEN`: a `claude setup-token` token as a worker setting, in plain text; the Claude sign-in recipe's Secret is the recommended way | yes | — | yes |
684
+ | `ORCHESTRA_WORKER_CLAUDE_ACCESS_TOKEN` and `ORCHESTRA_WORKER_CLAUDE_REFRESH_TOKEN`, plus `ORCHESTRA_WORKER_CLAUDE_EXPIRES_AT` in milliseconds if you have it: a renewing Claude sign-in | yes | — | yes |
685
+ | `ORCHESTRA_WORKER_OPENAI_API_KEY` | — | yes | yes |
686
+ | `ORCHESTRA_WORKER_CODEX_PERSONAL_ACCESS_TOKEN`: a Codex personal access token, which outranks the other Codex rows | — | yes | — |
687
+ | `ORCHESTRA_WORKER_CODEX_ID_TOKEN`, `ORCHESTRA_WORKER_CODEX_ACCESS_TOKEN`, `ORCHESTRA_WORKER_CODEX_REFRESH_TOKEN` and `ORCHESTRA_WORKER_CODEX_ACCOUNT_ID` (`tokens.account_id`, which Codex needs to renew): a ChatGPT sign-in | — | yes | — |
688
+ | `ORCHESTRA_WORKER_PI_PROVIDER`: pi's `defaultProvider`, such as `anthropic` or `openai` | — | — | yes |
689
+
690
+ You can also bake the harness's own sign-in or settings file into the image with the setup script. With
691
+ no variable set, the worker leaves a value already in that file alone. For pi, your own
692
+ `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_OAUTH_TOKEN`, `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` in `env` still works: the worker then leaves that provider's
693
+ placeholder out of pi's sign-in file, which pi would otherwise read first, and holds back the image's
694
+ `ANTHROPIC_OAUTH_TOKEN` placeholder beside your own `ANTHROPIC_API_KEY`.
695
+
696
+ - **The value is plain text.** `env` is stored unencrypted, and an environment read returns it. Anyone
697
+ who can read the environment can read its setup script.
698
+ - **Allow the sign-in hosts with no credential bound**: `platform.claude.com` for a copied Claude
699
+ credentials file, `api.openai.com` for an OpenAI key, `auth.openai.com` and `chatgpt.com` for ChatGPT. A
700
+ rule that binds a credential on one of these hosts replaces your sign-in on every call.
701
+ - **Name pi's provider when the model is not a Claude one.** pi runs the agent's model under its
702
+ `defaultProvider`, which `ORCHESTRA_WORKER_PI_PROVIDER` sets; with no variable, pi keeps the image's
703
+ `anthropic` or the provider you configured. The platform never picks it from the model.
704
+ - **On a Mac outpost, Claude Code reads the login Keychain first.** Sign out of Claude Code there, or
705
+ use `ORCHESTRA_WORKER_CLAUDE_OAUTH_TOKEN`, which Claude Code reads before the Keychain.
706
+
607
707
  ## Egress: apply the bundles, hand-write the rest
608
708
 
609
709
  An environment's `egress` is a policy you build up from one of **two starting points**:
@@ -454,6 +454,61 @@ bundle. Keep the safety rule:
454
454
  When a step fails, fix it here, prove the fix locally if a local mode runs, apply again, and assign the
455
455
  item again.
456
456
 
457
+ ### The model in the cloud: subscription, model gateway, or own key via the vault
458
+
459
+ The cloud agents reach a model in one of three ways. Ask the developer which one, and walk them through
460
+ its steps once. They copy every value themselves; you never read, print or copy it.
461
+
462
+ 1. **Subscription: their own Claude or Codex sign-in.** Follow the sign-in steps below.
463
+ 2. **Model gateway: Orchestra provides the key.** One endpoint reaches OpenAI, Anthropic and open-source
464
+ models, on the organization's credits. They can also bring their own provider key to the gateway, with
465
+ `orchestra fdk init --model-key anthropic` or `openai`. The gateway also routes models:
466
+ `orchestra model-configs` sets which providers serve a model and in what order, so a call falls back to
467
+ the next one when a provider is down, and `orchestra model-aliases` gives a model a name of their own, such as `default`.
468
+ 3. **Own API key via the vault.** Their Anthropic or OpenAI API key is a platform Secret, added at the
469
+ network edge on calls straight to `api.anthropic.com` or `api.openai.com`, with no gateway in between
470
+ and with the agent's gateway setting off. Nothing to sync, because an API key does not renew. When they
471
+ have an API key, recommend this over a subscription: no file to copy and no shared `/home`.
472
+ `references/authoring.md` § How the agents reach a model shows the declaration.
473
+
474
+ The sign-in steps. The developer makes the sign-in in a real terminal on their computer; run from an
475
+ agent's shell, its browser step hangs.
476
+
477
+ - **Claude: a long-lived token in a Secret, the recommended way.** `claude setup-token` prints an
478
+ `sk-ant-oat…` token made for servers. It lasts about a year, and nothing on a machine renews or revokes
479
+ it, so it needs no shared `/home`. Save it to a file only the developer can read, never through a chat:
480
+ copy it, then `umask 077 && pbpaste | tr -d '\n' > <file> && pbcopy </dev/null`. At apply, the shell
481
+ exports it from that file into the variable a `Secret` reads. An egress rule allows `api.anthropic.com`
482
+ and adds `Authorization: Bearer ${secret}`, removing `x-api-key`. The environment's `env` holds only
483
+ `CLAUDE_CODE_OAUTH_TOKEN=software-factory-egress-placeholder` and an empty `ANTHROPIC_API_KEY`, and the
484
+ agent's gateway setting is off. Never put the token itself in `env`. The Claude sign-in recipe in
485
+ `references/authoring.md` shows the declaration. Copying the renewing credentials file is the fallback,
486
+ and follows the ChatGPT sign-in steps below.
487
+ - **Codex: a personal access token, where the workspace allows one.** A ChatGPT Business or Enterprise
488
+ member creates it at chatgpt.com/admin/access-tokens, once the workspace owner allows access tokens,
489
+ and chooses how long it lasts, such as 7 to 90 days. Codex never renews it, so it needs no shared
490
+ `/home`; the developer supplies a new one before it expires. It begins `at-` and goes in
491
+ `ORCHESTRA_WORKER_CODEX_PERSONAL_ACCESS_TOKEN`, which outranks a ChatGPT sign-in or an API key beside
492
+ it. On their own computer, `codex login --with-access-token` reads the same token from standard input.
493
+ - **Codex otherwise: a separate ChatGPT sign-in on a shared `/home`.** The other long-lived choice is an
494
+ OpenAI API key, billed by use rather than by the subscription (option 3).
495
+ 1. **Make a separate sign-in, never a copy of the local one:** `mkdir -p <dir> && CODEX_HOME=<dir> codex
496
+ login` (`--device-auth` only with no browser). Its tokens in `<dir>/auth.json` go in the
497
+ `ORCHESTRA_WORKER_CODEX_` variables, or a setup step writes the file.
498
+ 2. **Give the environment a durable `/home` volume,** shared by every machine that uses the sign-in, so
499
+ one file renews for all of them. Each renewal replaces the refresh token, so separate copies sign
500
+ each other out; two machines that renew in the same second can too, and then need a new sign-in.
501
+ Remove the variables after the first machine starts.
502
+ 3. **Never sign in again in the default folder after the copy.** A new `codex login` in `~/.codex`
503
+ revokes the cloud copy, and every machine answers "refresh token was revoked". If it happened, make
504
+ a new separate sign-in.
505
+ - **Keep a Codex sign-in off an agent that uses the model gateway.** With a ChatGPT sign-in or a personal
506
+ access token, Codex compresses its requests, and the gateway refuses them with `model_not_found`, a
507
+ known issue. Give such an agent the subscription directly, with its gateway setting off, or keep the
508
+ homes apart.
509
+
510
+ The sign-in values are plain text: the environment's settings are stored unencrypted and shown on every read.
511
+
457
512
  ## What the platform can do
458
513
 
459
514
  Use the orchestra skill to build an answer for a case the paths do not cover. Never promise one of its "Cannot
@@ -45,11 +45,11 @@ chown agent:agent /work || exit 16
45
45
 
46
46
  # ---- Add the team's system setup here: packages, language runtimes, tools. -----------------------------------
47
47
 
48
- # Optional, off by default: a Node major version other than the image's. To turn it on, remove the leading `# `
49
- # from the lines from NODE_MAJOR to `esac`, and set NODE_MAJOR. It replaces the image's Node in /usr/local. The
50
- # image's own npm and corepack go first: unpacking a second Node over them leaves a mix of two npm versions, and
51
- # every npm command then exits 1. Use this Node's own corepack for pnpm or yarn.
52
- # NODE_MAJOR=24
48
+ # Optional, off by default: a Node major version other than the image's Node 24. To turn it on, remove the
49
+ # leading `# ` from the lines from NODE_MAJOR to `esac`, and set NODE_MAJOR. It replaces the image's Node in
50
+ # /usr/local. The image's own npm and corepack go first: unpacking a second Node over them leaves a mix of two npm
51
+ # versions, and every npm command then exits 1. Use this Node's own corepack for pnpm or yarn.
52
+ # NODE_MAJOR=22
53
53
  # case "$(node --version 2>/dev/null)" in "v$NODE_MAJOR".*) ;; *)
54
54
  # case "$(uname -m)" in aarch64 | arm64) node_arch=arm64 ;; *) node_arch=x64 ;; esac
55
55
  # node_version=$(curl -fsSL --retry 3 https://nodejs.org/dist/index.json |