@orchestraworks/cli 0.5.7 → 0.6.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 (94) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/api/approval.js +3 -3
  3. package/dist/cli/commands/outpost/local.js +3 -3
  4. package/dist/cli/commands/outpost/machine-names.d.ts +17 -0
  5. package/dist/cli/commands/outpost/machine-names.js +22 -0
  6. package/dist/cli/commands/outpost/up.js +7 -5
  7. package/dist/cli/commands/worker/enroll.d.ts +4 -3
  8. package/dist/cli/commands/worker/enroll.js +4 -3
  9. package/dist/cli/core/auth.d.ts +22 -2
  10. package/dist/cli/core/auth.js +69 -8
  11. package/dist/cli/core/errors.js +4 -0
  12. package/dist/cli/core/platform-client.d.ts +3 -2
  13. package/dist/cli/core/session-body.d.ts +1 -1
  14. package/dist/cli/core/session-body.js +1 -1
  15. package/dist/cli/firstRun.d.ts +3 -3
  16. package/dist/cli/firstRun.js +7 -5
  17. package/dist/cli/main.js +4 -3
  18. package/dist/constructs/environment.d.ts +24 -11
  19. package/dist/constructs/environment.js +76 -12
  20. package/dist/constructs/secret.d.ts +17 -7
  21. package/dist/constructs/secret.js +24 -16
  22. package/dist/core/labels.d.ts +32 -6
  23. package/dist/core/labels.js +70 -8
  24. package/dist/engine/diff.js +38 -11
  25. package/dist/engine/gateway-key-reconcile.d.ts +1 -2
  26. package/dist/engine/gateway-key-reconcile.js +19 -15
  27. package/dist/engine/pipeline-reconcile.js +2 -2
  28. package/dist/engine/pipeline-workflow-reconcile.js +15 -9
  29. package/dist/engine/reconcile.js +48 -15
  30. package/dist/engine/refusals.d.ts +0 -5
  31. package/dist/engine/refusals.js +1 -5
  32. package/dist/engine/secrets-apply.d.ts +10 -14
  33. package/dist/engine/secrets-apply.js +12 -17
  34. package/dist/engine/workflow-reconcile.d.ts +1 -1
  35. package/dist/generated/cli-commands.json +170 -34
  36. package/dist/generated/providers.d.ts +40 -0
  37. package/dist/generated/providers.js +30 -0
  38. package/docs/REFERENCE.md +26 -12
  39. package/node_modules/@orchestraworks/client/dist/esm/models/ApiErrorDetail.d.ts +1 -1
  40. package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.d.ts +7 -0
  41. package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.js +3 -0
  42. package/node_modules/@orchestraworks/client/dist/esm/models/CreateSecretRequest.d.ts +4 -3
  43. package/node_modules/@orchestraworks/client/dist/esm/models/Environment.d.ts +7 -0
  44. package/node_modules/@orchestraworks/client/dist/esm/models/Environment.js +3 -0
  45. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.d.ts +38 -0
  46. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.js +45 -0
  47. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpec.d.ts +1 -1
  48. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
  49. package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentSnapshot.d.ts +1 -1
  50. package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.d.ts +6 -0
  51. package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.js +2 -0
  52. package/node_modules/@orchestraworks/client/dist/esm/models/OAuthTokenRequest.d.ts +1 -1
  53. package/node_modules/@orchestraworks/client/dist/esm/models/Secret.d.ts +1 -7
  54. package/node_modules/@orchestraworks/client/dist/esm/models/Secret.js +0 -4
  55. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.d.ts +7 -0
  56. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.js +3 -0
  57. package/node_modules/@orchestraworks/client/dist/esm/models/UpdateSecretRequest.d.ts +4 -3
  58. package/node_modules/@orchestraworks/client/dist/esm/models/index.d.ts +1 -0
  59. package/node_modules/@orchestraworks/client/dist/esm/models/index.js +1 -0
  60. package/node_modules/@orchestraworks/client/dist/models/ApiErrorDetail.d.ts +1 -1
  61. package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.d.ts +7 -0
  62. package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.js +3 -0
  63. package/node_modules/@orchestraworks/client/dist/models/CreateSecretRequest.d.ts +4 -3
  64. package/node_modules/@orchestraworks/client/dist/models/Environment.d.ts +7 -0
  65. package/node_modules/@orchestraworks/client/dist/models/Environment.js +3 -0
  66. package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.d.ts +38 -0
  67. package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.js +52 -0
  68. package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpec.d.ts +1 -1
  69. package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
  70. package/node_modules/@orchestraworks/client/dist/models/EnvironmentSnapshot.d.ts +1 -1
  71. package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.d.ts +6 -0
  72. package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.js +2 -0
  73. package/node_modules/@orchestraworks/client/dist/models/OAuthTokenRequest.d.ts +1 -1
  74. package/node_modules/@orchestraworks/client/dist/models/Secret.d.ts +1 -7
  75. package/node_modules/@orchestraworks/client/dist/models/Secret.js +0 -4
  76. package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.d.ts +7 -0
  77. package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.js +3 -0
  78. package/node_modules/@orchestraworks/client/dist/models/UpdateSecretRequest.d.ts +4 -3
  79. package/node_modules/@orchestraworks/client/dist/models/index.d.ts +1 -0
  80. package/node_modules/@orchestraworks/client/dist/models/index.js +1 -0
  81. package/node_modules/@orchestraworks/client/package.json +1 -1
  82. package/package.json +4 -4
  83. package/skills/orchestra/SKILL.md +38 -17
  84. package/skills/orchestra-fdk/references/authoring.md +34 -26
  85. package/skills/orchestra-fdk/references/environment.md +49 -12
  86. package/skills/orchestra-fdk/references/integrations/code-repo.md +26 -5
  87. package/skills/orchestra-fdk/references/phases/1-discover.md +1 -1
  88. package/skills/orchestra-fdk/references/phases/2-plan.md +1 -1
  89. package/skills/orchestra-fdk/references/troubleshooting.md +5 -3
  90. package/templates/bug-fix/main.ts +7 -4
  91. package/templates/engineering-team/lib/environment.ts +6 -3
  92. package/templates/engineering-team/package.json +1 -1
  93. package/templates/todo-factory/main.ts +7 -4
  94. package/templates/todo-factory/package.json +1 -1
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.7 # this exact release
23
+ npm install @orchestraworks/cli@0.6.0 # this exact release
24
24
  ```
25
25
 
26
26
  Then `import { Factory, Agent, … } from "@orchestraworks/cli"` normally — the package `exports` entry resolves
@@ -6,7 +6,7 @@
6
6
  // policy is resolved once per invocation at command-handler entry and handed in — this module reads no
7
7
  // environment variable and holds no default, so a gate can never disagree with the banner that described it.
8
8
  // An unattended invocation that WOULD prompt is a named refusal, never a silent proceed.
9
- import { MANAGED_BY_LABEL_KEY } from "../../core/labels.js";
9
+ import { MANAGED_BY_LABEL_KEY, managedByOf } from "../../core/labels.js";
10
10
  import { describeSource, preApprovalLevers, } from "../core/approval-policy.js";
11
11
  import { isAffirmative, NOTHING_WAS_WRITTEN, readConfirmLine } from "../core/prompts.js";
12
12
  /**
@@ -139,8 +139,8 @@ export function readManagedByMarker(body) {
139
139
  if (typeof labels !== "object" || labels === null || Array.isArray(labels)) {
140
140
  return undefined;
141
141
  }
142
- const marker = labels[MANAGED_BY_LABEL_KEY];
143
- return typeof marker === "string" && marker !== "" ? marker : undefined;
142
+ const marker = managedByOf(labels);
143
+ return marker !== "" ? marker : undefined;
144
144
  }
145
145
  /**
146
146
  * It states the CONSEQUENCE — the next `apply` reverts this — because that, not the marker, is what the
@@ -15,14 +15,13 @@ import { spawnSync } from "node:child_process";
15
15
  import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
16
  import { dirname, join } from "node:path";
17
17
  import { StackUnreachableError, authorityBytes, readCurrentRun, readProbe, readRun, ensureDir, localDir, readRunFacts, removeIfPresent, sameAddress, stackConfigDir, addressKey, } from "../local/launcher.js";
18
+ import { EGRESS_CA_URL, machineValue } from "./machine-names.js";
18
19
  /**
19
20
  * Where a container fetches this run's certificate authority when nothing names another path. It is the
20
21
  * agent account's own home in our runtime image, which the image contract fixes at `/home`; any other
21
22
  * image names its own with `--trust-file`.
22
23
  */
23
24
  export const DEFAULT_TRUST_FILE = "/home/.orchestra/local-authority.pem";
24
- /** The name the container's own set carries the plain authority endpoint on (`local/launch/bootstrap.go`). */
25
- const AUTHORITY_URL_NAME = "SFDK_EGRESS_CA_URL";
26
25
  export async function requireLocalEnvironment(call) {
27
26
  try {
28
27
  await trustThisRunsAuthority(call);
@@ -116,7 +115,8 @@ function authorityEndpoint(call, env) {
116
115
  // The folder is gone or unreadable, so the exported endpoint below is all there is.
117
116
  }
118
117
  }
119
- const exported = (env[AUTHORITY_URL_NAME] ?? "").trim();
118
+ // The container's own set carries the plain authority endpoint on this name (`local/launch/bootstrap.go`).
119
+ const exported = machineValue(env, EGRESS_CA_URL);
120
120
  return exported === "" ? undefined : { url: exported, trustFile: DEFAULT_TRUST_FILE };
121
121
  }
122
122
  /**
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A machine name under the spelling a worker reads first and the former one that a worker built before the
3
+ * rename reads alone. Both are written, and the new one is read first, until the compatibility rules of
4
+ * the rename end. Each pair is the one `fwp/schema/environment-v1.json` declares.
5
+ */
6
+ export interface MachineName {
7
+ readonly name: string;
8
+ readonly former: string;
9
+ }
10
+ export declare const EGRESS_CA_URL: MachineName;
11
+ export declare const ENROLLMENT_TOKEN: MachineName;
12
+ export declare const TRANSPORT_TRUST_ROOTS: MachineName;
13
+ export declare const TRANSPORT_TRUST_ROOTS_FILE: MachineName;
14
+ /** The trimmed value under the new name, or under the former one where the new one is unset or blank. */
15
+ export declare function machineValue(env: NodeJS.ProcessEnv, machineName: MachineName): string;
16
+ /** The value under both spellings. */
17
+ export declare function bothSpellings(machineName: MachineName, value: string): Record<string, string>;
@@ -0,0 +1,22 @@
1
+ export const EGRESS_CA_URL = { name: "ORCHESTRA_EGRESS_CA_URL", former: "SFDK_EGRESS_CA_URL" };
2
+ export const ENROLLMENT_TOKEN = {
3
+ name: "ORCHESTRA_WORKER_ENROLLMENT_TOKEN",
4
+ former: "SFDK_ENROLLMENT_TOKEN",
5
+ };
6
+ export const TRANSPORT_TRUST_ROOTS = {
7
+ name: "ORCHESTRA_WORKER_TRANSPORT_TRUST_ROOTS",
8
+ former: "SFDK_TRANSPORT_TRUST_ROOTS",
9
+ };
10
+ export const TRANSPORT_TRUST_ROOTS_FILE = {
11
+ name: "ORCHESTRA_WORKER_TRANSPORT_TRUST_ROOTS_FILE",
12
+ former: "SFDK_TRANSPORT_TRUST_ROOTS_FILE",
13
+ };
14
+ /** The trimmed value under the new name, or under the former one where the new one is unset or blank. */
15
+ export function machineValue(env, machineName) {
16
+ const value = (env[machineName.name] ?? "").trim();
17
+ return value !== "" ? value : (env[machineName.former] ?? "").trim();
18
+ }
19
+ /** The value under both spellings. */
20
+ export function bothSpellings(machineName, value) {
21
+ return { [machineName.name]: value, [machineName.former]: value };
22
+ }
@@ -25,6 +25,7 @@ import { runWorkerBinary } from "../worker/launcher.js";
25
25
  import { bootCheckOfEnrollment, evaluateChecks, latestSession, listEnrollments, parseBudgets, readAgent, readEnrollment, readEnvironment, renderChecks, } from "../local/checks.js";
26
26
  import { ensureDir, readRun, runLocal, stackConfigDir, } from "../local/launcher.js";
27
27
  import { DEFAULT_TRUST_FILE, composeBootstrap, mintEnrollment, placeAuthority, requireAgentId, requireEnrolledEnvironment, requireHarness, readOutpostRecord, requireLocalEnvironment, resolveRunFolder, writeOutpostRecord, } from "./local.js";
28
+ import { bothSpellings, EGRESS_CA_URL, ENROLLMENT_TOKEN, machineValue, TRANSPORT_TRUST_ROOTS, TRANSPORT_TRUST_ROOTS_FILE, } from "./machine-names.js";
28
29
  /**
29
30
  * The port every worker's token feed listens on (`worker/shim/feed.go`). One worker runs on a machine and
30
31
  * one shim reads from it, so the two agree on a number rather than on a variable; a second outpost here
@@ -262,14 +263,14 @@ function accountNumber(args) {
262
263
  * composed from is this container's own, which is where `docker run -e OPENAI_API_KEY` put it.
263
264
  */
264
265
  async function prepareInContainer(deps, apis, env, agent, harness, trustFile, namedRoot) {
265
- const offer = (env.SFDK_ENROLLMENT_TOKEN ?? "").trim();
266
+ const offer = machineValue(env, ENROLLMENT_TOKEN);
266
267
  if (offer === "") {
267
268
  throw new Error("this container carries no enrollment offer: run `orchestra outpost token --agent " +
