@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 +1 -1
- package/dist/generated/cli-commands.json +34 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/ApiKey.d.ts +6 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/ApiKey.js +2 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateApiKeyRequest.d.ts +7 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateApiKeyRequest.js +2 -0
- package/node_modules/@orchestraworks/client/dist/models/ApiKey.d.ts +6 -0
- package/node_modules/@orchestraworks/client/dist/models/ApiKey.js +2 -0
- package/node_modules/@orchestraworks/client/dist/models/CreateApiKeyRequest.d.ts +7 -1
- package/node_modules/@orchestraworks/client/dist/models/CreateApiKeyRequest.js +2 -0
- package/node_modules/@orchestraworks/client/package.json +1 -1
- package/package.json +4 -4
- package/skills/orchestra/SKILL.md +1 -1
- package/skills/orchestra-fdk/references/authoring.md +101 -1
- package/skills/orchestra-fdk/references/environment.md +55 -0
- package/templates/engineering-team/lib/setup.sh +5 -5
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orchestraworks/cli",
|
|
3
|
-
"version": "0.5.
|
|
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.
|
|
113
|
-
"@orchestraworks/worker": "0.0.
|
|
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.
|
|
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
|
|
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
|
|
49
|
-
# from the lines from NODE_MAJOR to `esac`, and set NODE_MAJOR. It replaces the image's Node in
|
|
50
|
-
# image's own npm and corepack go first: unpacking a second Node over them leaves a mix of two npm
|
|
51
|
-
# every npm command then exits 1. Use this Node's own corepack for pnpm or yarn.
|
|
52
|
-
# NODE_MAJOR=
|
|
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 |
|