@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.
- package/README.md +1 -1
- package/dist/cli/api/approval.js +3 -3
- package/dist/cli/commands/outpost/local.js +3 -3
- package/dist/cli/commands/outpost/machine-names.d.ts +17 -0
- package/dist/cli/commands/outpost/machine-names.js +22 -0
- package/dist/cli/commands/outpost/up.js +7 -5
- package/dist/cli/commands/worker/enroll.d.ts +4 -3
- package/dist/cli/commands/worker/enroll.js +4 -3
- package/dist/cli/core/auth.d.ts +22 -2
- package/dist/cli/core/auth.js +69 -8
- package/dist/cli/core/errors.js +4 -0
- package/dist/cli/core/platform-client.d.ts +3 -2
- package/dist/cli/core/session-body.d.ts +1 -1
- package/dist/cli/core/session-body.js +1 -1
- package/dist/cli/firstRun.d.ts +3 -3
- package/dist/cli/firstRun.js +7 -5
- package/dist/cli/main.js +4 -3
- package/dist/constructs/environment.d.ts +24 -11
- package/dist/constructs/environment.js +76 -12
- package/dist/constructs/secret.d.ts +17 -7
- package/dist/constructs/secret.js +24 -16
- package/dist/core/labels.d.ts +32 -6
- package/dist/core/labels.js +70 -8
- package/dist/engine/diff.js +38 -11
- package/dist/engine/gateway-key-reconcile.d.ts +1 -2
- package/dist/engine/gateway-key-reconcile.js +19 -15
- package/dist/engine/pipeline-reconcile.js +2 -2
- package/dist/engine/pipeline-workflow-reconcile.js +15 -9
- package/dist/engine/reconcile.js +48 -15
- package/dist/engine/refusals.d.ts +0 -5
- package/dist/engine/refusals.js +1 -5
- package/dist/engine/secrets-apply.d.ts +10 -14
- package/dist/engine/secrets-apply.js +12 -17
- package/dist/engine/workflow-reconcile.d.ts +1 -1
- package/dist/generated/cli-commands.json +170 -34
- package/dist/generated/providers.d.ts +40 -0
- package/dist/generated/providers.js +30 -0
- package/docs/REFERENCE.md +26 -12
- package/node_modules/@orchestraworks/client/dist/esm/models/ApiErrorDetail.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/esm/models/Environment.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/Environment.js +3 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.d.ts +38 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.js +45 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpec.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentSnapshot.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.d.ts +6 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.js +2 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/OAuthTokenRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/Secret.d.ts +1 -7
- package/node_modules/@orchestraworks/client/dist/esm/models/Secret.js +0 -4
- package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/UpdateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/esm/models/index.d.ts +1 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/index.js +1 -0
- package/node_modules/@orchestraworks/client/dist/models/ApiErrorDetail.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/models/CreateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/models/Environment.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/models/Environment.js +3 -0
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.d.ts +38 -0
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.js +52 -0
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpec.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentSnapshot.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.d.ts +6 -0
- package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.js +2 -0
- package/node_modules/@orchestraworks/client/dist/models/OAuthTokenRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/Secret.d.ts +1 -7
- package/node_modules/@orchestraworks/client/dist/models/Secret.js +0 -4
- package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/models/UpdateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/models/index.d.ts +1 -0
- package/node_modules/@orchestraworks/client/dist/models/index.js +1 -0
- package/node_modules/@orchestraworks/client/package.json +1 -1
- package/package.json +4 -4
- package/skills/orchestra/SKILL.md +38 -17
- package/skills/orchestra-fdk/references/authoring.md +34 -26
- package/skills/orchestra-fdk/references/environment.md +49 -12
- package/skills/orchestra-fdk/references/integrations/code-repo.md +26 -5
- package/skills/orchestra-fdk/references/phases/1-discover.md +1 -1
- package/skills/orchestra-fdk/references/phases/2-plan.md +1 -1
- package/skills/orchestra-fdk/references/troubleshooting.md +5 -3
- package/templates/bug-fix/main.ts +7 -4
- package/templates/engineering-team/lib/environment.ts +6 -3
- package/templates/engineering-team/package.json +1 -1
- package/templates/todo-factory/main.ts +7 -4
- 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.
|
|
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
|
package/dist/cli/api/approval.js
CHANGED
|
@@ -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
|
|
143
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
271
|
+
const authority = machineValue(env, EGRESS_CA_URL);
|
|
271
272
|
if (authority === "") {
|
|
272
|
-
throw new Error(
|
|
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
|
-
|
|
282
|
+
...bothSpellings(TRANSPORT_TRUST_ROOTS_FILE, trustFile),
|
|
282
283
|
...emptiedCarriers(harness, env),
|
|
283
284
|
});
|
|
284
|
-
delete composed.
|
|
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
|
|
7
|
-
* the file `
|
|
8
|
-
* listing on the machine for as long as the process runs, and it
|
|
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
|
|
4
|
-
* the file `
|
|
5
|
-
* listing on the machine for as long as the process runs, and it
|
|
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);
|
package/dist/cli/core/auth.d.ts
CHANGED
|
@@ -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: "
|
|
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;
|
package/dist/cli/core/auth.js
CHANGED
|
@@ -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
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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: "
|
|
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
|
|
53
|
-
|
|
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 =
|
|
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,
|
package/dist/cli/core/errors.js
CHANGED
|
@@ -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
|
-
|
|
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 `
|
|
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 `
|
|
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.
|
package/dist/cli/firstRun.d.ts
CHANGED
|
@@ -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,
|
|
60
|
-
*
|
|
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
|
*/
|
package/dist/cli/firstRun.js
CHANGED
|
@@ -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,
|
|
172
|
-
*
|
|
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 ||
|
|
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
|
|
410
|
-
"names
|
|
411
|
-
"
|
|
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
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
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
|
-
|
|
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;
|