268
269
  `${agent.id}\` on the machine serving the stack and export what it prints here.`);
269
270
  }
270
- const authority = (env.SFDK_EGRESS_CA_URL ?? "").trim();
271
+ const authority = machineValue(env, EGRESS_CA_URL);
271
272
  if (authority === "") {
272
- throw new Error("this container's environment names no certificate authority in SFDK_EGRESS_CA_URL, and it verifies " +
273
+ throw new Error(`this container's environment names no certificate authority in ${EGRESS_CA_URL.name}, and it verifies ` +
273
274
  "this run's certificate against one. Export what `orchestra outpost token` printed.");
274
275
  }
275
276
  await placeAuthority(authority, trustFile, deps.fetchApi);
@@ -278,10 +279,11 @@ async function prepareInContainer(deps, apis, env, agent, harness, trustFile, na
278
279
  throw new Error("no enrollment of this run matches the offer this container carries; mint another one.");
279
280
  }
280
281
  const composed = workerEnvironment(env, {
281
- SFDK_TRANSPORT_TRUST_ROOTS_FILE: trustFile,
282
+ ...bothSpellings(TRANSPORT_TRUST_ROOTS_FILE, trustFile),
282
283
  ...emptiedCarriers(harness, env),
283
284
  });
284
- delete composed.SFDK_TRANSPORT_TRUST_ROOTS;
285
+ delete composed[TRANSPORT_TRUST_ROOTS.name];
286
+ delete composed[TRANSPORT_TRUST_ROOTS.former];
285
287
  const home = (composed.HOME ?? "").trim();
286
288
  const credentials = composeCredentialFiles(agent, harness, env);
287
289
  if (credentials.length > 0 && home === "") {
@@ -3,8 +3,9 @@ export interface WorkerEnrollOptions {
3
3
  root?: string;
4
4
  }
5
5
  /**
6
- * There is deliberately no token flag. The worker reads the enrollment token from `SFDK_ENROLLMENT_TOKEN` or
7
- * the file `SFDK_ENROLLMENT_TOKEN_FILE` names, because a token on a command line is readable by every process
8
- * listing on the machine for as long as the process runs, and it lands in shell history on the way in.
6
+ * There is deliberately no token flag. The worker reads the enrollment token from
7
+ * `ORCHESTRA_WORKER_ENROLLMENT_TOKEN` or the file `ORCHESTRA_WORKER_ENROLLMENT_TOKEN_FILE` names, because a token
8
+ * on a command line is readable by every process listing on the machine for as long as the process runs, and it
9
+ * lands in shell history on the way in.
9
10
  */
10
11
  export declare function workerEnrollCommand(options?: WorkerEnrollOptions, deps?: WorkerCommandDeps): Promise<void>;
@@ -1,8 +1,9 @@
1
1
  import { handOffToWorker, optionalFlag } from "./launcher.js";
2
2
  /**
3
- * There is deliberately no token flag. The worker reads the enrollment token from `SFDK_ENROLLMENT_TOKEN` or
4
- * the file `SFDK_ENROLLMENT_TOKEN_FILE` names, because a token on a command line is readable by every process
5
- * listing on the machine for as long as the process runs, and it lands in shell history on the way in.
3
+ * There is deliberately no token flag. The worker reads the enrollment token from
4
+ * `ORCHESTRA_WORKER_ENROLLMENT_TOKEN` or the file `ORCHESTRA_WORKER_ENROLLMENT_TOKEN_FILE` names, because a token
5
+ * on a command line is readable by every process listing on the machine for as long as the process runs, and it
6
+ * lands in shell history on the way in.
6
7
  */
7
8
  export async function workerEnrollCommand(options = {}, deps = {}) {
8
9
  await handOffToWorker(["enroll", ...optionalFlag("--root", options.root)], deps);
@@ -14,19 +14,31 @@ export declare const AUTH_ENV: {
14
14
  /** Read after `baseUrl`: the platform writes this name into every machine. */
15
15
  readonly machineBaseUrl: "PLATFORM_BASE_URL";
16
16
  readonly token: "ORCHESTRA_API_TOKEN";
17
+ /** Set by the worker on each harness and hook process; the file holds a token it renews. */
18
+ readonly tokenFile: "ORCHESTRA_API_TOKEN_FILE";
17
19
  readonly organizationId: "ORCHESTRA_ORGANIZATION_ID";
18
20
  /** Presence is the declaration. */
19
- readonly edgeInjected: "SFDK_AUTH_EDGE_INJECTED";
21
+ readonly edgeInjected: "ORCHESTRA_AUTH_EDGE_INJECTED";
20
22
  /** Set by the worker on each harness process it starts. */
23
+ readonly sessionId: "ORCHESTRA_SESSION_ID";
24
+ };
25
+ /**
26
+ * The spellings a machine set before the rename, read when the name above is absent: a running worker and a
27
+ * pinned runtime image still set only these.
28
+ */
29
+ export declare const FORMER_MACHINE_ENV: {
30
+ readonly edgeInjected: "SFDK_AUTH_EDGE_INJECTED";
21
31
  readonly sessionId: "SFDK_SESSION_ID";
22
32
  };
33
+ /** Whether this machine declares edge authentication, under either spelling. */
34
+ export declare function edgeInjectedFrom(env: NodeJS.ProcessEnv): boolean;
23
35
  /** The base URL the environment names, and which of the two variables named it. */
24
36
  export declare function baseUrlFromEnv(env: NodeJS.ProcessEnv): {
25
37
  readonly value: string;
26
38
  readonly variable: string;
27
39
  } | undefined;
28
40
  /** Which tier of the chain supplied the authorization. */
29
- export type AuthProvider = "env" | "profile" | "edge";
41
+ export type AuthProvider = "env" | "file" | "profile" | "edge";
30
42
  /**
31
43
  * The conversation the command runs in, or undefined when the variable is absent, empty or whitespace only.
32
44
  * The only reader of the variable: auth resolution and `login` both call it.
@@ -36,6 +48,9 @@ export declare function sessionIdFrom(env: NodeJS.ProcessEnv): string | undefine
36
48
  * `session` rides along under the PROFILE provider only: `token` is the bearer as of this resolution, and
37
49
  * the session is what keeps a bearer valid over time. The env provider's token has nothing to refresh it
38
50
  * with and the edge provider holds no credential at all, so for both the field's absence is structural.
51
+ *
52
+ * Under the FILE provider the worker renews the token in place, so `token` reads the file again before each
53
+ * request, and the token names its own organization, so no organization is resolved.
39
54
  */
40
55
  export type AuthResolution = {
41
56
  readonly provider: "env" | "profile";
@@ -44,6 +59,11 @@ export type AuthResolution = {
44
59
  readonly organizationId?: string;
45
60
  readonly session?: CredentialSession;
46
61
  readonly sessionId?: string;
62
+ } | {
63
+ readonly provider: "file";
64
+ readonly token: () => string;
65
+ readonly baseUrl: string;
66
+ readonly sessionId?: string;
47
67
  } | {
48
68
  readonly provider: "edge";
49
69
  readonly baseUrl: string;
@@ -1,8 +1,9 @@
1
1
  // Every command resolves credentials through ONE ordered chain, with strict presence-is-selection
2
2
  // precedence: `ORCHESTRA_API_TOKEN` (one opaque slot holding ANY principal's bearer — the CLI never looks inside,
3
- // IAM judges server-side), then the `orchestra auth login` profile, then the sandbox flag the platform DECLARES and
4
- // never infers (the request goes out BARE and the egress stamps the per-request agent authorization, so no
5
- // credential material ever enters the sandbox), then a refusal.
3
+ // IAM judges server-side), then the agent token file the worker names in `ORCHESTRA_API_TOKEN_FILE` and renews
4
+ // while the agent is placed, then the `orchestra auth login` profile, then the sandbox flag the platform
5
+ // DECLARES and never infers (the request goes out BARE and the egress stamps the per-request agent
6
+ // authorization), then a refusal.
6
7
  //
7
8
  // The precedence is purely CLIENT-side. Inside a sandbox the egress stamp REPLACES whatever the request
8
9
  // carried, so an exported token is sent, then overwritten, and the call executes as the agent.
@@ -13,6 +14,7 @@
13
14
  // Resolution is ASYNC because the profile tier renews itself. The renewal lives in
14
15
  // `core/credentialSession.ts`: this module decides WHICH credential a command uses, and the session owns
15
16
  // what happens to it over time.
17
+ import { readFileSync } from "node:fs";
16
18
  import { CredentialSession } from "./credentialSession.js";
17
19
  import { DEFAULT_PROFILE_NAME, loadProfile as loadProfileFromDisk, } from "./profile.js";
18
20
  /**
@@ -28,12 +30,26 @@ export const AUTH_ENV = {
28
30
  /** Read after `baseUrl`: the platform writes this name into every machine. */
29
31
  machineBaseUrl: "PLATFORM_BASE_URL",
30
32
  token: "ORCHESTRA_API_TOKEN",
33
+ /** Set by the worker on each harness and hook process; the file holds a token it renews. */
34
+ tokenFile: "ORCHESTRA_API_TOKEN_FILE",
31
35
  organizationId: "ORCHESTRA_ORGANIZATION_ID",
32
36
  /** Presence is the declaration. */
33
- edgeInjected: "SFDK_AUTH_EDGE_INJECTED",
37
+ edgeInjected: "ORCHESTRA_AUTH_EDGE_INJECTED",
34
38
  /** Set by the worker on each harness process it starts. */
39
+ sessionId: "ORCHESTRA_SESSION_ID",
40
+ };
41
+ /**
42
+ * The spellings a machine set before the rename, read when the name above is absent: a running worker and a
43
+ * pinned runtime image still set only these.
44
+ */
45
+ export const FORMER_MACHINE_ENV = {
46
+ edgeInjected: "SFDK_AUTH_EDGE_INJECTED",
35
47
  sessionId: "SFDK_SESSION_ID",
36
48
  };
49
+ /** Whether this machine declares edge authentication, under either spelling. */
50
+ export function edgeInjectedFrom(env) {
51
+ return firstPresent(env[AUTH_ENV.edgeInjected], env[FORMER_MACHINE_ENV.edgeInjected]) !== undefined;
52
+ }
37
53
  /** The base URL the environment names, and which of the two variables named it. */
38
54
  export function baseUrlFromEnv(env) {
39
55
  for (const variable of [AUTH_ENV.baseUrl, AUTH_ENV.machineBaseUrl]) {
@@ -49,8 +65,13 @@ export function baseUrlFromEnv(env) {
49
65
  * The only reader of the variable: auth resolution and `login` both call it.
50
66
  */
51
67
  export function sessionIdFrom(env) {
52
- const value = env[AUTH_ENV.sessionId]?.trim();
53
- return value === undefined || value === "" ? undefined : value;
68
+ for (const variable of [AUTH_ENV.sessionId, FORMER_MACHINE_ENV.sessionId]) {
69
+ const value = env[variable]?.trim();
70
+ if (value !== undefined && value !== "") {
71
+ return value;
72
+ }
73
+ }
74
+ return undefined;
54
75
  }
55
76
  /**
56
77
  * The chain distinguishes "absent" from "empty", so the fields are copied conditionally: an unconditional
@@ -80,6 +101,26 @@ export class MissingBaseUrlError extends Error {
80
101
  this.name = "MissingBaseUrlError";
81
102
  }
82
103
  }
104
+ /** The trimmed content, or undefined when the file is missing, unreadable, empty or whitespace only. */
105
+ function tokenFromFile(path) {
106
+ let content;
107
+ try {
108
+ content = readFileSync(path, "utf8");
109
+ }
110
+ catch {
111
+ return undefined;
112
+ }
113
+ const token = content.trim();
114
+ return token === "" ? undefined : token;
115
+ }
116
+ /** A failed read mid-command keeps the last token read rather than sending the request bare. */
117
+ function fileTokenReader(path, first) {
118
+ let last = first;
119
+ return () => {
120
+ last = tokenFromFile(path) ?? last;
121
+ return last;
122
+ };
123
+ }
83
124
  /** Loopback hosts reachable over plain HTTP — the local dev-stack contract. */
84
125
  const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
85
126
  /**
@@ -132,12 +173,18 @@ export async function resolveAuthWithWarnings(env, flags, deps = {}) {
132
173
  const profile = (deps.loadProfile ?? loadProfileFromDisk)(flags.profile);
133
174
  const baseUrl = firstPresent(flags.baseUrl, baseUrlFromEnv(env)?.value, profile?.baseUrl);
134
175
  const envToken = firstPresent(env[AUTH_ENV.token]);
176
+ const tokenFilePath = firstPresent(env[AUTH_ENV.tokenFile]);
177
+ const fileToken = envToken === undefined && tokenFilePath !== undefined ? tokenFromFile(tokenFilePath) : undefined;
135
178
  const profileToken = firstPresent(profile?.token);
136
- const edgeDeclared = firstPresent(env[AUTH_ENV.edgeInjected]) !== undefined;
179
+ const edgeDeclared = edgeInjectedFrom(env);
137
180
  // Read before the provider split: a person may export it by hand under any provider.
138
181
  const sessionId = sessionIdFrom(env);
139
182
  const withSessionId = sessionId !== undefined ? { sessionId } : {};
140
- if (envToken === undefined && profileToken === undefined && !edgeDeclared) {
183
+ if (envToken === undefined && fileToken === undefined && profileToken === undefined && !edgeDeclared) {
184
+ if (tokenFilePath !== undefined) {
185
+ throw new MissingCredentialsError(`no auth provider resolved: ${AUTH_ENV.tokenFile} names ${tokenFilePath}, which the worker has not ` +
186
+ `written yet. The worker writes it once the agent is placed on this machine; try again shortly.`);
187
+ }
141
188
  throw new MissingCredentialsError(`no auth provider resolved: export ${AUTH_ENV.token}, run \`orchestra auth login\`, or run inside a host ` +
142
189
  `where the platform declares ${AUTH_ENV.edgeInjected}.`);
143
190
  }
@@ -146,6 +193,17 @@ export async function resolveAuthWithWarnings(env, flags, deps = {}) {
146
193
  }
147
194
  // Refuse before ANY network access, for every provider — the bare edge send included.
148
195
  assertSecretTransport(baseUrl);
196
+ if (fileToken !== undefined && tokenFilePath !== undefined) {
197
+ return {
198
+ auth: {
199
+ provider: "file",
200
+ token: fileTokenReader(tokenFilePath, fileToken),
201
+ baseUrl,
202
+ ...withSessionId,
203
+ },
204
+ warnings: [],
205
+ };
206
+ }
149
207
  if (envToken === undefined && profileToken === undefined) {
150
208
  // The egress stamps the authorization and the stamped identity carries the org.
151
209
  return { auth: { provider: "edge", baseUrl, ...withSessionId }, warnings: [] };
@@ -223,6 +281,9 @@ export function platformTargetFrom(auth) {
223
281
  if (auth.provider === "edge") {
224
282
  return { baseUrl: auth.baseUrl, ...sessionId };
225
283
  }
284
+ if (auth.provider === "file") {
285
+ return { baseUrl: auth.baseUrl, token: auth.token, ...sessionId };
286
+ }
226
287
  return {
227
288
  baseUrl: auth.baseUrl,
228
289
  token: auth.token,
@@ -182,6 +182,10 @@ function guidance(status, api, ctx) {
182
182
  case "env":
183
183
  return (`The token in ${AUTH_ENV.token} is not accepted. Renew that token and re-export ` +
184
184
  `${AUTH_ENV.token}; a stored login profile is not what this command used.`);
185
+ case "file":
186
+ return (`The agent token in the file ${AUTH_ENV.tokenFile} names is not accepted. The worker renews that ` +
187
+ `file, so there is nothing to renew here: the token expired before the worker renewed it, or the ` +
188
+ `agent's placement on this machine, the machine's state or the agent's status refused it.`);
185
189
  case "edge":
186
190
  return (`Authorization is stamped at the egress edge (${AUTH_ENV.edgeInjected} is declared), so there is ` +
187
191
  `no credential here to renew — the stamping path itself failed. Check the egress/identity plane.`);
@@ -50,7 +50,8 @@ export declare function withRefreshRetry(fetchApi: FetchAPI, session: Credential
50
50
  */
51
51
  export interface PlatformTarget {
52
52
  readonly baseUrl: string;
53
- readonly token?: string;
53
+ /** The bearer, or under the file provider a reader of the worker's renewed token, called before each request. */
54
+ readonly token?: string | (() => string);
54
55
  readonly organizationId?: string;
55
56
  /**
56
57
  * `token` above is the bearer as of auth resolution; the session is what keeps a bearer valid, and every
@@ -73,7 +74,7 @@ export declare function createPlatformApis(target: PlatformTarget, fetchApi: Fet
73
74
  export interface RawPlatformRequest {
74
75
  baseUrl: string;
75
76
  /** Absent for the bare edge send, exactly as for a typed command. */
76
- token?: string;
77
+ token?: string | (() => string);
77
78
  organizationId?: string;
78
79
  /** The live credentials behind `token`, when the resolution carried one. */
79
80
  session?: CredentialSession;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The one body rule behind `SFDK_SESSION_ID`: a JSON object body that names no `sessionId` gains the
2
+ * The one body rule behind `ORCHESTRA_SESSION_ID`: a JSON object body that names no `sessionId` gains the
3
3
  * conversation id. Anything else goes out exactly as it came in: no id, a body that is not a string, a
4
4
  * content type other than `application/json`, a body that does not parse as a JSON object, and one that
5
5
  * already names a `sessionId`, which is how an explicit `--session-id` wins.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The one body rule behind `SFDK_SESSION_ID`: a JSON object body that names no `sessionId` gains the
2
+ * The one body rule behind `ORCHESTRA_SESSION_ID`: a JSON object body that names no `sessionId` gains the
3
3
  * conversation id. Anything else goes out exactly as it came in: no id, a body that is not a string, a
4
4
  * content type other than `application/json`, a body that does not parse as a JSON object, and one that
5
5
  * already names a `sessionId`, which is how an explicit `--session-id` wins.
@@ -56,9 +56,9 @@ export interface FirstRunGateDeps extends InterviewDeps {
56
56
  }
57
57
  /**
58
58
  * The pre-command gate: the interview runs on a machine where a person is present and nothing is
59
- * configured. A credential supplied by the environment — an exported token, or the marker the platform sets
60
- * on a machine where the egress stamps the authorization — skips the interview and nothing else, so a
61
- * command's own rules are untouched either way.
59
+ * configured. A credential supplied by the environment — an exported token, the token file the worker
60
+ * names, written yet or not, or the marker the platform sets on a machine where the egress stamps the
61
+ * authorization — skips the interview and nothing else, so a command's own rules are untouched either way.
62
62
  *
63
63
  * The terminal is checked first, so a run with no person to ask touches neither the disk nor the network.
64
64
  */
@@ -7,7 +7,7 @@
7
7
  // EVERYTHING THIS FILE PRINTS GOES TO STDERR, including the sign-in it runs. The interview can precede any
8
8
  // command, and stdout is that command's result channel — under `--output json` a question printed there
9
9
  // would land inside the document a caller is parsing.
10
- import { AUTH_ENV, authFlagsFrom, firstPresent, } from "./core/auth.js";
10
+ import { AUTH_ENV, authFlagsFrom, edgeInjectedFrom, firstPresent, } from "./core/auth.js";
11
11
  import { loginCommand } from "./core/login.js";
12
12
  import { defaultIo } from "./core/output.js";
13
13
  import { DEFAULT_PROFILE_NAME, loadProfile as loadProfileFromDisk, saveProfile as saveProfileToDisk, } from "./core/profile.js";
@@ -168,9 +168,9 @@ async function resolveHarnesses(flags, context) {
168
168
  }
169
169
  /**
170
170
  * The pre-command gate: the interview runs on a machine where a person is present and nothing is
171
- * configured. A credential supplied by the environment — an exported token, or the marker the platform sets
172
- * on a machine where the egress stamps the authorization — skips the interview and nothing else, so a
173
- * command's own rules are untouched either way.
171
+ * configured. A credential supplied by the environment — an exported token, the token file the worker
172
+ * names, written yet or not, or the marker the platform sets on a machine where the egress stamps the
173
+ * authorization — skips the interview and nothing else, so a command's own rules are untouched either way.
174
174
  *
175
175
  * The terminal is checked first, so a run with no person to ask touches neither the disk nor the network.
176
176
  */
@@ -179,7 +179,9 @@ export async function runFirstRunIfNeeded(flags, deps = {}) {
179
179
  if ((deps.isTTY ?? process.stdin.isTTY === true) !== true) {
180
180
  return;
181
181
  }
182
- if (firstPresent(env[AUTH_ENV.token]) !== undefined || firstPresent(env[AUTH_ENV.edgeInjected]) !== undefined) {
182
+ if (firstPresent(env[AUTH_ENV.token]) !== undefined ||
183
+ firstPresent(env[AUTH_ENV.tokenFile]) !== undefined ||
184
+ edgeInjectedFrom(env)) {
183
185
  return;
184
186
  }
185
187
  const loadProfile = deps.loadProfile ?? loadProfileFromDisk;
package/dist/cli/main.js CHANGED
@@ -406,9 +406,10 @@ export function buildProgram(handlers = defaultHandlers, pointer = bundledSkillP
406
406
  .command("enroll")
407
407
  .option("--root <dir>", "directory this worker installed its pinned runtime and harness families into")
408
408
  .description("redeem this machine's enrollment token, then serve it")
409
- .addHelpText("after", "\nThe enrollment token is read from SFDK_ENROLLMENT_TOKEN, or from the file SFDK_ENROLLMENT_TOKEN_FILE\n" +
410
- "names. There is no flag for it: a token on the command line is readable by every process listing on\n" +
411
- "the machine, and it lands in shell history on the way in.\n")
409
+ .addHelpText("after", "\nThe enrollment token is read from ORCHESTRA_WORKER_ENROLLMENT_TOKEN, or from the file\n" +
410
+ "ORCHESTRA_WORKER_ENROLLMENT_TOKEN_FILE names; the former names SFDK_ENROLLMENT_TOKEN and\n" +
411
+ "SFDK_ENROLLMENT_TOKEN_FILE still work. There is no flag for it: a token on the command line is readable\n" +
412
+ "by every process listing on the machine, and it lands in shell history on the way in.\n")
412
413
  .action((options) => handlers.workerEnroll(options));
413
414
  // The local environment, and the machines it runs. `up` deliberately takes NO auth flags: it starts a
414
415
  // stack rather than reaching one, and the address and the token it prints are the stack's own answer. The
@@ -93,6 +93,7 @@ export interface RouteModelsViaGatewayOptions {
93
93
  export declare class EgressPolicy {
94
94
  private readonly defaultAction;
95
95
  private readonly rules;
96
+ private gatewayKey;
96
97
  private constructor();
97
98
  static denyAll(): EgressPolicy;
98
99
  /**
@@ -140,6 +141,8 @@ export declare class EgressPolicy {
140
141
  * as well.
141
142
  */
142
143
  routeModelsViaGateway(opts: RouteModelsViaGatewayOptions): EgressPolicy;
144
+ /** @internal The variables this policy names a credential in, each set to its reference. */
145
+ _declaredEnv(): Record<string, string>;
143
146
  /**
144
147
  * Each entry lands as an ordinary allow rule — the same wire shape, in the same `rules` array, in the same
145
148
  * declaration order — so the emitted plan is byte-identical to one where the author typed every rule out by
@@ -270,18 +273,24 @@ export interface EnvironmentProps {
270
273
  /** At most four, and never at `/home`. */
271
274
  disks?: DiskMount[];
272
275
  /**
273
- * Set on every machine built from this environment and forwarded into the agent process itself. There is
274
- * deliberately no "looks like a secret" detector on them: a checker that caught most API-key shapes would
275
- * teach authors that the ones it missed were safe. Names beginning `SFDK_` are platform-owned, written last
276
- * and rejected with a 400, except the four `SFDK_WORKER_*` release settings, which reach the worker alone. `HOME` is not a platform
277
- * variable at all — the account inside the runtime image decides where the home directory is — so declaring
278
- * it points the harness elsewhere, and a home off the mounted volume does not survive a machine recycle.
279
- * `ANTHROPIC_BASE_URL` is not reserved; the way to point a harness at the model gateway is the agent's
280
- * `modelSettings.enableModelGateway`. Whether two names are the same name is keyed on the TARGET OS, which makes two rejections real on
281
- * Windows that cannot happen on POSIX: a case-variant of the reserved prefix is reserved, and two keys
282
- * differing only in case are one variable, so the API keeps neither.
276
+ * Set on every machine built from this environment and forwarded into the agent process. There is no "looks
277
+ * like a secret" detector: one that caught most key shapes would teach authors the rest were safe. Names
278
+ * beginning `ORCHESTRA_` or `SFDK_` are platform-owned and refused, except ordinary `ORCHESTRA_FDK_` names and
279
+ * the `ORCHESTRA_WORKER_*` release settings, which reach the worker alone. `HOME` is the image's to decide;
280
+ * declaring it moves the harness off the mounted home. Names are compared under the target OS, so on Windows a
281
+ * case-variant of a reserved prefix is reserved and two keys differing only in case are one variable.
282
+ *
283
+ * A Secret or a Connection in place of text sets the reference `orchestra:secret/<id>` or
284
+ * `orchestra:connection/<id>`, resolved at apply, which a tool sends as its token and the egress edge
285
+ * replaces with that credential (`GH_TOKEN`, `GITLAB_TOKEN`, `ORCHESTRA_GIT_CREDENTIAL` for git).
286
+ */
287
+ env?: Record<string, string | ISecret | IConnection>;
288
+ /**
289
+ * The credentials a request from this environment's machines gets when it names none, the first whose hosts
290
+ * cover the request's host serving. `replace` defaults to true; false leaves a request that carries its own
291
+ * value in the credential's header as it was sent. Omitted, apply leaves the live list alone; `[]` clears it.
283
292
  */
284
- env?: Record<string, string>;
293
+ credentials?: EnvironmentCredential[];
285
294
  /** The reference rides the environment; the value is resolved platform-side at each machine create and never
286
295
  * leaves the control plane. */
287
296
  imagePullCredential?: ISecret;
@@ -304,6 +313,10 @@ export interface EnvironmentProps {
304
313
  /** Fixed at create; undeclared, the environment is created in Shared. */
305
314
  team?: ITeam;
306
315
  }
316
+ export interface EnvironmentCredential {
317
+ credential: ISecret | IConnection;
318
+ replace?: boolean;
319
+ }
307
320
  export declare class Environment extends Construct implements IEnvironment {
308
321
  /** Accepts a platform id or a slug interchangeably — one rule, stated on `IResource`. */
309
322
  readonly environmentId: string;