open-managed-agents 0.1.0 → 0.1.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 +32 -7
- package/bin/oma.mjs +24 -2
- package/dist/scripts/oma-onboard.js +544 -0
- package/dist/scripts/oma-onboarding-resources.js +131 -0
- package/dist/src/control-plane/app.js +33 -0
- package/dist/src/control-plane/console/bootstrap.js +59 -0
- package/dist/src/control-plane/environments/service.js +17 -1
- package/dist/src/control-plane/environments/store.js +12 -3
- package/dist/src/control-plane/openapi/document.js +2 -2
- package/dist/src/main.js +31 -2
- package/dist/ui/managed-agents-console/src/api.js +8 -0
- package/dist/ui/managed-agents-console/src/app.jsx +19 -1
- package/package.json +12 -1
package/README.md
CHANGED
|
@@ -12,17 +12,35 @@ files, and sandboxes live on your machines, under your policies.
|
|
|
12
12
|
> self-hosted baseline. The orchestration/persistence layer (multi-agent
|
|
13
13
|
> runtime, memory stores,
|
|
14
14
|
> scheduled deployments, webhooks, outcomes) is deliberately deferred. See
|
|
15
|
-
> **[PARITY.md](PARITY.md)** for the full domain-by-domain scorecard and the
|
|
16
|
-
> active parity worklist, and **[ALPHA.md](ALPHA.md)** for the first-user
|
|
15
|
+
> **[PARITY.md](https://github.com/oneryalcin/open-managed-agents/blob/main/PARITY.md)** for the full domain-by-domain scorecard and the
|
|
16
|
+
> active parity worklist, and **[ALPHA.md](https://github.com/oneryalcin/open-managed-agents/blob/main/ALPHA.md)** for the first-user
|
|
17
17
|
> alpha readiness plan.
|
|
18
18
|
|
|
19
19
|
## Quickstart
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
> **Public onboarding preview:** the one-command path is shipped, but its
|
|
22
|
+
> three-minute warm-path target has not yet completed the real-user timing
|
|
23
|
+
> study. Docker image downloads are explicitly outside that warm-path target.
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
Prerequisites: Node.js 22.19 or newer and a running Docker-compatible daemon.
|
|
26
|
+
Then run:
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
```bash
|
|
29
|
+
npx --yes open-managed-agents@latest
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The guided terminal checks local prerequisites, asks which model provider to
|
|
33
|
+
use, reads the API key through a masked prompt, starts an authenticated
|
|
34
|
+
loopback appliance, creates or reuses a starter agent/environment/session, and
|
|
35
|
+
opens that session in the console. The appliance stays attached to the command;
|
|
36
|
+
press Ctrl-C to stop it. State and provider credentials persist under `~/.oma`.
|
|
37
|
+
|
|
38
|
+
If the pinned sandbox image is not cached, OMA names the image and asks before
|
|
39
|
+
pulling it. Rerunning the same command while its onboarding-owned appliance is
|
|
40
|
+
alive reopens the existing starter session rather than creating duplicates.
|
|
41
|
+
|
|
42
|
+
For the full source-checkout diagnostics and deterministic smoke tests, use
|
|
43
|
+
the **[Getting Started guide](https://github.com/oneryalcin/open-managed-agents/blob/main/docs/getting-started.md)**:
|
|
26
44
|
|
|
27
45
|
```bash
|
|
28
46
|
git clone https://github.com/oneryalcin/open-managed-agents.git
|
|
@@ -45,7 +63,8 @@ events, and cleanup.
|
|
|
45
63
|
Docker-local egress boundary: npm, uv/PyPI, and GitHub must work under the
|
|
46
64
|
reviewed preset while an unrelated HTTPS destination remains denied.
|
|
47
65
|
|
|
48
|
-
`oma up` starts the durable local appliance
|
|
66
|
+
In the advanced source-checkout flow, `oma up` starts the durable local appliance
|
|
67
|
+
in the foreground, stores data
|
|
49
68
|
under `~/.oma`, and prints the console URL plus the first workspace API key
|
|
50
69
|
once. Docker deployments have the pinned approved-HTTPS sidecar capability,
|
|
51
70
|
but every environment remains offline unless its immutable allowlist selects a
|
|
@@ -57,6 +76,12 @@ mint another while the server is running:
|
|
|
57
76
|
oma keys mint
|
|
58
77
|
```
|
|
59
78
|
|
|
79
|
+
When using the public package without a global install, the equivalent is:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx --yes open-managed-agents@latest keys mint
|
|
83
|
+
```
|
|
84
|
+
|
|
60
85
|
Interactive, air-gap-safe OpenAPI documentation is served at `/docs/`, with
|
|
61
86
|
the same schema available to tools at `/openapi.json`. The documentation lists
|
|
62
87
|
only routes OMA currently ships; workspace and admin credentials entered in the
|
|
@@ -69,7 +94,7 @@ import anthropic
|
|
|
69
94
|
|
|
70
95
|
client = anthropic.Anthropic(
|
|
71
96
|
base_url="http://127.0.0.1:4180",
|
|
72
|
-
api_key="oma_...", #
|
|
97
|
+
api_key="oma_...", # returned by `oma keys mint`
|
|
73
98
|
default_headers={"anthropic-beta": "managed-agents-2026-04-01"},
|
|
74
99
|
)
|
|
75
100
|
|
package/bin/oma.mjs
CHANGED
|
@@ -20,7 +20,9 @@ const { resolveOmaUpEgressEnvironment } = await import(
|
|
|
20
20
|
|
|
21
21
|
checkNode();
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// `npx open-managed-agents` is the public one-command path. Keep an installed
|
|
24
|
+
// bare `oma` invocation as help so existing CLI discovery remains stable.
|
|
25
|
+
const command = args[0] ?? (process.env.npm_command === "exec" ? "onboard" : undefined);
|
|
24
26
|
if (command === undefined || command === "help" || command === "--help" || command === "-h") {
|
|
25
27
|
if (command === "help" && args.length > 1) {
|
|
26
28
|
printCommandHelp(args.slice(1));
|
|
@@ -33,7 +35,7 @@ if ((command === "version" && args.length === 1) || command === "--version" || c
|
|
|
33
35
|
console.log(`oma ${packageJson.version}`);
|
|
34
36
|
process.exit(0);
|
|
35
37
|
}
|
|
36
|
-
const commandArgs = args.slice(1);
|
|
38
|
+
const commandArgs = command === "onboard" && args.length === 0 ? [] : args.slice(1);
|
|
37
39
|
const helpIndex = commandArgs.findIndex((arg) => arg === "--help" || arg === "-h");
|
|
38
40
|
if (helpIndex !== -1) {
|
|
39
41
|
if (helpIndex !== commandArgs.length - 1) fail(`Help must be the last argument. Run \`oma help ${[command, ...commandArgs.slice(0, helpIndex)].join(" ")}\`.`);
|
|
@@ -57,6 +59,8 @@ if (command === "up") {
|
|
|
57
59
|
await runModels(args.slice(1));
|
|
58
60
|
} else if (command === "auth") {
|
|
59
61
|
await runAuth(args.slice(1));
|
|
62
|
+
} else if (command === "onboard") {
|
|
63
|
+
await runOnboard(commandArgs);
|
|
60
64
|
} else if (command === "doctor") {
|
|
61
65
|
await runDoctor(args.slice(1));
|
|
62
66
|
} else if (command === "down" || command === "logs" || command === "status") {
|
|
@@ -77,6 +81,21 @@ async function runDoctor(commandArgs) {
|
|
|
77
81
|
);
|
|
78
82
|
}
|
|
79
83
|
|
|
84
|
+
async function runOnboard(commandArgs) {
|
|
85
|
+
const childEnv = commandArgs.includes("--json")
|
|
86
|
+
? { ...process.env, OMA_LOG_LEVEL: "error" }
|
|
87
|
+
: process.env;
|
|
88
|
+
await runChild(
|
|
89
|
+
process.execPath,
|
|
90
|
+
[
|
|
91
|
+
...runtimeNodeFlags(),
|
|
92
|
+
runtimeFile("scripts", "oma-onboard"),
|
|
93
|
+
...commandArgs,
|
|
94
|
+
],
|
|
95
|
+
childEnv,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
80
99
|
async function runUp(commandArgs) {
|
|
81
100
|
let sandbox = "docker-local";
|
|
82
101
|
for (let index = 0; index < commandArgs.length; index += 1) {
|
|
@@ -415,6 +434,7 @@ Usage:
|
|
|
415
434
|
oma auth set <provider> [--stdin]
|
|
416
435
|
oma auth status [provider]
|
|
417
436
|
oma auth remove <provider>
|
|
437
|
+
oma onboard [--provider name] [--stdin] [--sandbox docker|microsandbox] [--pull] [--json]
|
|
418
438
|
oma doctor [--sandbox docker|microsandbox] [--json]
|
|
419
439
|
oma admin init
|
|
420
440
|
oma admin status
|
|
@@ -429,6 +449,7 @@ Commands:
|
|
|
429
449
|
providers Inspect enabled Pi model providers and credential readiness.
|
|
430
450
|
models List or validate enabled Pi models.
|
|
431
451
|
auth Store, remove, or inspect model provider API-key credentials.
|
|
452
|
+
onboard Prepare credentials, start or resume local OMA, and open a starter console session.
|
|
432
453
|
doctor Run secret-safe, read-only local readiness diagnostics.
|
|
433
454
|
admin Initialize or inspect local appliance admin mode.
|
|
434
455
|
version Print the installed OMA version.
|
|
@@ -478,6 +499,7 @@ function commandHelpFor(key) {
|
|
|
478
499
|
"auth set": ["Usage: oma auth set <provider> [--stdin]", "Store a provider key without accepting it as a command-line argument.", "Example: printf '%s\\n' \"$OPENAI_API_KEY\" | oma auth set openai --stdin"],
|
|
479
500
|
"auth status": ["Usage: oma auth status [provider]", "Show credential readiness without secret values.", "Example: oma auth status openai"],
|
|
480
501
|
"auth remove": ["Usage: oma auth remove <provider>", "Remove a stored provider credential idempotently.", "Example: oma auth remove openai"],
|
|
502
|
+
onboard: ["Usage: oma onboard [--provider name] [--stdin] [--sandbox docker|microsandbox] [--pull] [--json]", "Verify warm-path prerequisites, select or store a provider credential, start or resume the attached local appliance, and open its starter console session. --pull explicitly permits a missing Docker sandbox image to be downloaded.", "Examples: oma onboard; oma onboard --pull; printf '%s\\n' \"$ANTHROPIC_API_KEY\" | oma onboard --provider anthropic --stdin"],
|
|
481
503
|
admin: ["Usage: oma admin <init|status>", "Initialize or inspect appliance-wide admin mode.", "Examples: oma admin init; oma admin status"],
|
|
482
504
|
"admin init": ["Usage: oma admin init [--file path]", "Create a new owner-only admin key file; existing files are never overwritten.", "Example: oma admin init"],
|
|
483
505
|
"admin status": ["Usage: oma admin status", "Inspect admin configuration without printing the key.", "Example: oma admin status"],
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { stdin as input } from "node:process";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { inspectOma, } from "./oma-doctor.js";
|
|
10
|
+
import { runModelsCli } from "./oma-models.js";
|
|
11
|
+
import { DEFAULT_OMA_SANDBOX_IMAGE } from "../src/control-plane/sessions/pi/sandbox/image.js";
|
|
12
|
+
import { resolveOmaUpEgressEnvironment } from "../src/control-plane/egress/image.js";
|
|
13
|
+
import { ConsoleBootstrapService } from "../src/control-plane/console/bootstrap.js";
|
|
14
|
+
import { startAppliance } from "../src/main.js";
|
|
15
|
+
import { ensureStarterResources } from "./oma-onboarding-resources.js";
|
|
16
|
+
export const ONBOARD_CANCELLED = Symbol("oma-onboard-cancelled");
|
|
17
|
+
const PROVIDER_LABELS = {
|
|
18
|
+
anthropic: "Anthropic",
|
|
19
|
+
openai: "OpenAI",
|
|
20
|
+
openrouter: "OpenRouter",
|
|
21
|
+
};
|
|
22
|
+
export async function runOnboard(argv, runtime) {
|
|
23
|
+
const wantsJson = argv.includes("--json");
|
|
24
|
+
let options;
|
|
25
|
+
try {
|
|
26
|
+
options = parseOnboardArgs(argv);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
emitFailure(runtime, wantsJson, error instanceof Error ? error.message : String(error));
|
|
30
|
+
return { code: 2, credential: "not_reached" };
|
|
31
|
+
}
|
|
32
|
+
if (!options.json)
|
|
33
|
+
runtime.terminal.intro(pc.bgCyan(pc.black(" Open Managed Agents ")));
|
|
34
|
+
if (options.provider === undefined && !options.stdin) {
|
|
35
|
+
try {
|
|
36
|
+
const resumed = await runtime.resume({ json: options.json });
|
|
37
|
+
if (resumed !== undefined) {
|
|
38
|
+
emitResumed(runtime, options.json, resumed.sessionId);
|
|
39
|
+
return { code: 0, credential: "reused", sessionId: resumed.sessionId };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
emitFailure(runtime, options.json, safeMessage(error));
|
|
44
|
+
return { code: 1, credential: "not_reached" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!runtime.interactive && !options.stdin && options.provider === undefined) {
|
|
48
|
+
emitFailure(runtime, options.json, "Non-interactive onboarding requires --provider and either --stdin or an existing stored credential.");
|
|
49
|
+
return { code: 2, credential: "not_reached" };
|
|
50
|
+
}
|
|
51
|
+
if (options.stdin && options.provider === undefined) {
|
|
52
|
+
emitFailure(runtime, options.json, "--stdin requires --provider so OMA knows which credential to store.");
|
|
53
|
+
return { code: 2, credential: "not_reached" };
|
|
54
|
+
}
|
|
55
|
+
let report = await runtime.inspect({ sandbox: options.sandbox });
|
|
56
|
+
const imageMissing = options.sandbox === "docker-local"
|
|
57
|
+
&& report.checks.find((check) => check.id === "sandbox.image")?.status !== "pass";
|
|
58
|
+
const failures = onboardingPreflightFailures(report, options.sandbox, { allowMissingImage: imageMissing });
|
|
59
|
+
if (failures.length > 0) {
|
|
60
|
+
const detail = failures.join("\n");
|
|
61
|
+
emitFailure(runtime, options.json, detail);
|
|
62
|
+
return { code: 1, credential: "not_reached", report };
|
|
63
|
+
}
|
|
64
|
+
if (imageMissing) {
|
|
65
|
+
let approved = options.pull;
|
|
66
|
+
if (!approved && runtime.interactive && !options.json) {
|
|
67
|
+
const answer = await runtime.terminal.confirm({
|
|
68
|
+
message: `The cached OMA image is missing. Pull it now? This download is outside the three-minute warm-path target.`,
|
|
69
|
+
initialValue: true,
|
|
70
|
+
});
|
|
71
|
+
if (answer === ONBOARD_CANCELLED)
|
|
72
|
+
return cancelled(runtime, options.json, report);
|
|
73
|
+
approved = answer;
|
|
74
|
+
}
|
|
75
|
+
if (!approved) {
|
|
76
|
+
emitFailure(runtime, options.json, `Pull the cached prerequisite image, then retry: docker pull ${DEFAULT_OMA_SANDBOX_IMAGE}`);
|
|
77
|
+
return { code: 1, credential: "not_reached", report };
|
|
78
|
+
}
|
|
79
|
+
if (!options.json)
|
|
80
|
+
runtime.terminal.step(`Pulling ${DEFAULT_OMA_SANDBOX_IMAGE}`);
|
|
81
|
+
try {
|
|
82
|
+
await runtime.pullImage(DEFAULT_OMA_SANDBOX_IMAGE);
|
|
83
|
+
report = await runtime.inspect({ sandbox: options.sandbox });
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
emitFailure(runtime, options.json, safeMessage(error));
|
|
87
|
+
return { code: 1, credential: "not_reached", report };
|
|
88
|
+
}
|
|
89
|
+
const afterPull = onboardingPreflightFailures(report, options.sandbox);
|
|
90
|
+
if (afterPull.length > 0) {
|
|
91
|
+
emitFailure(runtime, options.json, afterPull.join("\n"));
|
|
92
|
+
return { code: 1, credential: "not_reached", report };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!options.json)
|
|
96
|
+
runtime.terminal.step("Docker and the cached OMA sandbox image are ready.");
|
|
97
|
+
let provider = options.provider;
|
|
98
|
+
let providerState;
|
|
99
|
+
try {
|
|
100
|
+
providerState = await runtime.providerStatus(provider);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
emitFailure(runtime, options.json, safeMessage(error));
|
|
104
|
+
return { code: 2, credential: "not_reached", report };
|
|
105
|
+
}
|
|
106
|
+
if (providerState.providers.length === 0) {
|
|
107
|
+
emitFailure(runtime, options.json, "No enabled model providers are available for onboarding.");
|
|
108
|
+
return { code: 2, credential: "not_reached", report };
|
|
109
|
+
}
|
|
110
|
+
if (provider === undefined) {
|
|
111
|
+
if (!runtime.interactive) {
|
|
112
|
+
emitFailure(runtime, options.json, "Non-interactive onboarding requires --provider.");
|
|
113
|
+
return { code: 2, credential: "not_reached", report };
|
|
114
|
+
}
|
|
115
|
+
const selected = await runtime.terminal.select({
|
|
116
|
+
message: "Choose a model provider",
|
|
117
|
+
options: providerState.providers.map((value) => ({
|
|
118
|
+
value,
|
|
119
|
+
label: PROVIDER_LABELS[value] ?? value,
|
|
120
|
+
})),
|
|
121
|
+
});
|
|
122
|
+
if (selected === ONBOARD_CANCELLED)
|
|
123
|
+
return cancelled(runtime, options.json, report);
|
|
124
|
+
provider = selected;
|
|
125
|
+
try {
|
|
126
|
+
providerState = await runtime.providerStatus(provider);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
emitFailure(runtime, options.json, safeMessage(error));
|
|
130
|
+
return { code: 2, credential: "not_reached", report };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!providerState.providers.includes(provider)) {
|
|
134
|
+
emitFailure(runtime, options.json, `Model provider ${JSON.stringify(provider)} is not enabled. Enabled providers: ${providerState.providers.join(", ")}.`);
|
|
135
|
+
return { code: 2, credential: "not_reached", report };
|
|
136
|
+
}
|
|
137
|
+
if (providerState.stored) {
|
|
138
|
+
if (!runtime.interactive || options.json) {
|
|
139
|
+
return finishOnboarding(runtime, options, provider, "reused", report);
|
|
140
|
+
}
|
|
141
|
+
const reuse = await runtime.terminal.confirm({
|
|
142
|
+
message: `Use the saved ${PROVIDER_LABELS[provider] ?? provider} credential?`,
|
|
143
|
+
initialValue: true,
|
|
144
|
+
});
|
|
145
|
+
if (reuse === ONBOARD_CANCELLED)
|
|
146
|
+
return cancelled(runtime, options.json, report);
|
|
147
|
+
if (reuse) {
|
|
148
|
+
return finishOnboarding(runtime, options, provider, "reused", report);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
let key;
|
|
152
|
+
if (options.stdin) {
|
|
153
|
+
try {
|
|
154
|
+
key = await runtime.readStdin();
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
emitFailure(runtime, options.json, safeMessage(error));
|
|
158
|
+
return { code: 2, credential: "not_reached", report };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else if (runtime.interactive) {
|
|
162
|
+
key = await runtime.terminal.password({
|
|
163
|
+
message: `Paste API key for ${PROVIDER_LABELS[provider] ?? provider}`,
|
|
164
|
+
validate(value) {
|
|
165
|
+
return value.trim() === "" ? "API key must not be empty." : undefined;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
emitFailure(runtime, options.json, "Non-interactive onboarding requires --stdin when no stored credential exists.");
|
|
171
|
+
return { code: 2, credential: "not_reached", report };
|
|
172
|
+
}
|
|
173
|
+
if (key === ONBOARD_CANCELLED)
|
|
174
|
+
return cancelled(runtime, options.json, report);
|
|
175
|
+
if (key.trim() === "") {
|
|
176
|
+
emitFailure(runtime, options.json, "API key must not be empty.");
|
|
177
|
+
return { code: 2, credential: "not_reached", report };
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
await runtime.storeCredential(provider, key.trim());
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
emitFailure(runtime, options.json, safeMessage(error));
|
|
184
|
+
return { code: 1, credential: "not_reached", report };
|
|
185
|
+
}
|
|
186
|
+
return finishOnboarding(runtime, options, provider, "stored", report);
|
|
187
|
+
}
|
|
188
|
+
async function finishOnboarding(runtime, options, provider, credential, report) {
|
|
189
|
+
try {
|
|
190
|
+
let readyEmitted = false;
|
|
191
|
+
const launched = await runtime.launch(provider, options.sandbox, {
|
|
192
|
+
json: options.json,
|
|
193
|
+
onReady(sessionId) {
|
|
194
|
+
readyEmitted = true;
|
|
195
|
+
emitSuccess(runtime, options.json, provider, credential, sessionId);
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
if (!readyEmitted)
|
|
199
|
+
emitSuccess(runtime, options.json, provider, credential, launched.sessionId);
|
|
200
|
+
return { code: 0, provider, credential, report, sessionId: launched.sessionId };
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
emitFailure(runtime, options.json, safeMessage(error));
|
|
204
|
+
return { code: 1, provider, credential, report };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export function createProcessOnboardRuntime() {
|
|
208
|
+
return {
|
|
209
|
+
env: process.env,
|
|
210
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
211
|
+
terminal: createClackTerminal(),
|
|
212
|
+
inspect: (options) => inspectOma(options, { env: process.env }),
|
|
213
|
+
async providerStatus(provider) {
|
|
214
|
+
const status = await runModelsCli(["providers-status"], { env: process.env });
|
|
215
|
+
if (status.code !== 0)
|
|
216
|
+
throw new Error(status.stderr.trim());
|
|
217
|
+
const providers = parseProviderStatus(status.stdout);
|
|
218
|
+
if (provider === undefined)
|
|
219
|
+
return { providers, stored: undefined };
|
|
220
|
+
const auth = await runModelsCli(["auth-status", provider], { env: process.env });
|
|
221
|
+
if (auth.code !== 0)
|
|
222
|
+
throw new Error(auth.stderr.trim());
|
|
223
|
+
return { providers, stored: parseStoredCredential(auth.stdout, provider) };
|
|
224
|
+
},
|
|
225
|
+
async storeCredential(provider, key) {
|
|
226
|
+
const result = await runModelsCli(["auth-set", provider], {
|
|
227
|
+
env: process.env,
|
|
228
|
+
promptSecret: async () => key,
|
|
229
|
+
});
|
|
230
|
+
if (result.code !== 0)
|
|
231
|
+
throw new Error(result.stderr.trim());
|
|
232
|
+
},
|
|
233
|
+
readStdin: readOneSecretFromStdin,
|
|
234
|
+
async pullImage(image) {
|
|
235
|
+
const result = spawnSync("docker", ["pull", image], { stdio: "inherit" });
|
|
236
|
+
if (result.status !== 0)
|
|
237
|
+
throw new Error(`docker pull failed for ${image}`);
|
|
238
|
+
},
|
|
239
|
+
resume: (options) => tryResumeLocalOnboarding(process.env, createClackTerminal(), options.json),
|
|
240
|
+
launch: (provider, sandbox, options) => launchLocalOnboarding(provider, sandbox, process.env, createClackTerminal(), options),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
export async function launchLocalOnboarding(provider, sandbox, env, terminal, options) {
|
|
244
|
+
const bootstrap = new ConsoleBootstrapService();
|
|
245
|
+
const controlToken = `oct_${randomBytes(32).toString("base64url")}`;
|
|
246
|
+
const runtimeEnv = resolveOmaUpEgressEnvironment(env, sandbox);
|
|
247
|
+
const applianceEnv = {
|
|
248
|
+
...runtimeEnv,
|
|
249
|
+
OMA_AUTH_MODE: "api-key",
|
|
250
|
+
OMA_HOST: "127.0.0.1",
|
|
251
|
+
OMA_PORT: runtimeEnv.OMA_PORT ?? "0",
|
|
252
|
+
OMA_TLS_TERMINATED: "0",
|
|
253
|
+
OMA_SANDBOX_PROVIDER: sandbox,
|
|
254
|
+
...(sandbox === "docker-local"
|
|
255
|
+
? { OMA_ALLOW_DOCKER_LOCAL: "true" }
|
|
256
|
+
: { OMA_ALLOW_MICROSANDBOX_LOCAL: "true" }),
|
|
257
|
+
};
|
|
258
|
+
if (!options.json)
|
|
259
|
+
terminal.step("Starting local OMA");
|
|
260
|
+
const appliance = await startAppliance(applianceEnv, {
|
|
261
|
+
log: () => undefined,
|
|
262
|
+
onboarding: { bootstrap, controlToken },
|
|
263
|
+
});
|
|
264
|
+
let resumeStateWritten = false;
|
|
265
|
+
try {
|
|
266
|
+
if (appliance.onboarding === undefined)
|
|
267
|
+
throw new Error("Local onboarding requires workspace authentication");
|
|
268
|
+
if (!options.json)
|
|
269
|
+
terminal.step("Preparing your first session");
|
|
270
|
+
const starter = await ensureStarterResources({
|
|
271
|
+
baseUrl: appliance.baseUrl,
|
|
272
|
+
workspaceKey: appliance.onboarding.workspaceKey,
|
|
273
|
+
provider,
|
|
274
|
+
});
|
|
275
|
+
if (!options.json)
|
|
276
|
+
for (const warning of starter.warnings)
|
|
277
|
+
terminal.warn(warning);
|
|
278
|
+
writeResumeState(env, {
|
|
279
|
+
schema_version: 1,
|
|
280
|
+
pid: process.pid,
|
|
281
|
+
base_url: appliance.baseUrl,
|
|
282
|
+
session_id: starter.sessionId,
|
|
283
|
+
control_token: controlToken,
|
|
284
|
+
});
|
|
285
|
+
resumeStateWritten = true;
|
|
286
|
+
const url = `${appliance.baseUrl}/console/#bootstrap=${encodeURIComponent(appliance.onboarding.bootstrapNonce)}&session=${encodeURIComponent(starter.sessionId)}`;
|
|
287
|
+
const opened = openBrowser(url);
|
|
288
|
+
if (!opened && !options.json)
|
|
289
|
+
terminal.warn(`Browser launch failed. Open this local URL manually: ${url}`);
|
|
290
|
+
options.onReady(starter.sessionId);
|
|
291
|
+
if (!options.json)
|
|
292
|
+
terminal.outro(`Ready — ${opened ? "console opened" : "console available"} at ${appliance.baseUrl}/console/\nPress Ctrl-C to stop OMA.`);
|
|
293
|
+
await waitForShutdownSignal();
|
|
294
|
+
return { sessionId: starter.sessionId };
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
if (resumeStateWritten)
|
|
298
|
+
removeResumeState(env, controlToken);
|
|
299
|
+
await appliance.close();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
export async function tryResumeLocalOnboarding(env, terminal, json, dependencies = {}) {
|
|
303
|
+
const fetchImpl = dependencies.fetch ?? fetch;
|
|
304
|
+
const openBrowserImpl = dependencies.openBrowser ?? openBrowser;
|
|
305
|
+
const state = readResumeState(env);
|
|
306
|
+
if (state === undefined)
|
|
307
|
+
return undefined;
|
|
308
|
+
const health = await fetchImpl(new URL("/health", state.base_url), {
|
|
309
|
+
signal: AbortSignal.timeout(1_000),
|
|
310
|
+
}).catch(() => undefined);
|
|
311
|
+
if (health?.ok !== true) {
|
|
312
|
+
removeResumeState(env, state.control_token);
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
const renewed = await fetchImpl(new URL("/console/auth/bootstrap/renew", state.base_url), {
|
|
316
|
+
method: "POST",
|
|
317
|
+
headers: { "x-oma-onboarding-token": state.control_token },
|
|
318
|
+
signal: AbortSignal.timeout(1_000),
|
|
319
|
+
}).catch(() => undefined);
|
|
320
|
+
if (renewed?.ok !== true) {
|
|
321
|
+
removeResumeState(env, state.control_token);
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
const body = await renewed.json();
|
|
325
|
+
if (typeof body.nonce !== "string" || !body.nonce.startsWith("ocb_")) {
|
|
326
|
+
throw new Error("The running OMA appliance returned an invalid onboarding resume response");
|
|
327
|
+
}
|
|
328
|
+
const url = `${state.base_url}/console/#bootstrap=${encodeURIComponent(body.nonce)}&session=${encodeURIComponent(state.session_id)}`;
|
|
329
|
+
const opened = openBrowserImpl(url);
|
|
330
|
+
if (!opened && !json)
|
|
331
|
+
terminal.warn(`Browser launch failed. Open this local URL manually: ${url}`);
|
|
332
|
+
return { sessionId: state.session_id };
|
|
333
|
+
}
|
|
334
|
+
function writeResumeState(env, state) {
|
|
335
|
+
const path = resumeStatePath(env);
|
|
336
|
+
const directory = dirname(path);
|
|
337
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
338
|
+
const directoryStat = lstatSync(directory);
|
|
339
|
+
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) {
|
|
340
|
+
throw new Error(`OMA onboarding state directory is unsafe: ${directory}`);
|
|
341
|
+
}
|
|
342
|
+
const temporary = `${path}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
|
|
343
|
+
try {
|
|
344
|
+
writeFileSync(temporary, `${JSON.stringify(state)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
345
|
+
renameSync(temporary, path);
|
|
346
|
+
chmodSync(path, 0o600);
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
rmSync(temporary, { force: true });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function readResumeState(env) {
|
|
353
|
+
const path = resumeStatePath(env);
|
|
354
|
+
if (!existsSync(path))
|
|
355
|
+
return undefined;
|
|
356
|
+
const stat = lstatSync(path);
|
|
357
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) {
|
|
358
|
+
throw new Error(`OMA onboarding resume state is unsafe: ${path} must be a private regular file`);
|
|
359
|
+
}
|
|
360
|
+
let value;
|
|
361
|
+
try {
|
|
362
|
+
value = JSON.parse(readFileSync(path, "utf8"));
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
throw new Error(`OMA onboarding resume state is invalid: ${path}`);
|
|
366
|
+
}
|
|
367
|
+
if (value.schema_version !== 1
|
|
368
|
+
|| typeof value.pid !== "number"
|
|
369
|
+
|| typeof value.base_url !== "string"
|
|
370
|
+
|| typeof value.session_id !== "string"
|
|
371
|
+
|| typeof value.control_token !== "string") {
|
|
372
|
+
throw new Error(`OMA onboarding resume state is invalid: ${path}`);
|
|
373
|
+
}
|
|
374
|
+
const url = new URL(value.base_url);
|
|
375
|
+
if (url.protocol !== "http:" || !isLoopbackHostname(url.hostname) || url.username || url.password) {
|
|
376
|
+
throw new Error(`OMA onboarding resume state has an unsafe appliance URL: ${value.base_url}`);
|
|
377
|
+
}
|
|
378
|
+
return value;
|
|
379
|
+
}
|
|
380
|
+
function removeResumeState(env, expectedControlToken) {
|
|
381
|
+
const path = resumeStatePath(env);
|
|
382
|
+
if (!existsSync(path))
|
|
383
|
+
return;
|
|
384
|
+
try {
|
|
385
|
+
const current = readResumeState(env);
|
|
386
|
+
if (current?.control_token === expectedControlToken)
|
|
387
|
+
rmSync(path, { force: true });
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
// Never delete a state file that failed validation or may belong to a
|
|
391
|
+
// newer onboarding process.
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function resumeStatePath(env) {
|
|
395
|
+
return join(env.OMA_HOME?.trim() || join(homedir(), ".oma"), "onboarding-resume.json");
|
|
396
|
+
}
|
|
397
|
+
function isLoopbackHostname(hostname) {
|
|
398
|
+
return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "[::1]" || hostname === "::1";
|
|
399
|
+
}
|
|
400
|
+
function openBrowser(url) {
|
|
401
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
402
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
403
|
+
return spawnSync(command, args, { stdio: "ignore" }).status === 0;
|
|
404
|
+
}
|
|
405
|
+
function waitForShutdownSignal() {
|
|
406
|
+
return new Promise((resolve) => {
|
|
407
|
+
const done = () => {
|
|
408
|
+
process.off("SIGINT", done);
|
|
409
|
+
process.off("SIGTERM", done);
|
|
410
|
+
resolve();
|
|
411
|
+
};
|
|
412
|
+
process.once("SIGINT", done);
|
|
413
|
+
process.once("SIGTERM", done);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
function createClackTerminal() {
|
|
417
|
+
return {
|
|
418
|
+
intro: p.intro,
|
|
419
|
+
step: (message) => p.log.success(message),
|
|
420
|
+
warn: (message) => p.log.warn(message),
|
|
421
|
+
error: (message) => p.log.error(message),
|
|
422
|
+
outro: p.outro,
|
|
423
|
+
cancel: p.cancel,
|
|
424
|
+
async select(options) {
|
|
425
|
+
const selected = await p.select(options);
|
|
426
|
+
return p.isCancel(selected) ? ONBOARD_CANCELLED : selected;
|
|
427
|
+
},
|
|
428
|
+
async confirm(options) {
|
|
429
|
+
const confirmed = await p.confirm(options);
|
|
430
|
+
return p.isCancel(confirmed) ? ONBOARD_CANCELLED : confirmed;
|
|
431
|
+
},
|
|
432
|
+
async password(options) {
|
|
433
|
+
const key = await p.password({
|
|
434
|
+
...options,
|
|
435
|
+
validate: (value) => options.validate(value ?? ""),
|
|
436
|
+
});
|
|
437
|
+
return p.isCancel(key) ? ONBOARD_CANCELLED : key;
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function parseOnboardArgs(argv) {
|
|
442
|
+
const options = { stdin: false, json: false, pull: false, sandbox: "docker-local" };
|
|
443
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
444
|
+
const arg = argv[index];
|
|
445
|
+
if (arg === "--provider") {
|
|
446
|
+
const provider = argv[++index];
|
|
447
|
+
if (provider === undefined || provider.startsWith("-"))
|
|
448
|
+
throw new Error("--provider requires a provider name");
|
|
449
|
+
options.provider = provider;
|
|
450
|
+
}
|
|
451
|
+
else if (arg === "--stdin") {
|
|
452
|
+
options.stdin = true;
|
|
453
|
+
}
|
|
454
|
+
else if (arg === "--json") {
|
|
455
|
+
options.json = true;
|
|
456
|
+
}
|
|
457
|
+
else if (arg === "--pull") {
|
|
458
|
+
options.pull = true;
|
|
459
|
+
}
|
|
460
|
+
else if (arg === "--sandbox") {
|
|
461
|
+
const sandbox = argv[++index];
|
|
462
|
+
if (sandbox === "docker" || sandbox === "docker-local")
|
|
463
|
+
options.sandbox = "docker-local";
|
|
464
|
+
else if (sandbox === "microsandbox" || sandbox === "microsandbox-local")
|
|
465
|
+
options.sandbox = "microsandbox-local";
|
|
466
|
+
else
|
|
467
|
+
throw new Error("--sandbox requires docker or microsandbox");
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
throw new Error(`Unknown option for oma onboard: ${arg}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return options;
|
|
474
|
+
}
|
|
475
|
+
function onboardingPreflightFailures(report, sandbox, options = {}) {
|
|
476
|
+
const required = ["node.version", "models.catalog", "sandbox.runtime", "server.port"];
|
|
477
|
+
if (sandbox === "docker-local" && !options.allowMissingImage)
|
|
478
|
+
required.push("sandbox.image");
|
|
479
|
+
const failures = report.checks
|
|
480
|
+
.filter((check) => required.includes(check.id) && check.status !== "pass")
|
|
481
|
+
.map((check) => check.summary);
|
|
482
|
+
if (sandbox === "docker-local" && !options.allowMissingImage && report.checks.find((check) => check.id === "sandbox.image")?.status !== "pass") {
|
|
483
|
+
failures.push(`Pull the cached prerequisite image, then retry: docker pull ${DEFAULT_OMA_SANDBOX_IMAGE}`);
|
|
484
|
+
}
|
|
485
|
+
return [...new Set(failures)];
|
|
486
|
+
}
|
|
487
|
+
function parseProviderStatus(output) {
|
|
488
|
+
const rows = output.trim().split("\n").slice(1).filter(Boolean);
|
|
489
|
+
return rows.map((row) => row.split("\t", 1)[0]).filter(Boolean);
|
|
490
|
+
}
|
|
491
|
+
function parseStoredCredential(output, provider) {
|
|
492
|
+
const row = output.trim().split("\n").slice(1).find((line) => line.startsWith(`${provider}\t`));
|
|
493
|
+
if (row === undefined)
|
|
494
|
+
throw new Error(`Provider ${provider} was not returned by auth status`);
|
|
495
|
+
return row.split("\t")[1] === "true";
|
|
496
|
+
}
|
|
497
|
+
async function readOneSecretFromStdin() {
|
|
498
|
+
let value = "";
|
|
499
|
+
input.setEncoding("utf8");
|
|
500
|
+
for await (const chunk of input)
|
|
501
|
+
value += chunk;
|
|
502
|
+
value = value.replace(/\r?\n$/, "");
|
|
503
|
+
if (value.includes("\n") || value.includes("\r"))
|
|
504
|
+
throw new Error("--stdin accepts exactly one API key value");
|
|
505
|
+
return value;
|
|
506
|
+
}
|
|
507
|
+
function cancelled(runtime, json, report) {
|
|
508
|
+
if (json)
|
|
509
|
+
console.log(JSON.stringify({ schema_version: 1, status: "cancelled" }));
|
|
510
|
+
else
|
|
511
|
+
runtime.terminal.cancel("Onboarding cancelled before changing OMA credentials.");
|
|
512
|
+
return { code: 130, credential: "not_reached", report };
|
|
513
|
+
}
|
|
514
|
+
function emitFailure(runtime, json, message) {
|
|
515
|
+
if (json)
|
|
516
|
+
console.log(JSON.stringify({ schema_version: 1, status: "failed", error: message }));
|
|
517
|
+
else
|
|
518
|
+
runtime.terminal.error(message);
|
|
519
|
+
}
|
|
520
|
+
function emitSuccess(runtime, json, provider, credential, sessionId) {
|
|
521
|
+
if (json) {
|
|
522
|
+
console.log(JSON.stringify({ schema_version: 1, status: "ready", provider, credential, session_id: sessionId }));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const verb = credential === "stored" ? "stored securely" : "already stored";
|
|
526
|
+
runtime.terminal.step(`Credential ${verb}.`);
|
|
527
|
+
}
|
|
528
|
+
function emitResumed(runtime, json, sessionId) {
|
|
529
|
+
if (json) {
|
|
530
|
+
console.log(JSON.stringify({ schema_version: 1, status: "ready", resumed: true, session_id: sessionId }));
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
runtime.terminal.outro("Existing local OMA session reopened.");
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function safeMessage(error) {
|
|
537
|
+
return error instanceof Error ? error.message : String(error);
|
|
538
|
+
}
|
|
539
|
+
async function main() {
|
|
540
|
+
const result = await runOnboard(process.argv.slice(2), createProcessOnboardRuntime());
|
|
541
|
+
process.exitCode = result.code;
|
|
542
|
+
}
|
|
543
|
+
if (import.meta.main)
|
|
544
|
+
void main();
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { MANAGED_AGENTS_BETA } from "../src/control-plane/api-constants.js";
|
|
3
|
+
const MARKER = "oma.onboarding";
|
|
4
|
+
const MARKER_VERSION = "starter-v1";
|
|
5
|
+
export async function ensureStarterResources(input) {
|
|
6
|
+
const request = createApiRequest(input.baseUrl, input.workspaceKey, input.fetch ?? fetch);
|
|
7
|
+
const warnings = [];
|
|
8
|
+
const created = [];
|
|
9
|
+
const models = await listAll(request, `/v1/model-catalog?available=true&provider=${encodeURIComponent(input.provider)}`);
|
|
10
|
+
const model = models.find((item) => item.default) ?? models[0];
|
|
11
|
+
if (model === undefined)
|
|
12
|
+
throw new Error(`No credential-ready model is available for provider ${input.provider}`);
|
|
13
|
+
const agentRows = await listAll(request, "/v1/agents?include_archived=true");
|
|
14
|
+
const markedAgents = agentRows.filter((item) => hasMarker(item, "agent"));
|
|
15
|
+
let agent = markedAgents.find((item) => item.archived_at === null && item.model.provider === model.provider && item.model.id === model.id);
|
|
16
|
+
if (agent === undefined) {
|
|
17
|
+
if (markedAgents.length > 0)
|
|
18
|
+
warnings.push("The previous starter agent was archived or used a different model; a compatible starter agent was created.");
|
|
19
|
+
agent = await request("/v1/agents", {
|
|
20
|
+
method: "POST",
|
|
21
|
+
body: {
|
|
22
|
+
name: "OMA Starter",
|
|
23
|
+
model: { provider: model.provider, id: model.id },
|
|
24
|
+
description: "Local starter agent created by oma onboard.",
|
|
25
|
+
system: "You are the local Open Managed Agents starter. Help the operator explore this workspace clearly and safely.",
|
|
26
|
+
tools: [], skills: [], mcp_servers: [],
|
|
27
|
+
metadata: marker("agent"),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
created.push("agent");
|
|
31
|
+
}
|
|
32
|
+
const environmentRows = await listAll(request, "/v1/environments?include_archived=true");
|
|
33
|
+
const markedEnvironments = environmentRows.filter((item) => hasMarker(item, "environment"));
|
|
34
|
+
let environment = markedEnvironments.find((item) => item.archived_at === null && isOffline(item.config));
|
|
35
|
+
if (environment === undefined) {
|
|
36
|
+
if (markedEnvironments.length > 0)
|
|
37
|
+
warnings.push("The previous starter environment was archived or no longer offline; a new offline environment was created.");
|
|
38
|
+
environment = await request("/v1/environments", {
|
|
39
|
+
method: "POST",
|
|
40
|
+
body: {
|
|
41
|
+
name: "OMA Starter (offline)",
|
|
42
|
+
config: { type: "cloud", networking: { type: "limited", allowed_hosts: [] } },
|
|
43
|
+
metadata: marker("environment"),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
created.push("environment");
|
|
47
|
+
}
|
|
48
|
+
const sessionRows = await listAll(request, "/v1/sessions?include_archived=true");
|
|
49
|
+
const markedSessions = sessionRows.filter((item) => hasMarker(item, "session"));
|
|
50
|
+
let session = markedSessions.find((item) => item.archived_at === null && item.agent.id === agent.id && item.environment_id === environment.id);
|
|
51
|
+
if (session === undefined) {
|
|
52
|
+
if (markedSessions.length > 0)
|
|
53
|
+
warnings.push("The previous starter session was archived or referenced older starter resources; a new session was created.");
|
|
54
|
+
session = await request("/v1/sessions", {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "idempotency-key": idempotencyKey(agent.id, environment.id) },
|
|
57
|
+
body: {
|
|
58
|
+
agent: agent.id,
|
|
59
|
+
environment_id: environment.id,
|
|
60
|
+
title: "Your first OMA session",
|
|
61
|
+
metadata: marker("session"),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
created.push("session");
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
agentId: agent.id,
|
|
68
|
+
environmentId: environment.id,
|
|
69
|
+
sessionId: session.id,
|
|
70
|
+
model: { provider: model.provider, id: model.id },
|
|
71
|
+
created,
|
|
72
|
+
warnings,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function marker(resource) {
|
|
76
|
+
return { [MARKER]: MARKER_VERSION, "oma.onboarding.resource": resource };
|
|
77
|
+
}
|
|
78
|
+
function hasMarker(resource, kind) {
|
|
79
|
+
return resource.metadata?.[MARKER] === MARKER_VERSION && resource.metadata["oma.onboarding.resource"] === kind;
|
|
80
|
+
}
|
|
81
|
+
function isOffline(config) {
|
|
82
|
+
const networking = config.networking;
|
|
83
|
+
return networking?.type === "limited" && Array.isArray(networking.allowed_hosts) && networking.allowed_hosts.length === 0;
|
|
84
|
+
}
|
|
85
|
+
function idempotencyKey(agentId, environmentId) {
|
|
86
|
+
return `oma-onboard-${createHash("sha256").update(`${MARKER_VERSION}:${agentId}:${environmentId}`).digest("hex").slice(0, 32)}`;
|
|
87
|
+
}
|
|
88
|
+
function createApiRequest(baseUrl, workspaceKey, fetchImpl) {
|
|
89
|
+
return async function request(path, options = {}) {
|
|
90
|
+
const response = await fetchImpl(new URL(path, baseUrl), {
|
|
91
|
+
method: options.method ?? "GET",
|
|
92
|
+
headers: {
|
|
93
|
+
accept: "application/json",
|
|
94
|
+
"anthropic-beta": MANAGED_AGENTS_BETA,
|
|
95
|
+
"x-api-key": workspaceKey,
|
|
96
|
+
...(options.body === undefined ? {} : { "content-type": "application/json" }),
|
|
97
|
+
...options.headers,
|
|
98
|
+
},
|
|
99
|
+
...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }),
|
|
100
|
+
});
|
|
101
|
+
const body = await response.json().catch(() => undefined);
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
const message = typeof body === "object" && body !== null && "error" in body
|
|
104
|
+
? body.error?.message
|
|
105
|
+
: undefined;
|
|
106
|
+
throw new Error(message ?? `OMA API ${options.method ?? "GET"} ${path} failed with HTTP ${response.status}`);
|
|
107
|
+
}
|
|
108
|
+
return body;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function listAll(request, initialPath) {
|
|
112
|
+
const data = [];
|
|
113
|
+
let path = addLimit(initialPath);
|
|
114
|
+
for (let pageCount = 0; path !== undefined && pageCount < 100; pageCount += 1) {
|
|
115
|
+
const page = await request(path);
|
|
116
|
+
data.push(...page.data);
|
|
117
|
+
path = page.has_more && page.next_page ? addPage(initialPath, page.next_page) : undefined;
|
|
118
|
+
}
|
|
119
|
+
return data;
|
|
120
|
+
}
|
|
121
|
+
function addLimit(path) {
|
|
122
|
+
const url = new URL(path, "http://oma.local");
|
|
123
|
+
url.searchParams.set("limit", "100");
|
|
124
|
+
return `${url.pathname}${url.search}`;
|
|
125
|
+
}
|
|
126
|
+
function addPage(path, page) {
|
|
127
|
+
const url = new URL(path, "http://oma.local");
|
|
128
|
+
url.searchParams.set("limit", "100");
|
|
129
|
+
url.searchParams.set("page", page);
|
|
130
|
+
return `${url.pathname}${url.search}`;
|
|
131
|
+
}
|
|
@@ -274,6 +274,32 @@ function registerConsoleSessionRoutes(app, services) {
|
|
|
274
274
|
c.header("cache-control", "no-store");
|
|
275
275
|
return c.json({ workspace: consoleWorkspace(session.workspaceId, consoleAuth.service) });
|
|
276
276
|
});
|
|
277
|
+
if (consoleAuth.bootstrap !== undefined) {
|
|
278
|
+
app.post("/console/auth/bootstrap/renew", async (c) => {
|
|
279
|
+
const controlToken = c.req.header("x-oma-onboarding-token");
|
|
280
|
+
if (controlToken === undefined)
|
|
281
|
+
return consoleAuthenticationFailed(c);
|
|
282
|
+
const nonce = consoleAuth.bootstrap.issueForControlToken(controlToken);
|
|
283
|
+
if (nonce === undefined)
|
|
284
|
+
return consoleAuthenticationFailed(c);
|
|
285
|
+
c.header("cache-control", "no-store");
|
|
286
|
+
return c.json({ nonce });
|
|
287
|
+
});
|
|
288
|
+
app.post("/console/auth/bootstrap", async (c) => {
|
|
289
|
+
if (!sameOriginForCookieWrite(c))
|
|
290
|
+
return c.text("Forbidden\n", 403);
|
|
291
|
+
const nonce = stringField(await parseJsonBody(c.req), "nonce");
|
|
292
|
+
const workspaceKey = consoleAuth.bootstrap.consume(nonce);
|
|
293
|
+
if (workspaceKey === undefined)
|
|
294
|
+
return consoleAuthenticationFailed(c);
|
|
295
|
+
const session = consoleAuth.service.createWorkspaceSession(workspaceKey);
|
|
296
|
+
if (session === undefined)
|
|
297
|
+
return consoleAuthenticationFailed(c);
|
|
298
|
+
setConsoleCookie(c, CONSOLE_WORKSPACE_COOKIE, session.token, consoleAuth.secureCookies, 30 * 24 * 60 * 60);
|
|
299
|
+
c.header("cache-control", "no-store");
|
|
300
|
+
return c.json({ workspace: consoleWorkspace(session.workspaceId, consoleAuth.service) });
|
|
301
|
+
});
|
|
302
|
+
}
|
|
277
303
|
app.post("/console/auth/admin", async (c) => {
|
|
278
304
|
if (!sameOriginForCookieWrite(c) || services.admin === undefined)
|
|
279
305
|
return c.text("Not found\n", 404);
|
|
@@ -359,6 +385,12 @@ export function createDeploymentControlPlaneApp(env = process.env, opts = {}) {
|
|
|
359
385
|
// tests that boot twice against one OMA_HOME) can close them and release the
|
|
360
386
|
// .oma.lock instead of leaking them until process exit.
|
|
361
387
|
export function createDeploymentControlPlane(env = process.env, opts = {}, internal = {}) {
|
|
388
|
+
if (opts.consoleBootstrap !== undefined && !isLoopbackHost(env.OMA_HOST)) {
|
|
389
|
+
throw new Error("Console bootstrap is available only on a loopback appliance bind");
|
|
390
|
+
}
|
|
391
|
+
if (opts.consoleBootstrap !== undefined && env.OMA_TLS_TERMINATED === "1") {
|
|
392
|
+
throw new Error("Console bootstrap is not available behind TLS termination");
|
|
393
|
+
}
|
|
362
394
|
const runtimeConfig = parseDeploymentRuntimeConfigFromEnv(env);
|
|
363
395
|
const modelConfig = parseModelDeploymentConfigFromEnv(env);
|
|
364
396
|
const authMode = parseDeploymentAuthMode(env);
|
|
@@ -642,6 +674,7 @@ export function createDeploymentControlPlane(env = process.env, opts = {}, inter
|
|
|
642
674
|
consoleSessionAuth: {
|
|
643
675
|
service: consoleSessionAuth,
|
|
644
676
|
secureCookies: tlsTerminated,
|
|
677
|
+
...(opts.consoleBootstrap === undefined ? {} : { bootstrap: opts.consoleBootstrap }),
|
|
645
678
|
},
|
|
646
679
|
...(openapiRoot === undefined ? {} : { openapi: { root: openapiRoot } }),
|
|
647
680
|
...(authMode === "api-key"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
const DEFAULT_TTL_MS = 10 * 60 * 1000;
|
|
3
|
+
/**
|
|
4
|
+
* Process-local bearer grants used only to exchange the onboarding browser
|
|
5
|
+
* handoff for the ordinary opaque console cookie. Grants are never persisted,
|
|
6
|
+
* are stored by hash, and are removed before their workspace key is returned.
|
|
7
|
+
*/
|
|
8
|
+
export class ConsoleBootstrapService {
|
|
9
|
+
now;
|
|
10
|
+
ttlMs;
|
|
11
|
+
grants = new Map();
|
|
12
|
+
authority;
|
|
13
|
+
constructor(now = Date.now, ttlMs = DEFAULT_TTL_MS) {
|
|
14
|
+
this.now = now;
|
|
15
|
+
this.ttlMs = ttlMs;
|
|
16
|
+
}
|
|
17
|
+
issue(workspaceKey) {
|
|
18
|
+
const nonce = `ocb_${randomBytes(32).toString("base64url")}`;
|
|
19
|
+
this.grants.set(hash(nonce), {
|
|
20
|
+
workspaceKey,
|
|
21
|
+
expiresAt: this.now() + this.ttlMs,
|
|
22
|
+
});
|
|
23
|
+
return nonce;
|
|
24
|
+
}
|
|
25
|
+
bindResumeAuthority(workspaceKey, controlToken) {
|
|
26
|
+
this.authority = {
|
|
27
|
+
workspaceKey,
|
|
28
|
+
controlTokenHash: digest(controlToken),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
issueForControlToken(controlToken) {
|
|
32
|
+
if (this.authority === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
const candidate = digest(controlToken);
|
|
35
|
+
if (!timingSafeEqual(candidate, this.authority.controlTokenHash))
|
|
36
|
+
return undefined;
|
|
37
|
+
return this.issue(this.authority.workspaceKey);
|
|
38
|
+
}
|
|
39
|
+
consume(nonce) {
|
|
40
|
+
const key = hash(nonce);
|
|
41
|
+
const grant = this.grants.get(key);
|
|
42
|
+
// Delete before validating/returning so every attempted consumption is
|
|
43
|
+
// terminal, including an expired grant or an invalid downstream key.
|
|
44
|
+
this.grants.delete(key);
|
|
45
|
+
if (grant === undefined || grant.expiresAt <= this.now())
|
|
46
|
+
return undefined;
|
|
47
|
+
return grant.workspaceKey;
|
|
48
|
+
}
|
|
49
|
+
clear() {
|
|
50
|
+
this.grants.clear();
|
|
51
|
+
this.authority = undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function hash(value) {
|
|
55
|
+
return createHash("sha256").update(value).digest("hex");
|
|
56
|
+
}
|
|
57
|
+
function digest(value) {
|
|
58
|
+
return createHash("sha256").update(value).digest();
|
|
59
|
+
}
|
|
@@ -18,6 +18,7 @@ export class DefaultEnvironmentService {
|
|
|
18
18
|
type: "environment",
|
|
19
19
|
name: req.name,
|
|
20
20
|
config: req.config,
|
|
21
|
+
metadata: req.metadata ?? {},
|
|
21
22
|
created_at: now,
|
|
22
23
|
updated_at: now,
|
|
23
24
|
archived_at: null,
|
|
@@ -83,7 +84,7 @@ function parseCreateEnvironment(input) {
|
|
|
83
84
|
}
|
|
84
85
|
throw error;
|
|
85
86
|
}
|
|
86
|
-
return { name, config: canonicalConfig };
|
|
87
|
+
return { name, config: canonicalConfig, metadata: metadataField(obj) };
|
|
87
88
|
}
|
|
88
89
|
function toManagedEnvironment(row) {
|
|
89
90
|
return {
|
|
@@ -91,11 +92,26 @@ function toManagedEnvironment(row) {
|
|
|
91
92
|
type: row.type,
|
|
92
93
|
name: row.name,
|
|
93
94
|
config: row.config,
|
|
95
|
+
metadata: row.metadata ?? {},
|
|
94
96
|
created_at: row.created_at,
|
|
95
97
|
updated_at: row.updated_at,
|
|
96
98
|
archived_at: row.archived_at,
|
|
97
99
|
};
|
|
98
100
|
}
|
|
101
|
+
function metadataField(obj) {
|
|
102
|
+
const value = obj.metadata;
|
|
103
|
+
if (value === undefined)
|
|
104
|
+
return undefined;
|
|
105
|
+
if (!isJsonObject(value))
|
|
106
|
+
throw invalidRequest("`metadata` must be an object");
|
|
107
|
+
const metadata = {};
|
|
108
|
+
for (const [key, item] of Object.entries(value)) {
|
|
109
|
+
if (typeof item !== "string")
|
|
110
|
+
throw invalidRequest("`metadata` values must be strings");
|
|
111
|
+
metadata[key] = item;
|
|
112
|
+
}
|
|
113
|
+
return metadata;
|
|
114
|
+
}
|
|
99
115
|
function objectInput(input) {
|
|
100
116
|
if (!isJsonObject(input)) {
|
|
101
117
|
throw invalidRequest("Request body must be a JSON object");
|
|
@@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS environments (
|
|
|
6
6
|
type TEXT NOT NULL,
|
|
7
7
|
name TEXT NOT NULL,
|
|
8
8
|
config TEXT NOT NULL,
|
|
9
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
9
10
|
created_at TEXT NOT NULL,
|
|
10
11
|
updated_at TEXT NOT NULL,
|
|
11
12
|
archived_at TEXT
|
|
@@ -26,9 +27,10 @@ export class SqliteEnvironmentStore {
|
|
|
26
27
|
constructor(db) {
|
|
27
28
|
this.db = db;
|
|
28
29
|
this.db.exec(SCHEMA);
|
|
30
|
+
migrateMetadata(this.db);
|
|
29
31
|
this.insertStmt = this.db.prepare(`INSERT INTO environments (
|
|
30
|
-
id, workspace_id, type, name, config, created_at, updated_at, archived_at
|
|
31
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
32
|
+
id, workspace_id, type, name, config, metadata, created_at, updated_at, archived_at
|
|
33
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
32
34
|
this.retrieveActiveStmt = this.db.prepare(`SELECT * FROM environments
|
|
33
35
|
WHERE workspace_id = ? AND id = ? AND archived_at IS NULL`);
|
|
34
36
|
this.retrieveAnyStmt = this.db.prepare(`SELECT * FROM environments WHERE workspace_id = ? AND id = ?`);
|
|
@@ -59,7 +61,7 @@ export class SqliteEnvironmentStore {
|
|
|
59
61
|
}
|
|
60
62
|
create(record) {
|
|
61
63
|
const e = record.row;
|
|
62
|
-
this.insertStmt.run(e.id, e.workspace_id, e.type, e.name, JSON.stringify(e.config), e.created_at, e.updated_at, e.archived_at);
|
|
64
|
+
this.insertStmt.run(e.id, e.workspace_id, e.type, e.name, JSON.stringify(e.config), JSON.stringify(e.metadata ?? {}), e.created_at, e.updated_at, e.archived_at);
|
|
63
65
|
return e;
|
|
64
66
|
}
|
|
65
67
|
retrieve(workspaceId, environmentId) {
|
|
@@ -125,8 +127,15 @@ function deserialize(row) {
|
|
|
125
127
|
type: "environment",
|
|
126
128
|
name: row.name,
|
|
127
129
|
config: JSON.parse(row.config),
|
|
130
|
+
metadata: JSON.parse(row.metadata),
|
|
128
131
|
created_at: row.created_at,
|
|
129
132
|
updated_at: row.updated_at,
|
|
130
133
|
archived_at: row.archived_at,
|
|
131
134
|
};
|
|
132
135
|
}
|
|
136
|
+
function migrateMetadata(db) {
|
|
137
|
+
const columns = db.prepare("PRAGMA table_info(environments)").all();
|
|
138
|
+
if (!columns.some((column) => column.name === "metadata")) {
|
|
139
|
+
db.exec("ALTER TABLE environments ADD COLUMN metadata TEXT NOT NULL DEFAULT '{}'");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -167,8 +167,8 @@ const schemas = {
|
|
|
167
167
|
CreateAgentRequest: { type: "object", required: ["name", "model"], properties: { name: { type: "string", minLength: 1 }, model: ref("ModelInput"), system: ref("NullableString"), description: ref("NullableString"), tools: { type: "array", items: ref("JsonObject") }, skills: { type: "array", items: ref("JsonObject") }, mcp_servers: { type: "array", items: ref("JsonObject") }, metadata: ref("StringMap"), multiagent: { type: "null", description: "Full multi-agent runtime is not implemented. Omit this field or send null." } }, additionalProperties: false },
|
|
168
168
|
UpdateAgentRequest: { type: "object", required: ["version"], properties: { version: { type: "integer", minimum: 1 }, name: { type: "string", minLength: 1 }, model: ref("ModelInput"), system: ref("NullableString"), description: ref("NullableString"), tools: { type: ["array", "null"], items: ref("JsonObject") }, skills: { type: ["array", "null"], items: ref("JsonObject") }, mcp_servers: { type: ["array", "null"], items: ref("JsonObject") }, metadata: { type: "object", additionalProperties: { type: ["string", "null"] } }, multiagent: { type: "null", description: "Full multi-agent runtime is not implemented. Omit this field or send null." } }, additionalProperties: false },
|
|
169
169
|
Agent: { type: "object", required: ["id", "type", "name", "model", "version", "created_at", "updated_at", "archived_at"], properties: { id: { type: "string" }, type: { const: "agent" }, name: { type: "string" }, model: { type: "object", required: ["provider", "id", "speed"], properties: { provider: { type: "string" }, id: { type: "string" }, speed: { enum: ["standard", "fast"] } }, additionalProperties: false }, system: ref("NullableString"), description: ref("NullableString"), tools: { type: "array", items: ref("JsonObject") }, skills: { type: "array", items: ref("JsonObject") }, mcp_servers: { type: "array", items: ref("JsonObject") }, metadata: ref("StringMap"), multiagent: { type: ["object", "null"] }, version: { type: "integer", minimum: 1 }, created_at: { type: "string", format: "date-time" }, updated_at: { type: "string", format: "date-time" }, archived_at: { type: ["string", "null"], format: "date-time" } }, additionalProperties: false },
|
|
170
|
-
CreateEnvironmentRequest: { type: "object", required: ["name", "config"], properties: { name: { type: "string", minLength: 1 }, config: ref("JsonObject") }, additionalProperties: false },
|
|
171
|
-
Environment: { type: "object", required: ["id", "type", "name", "config", "created_at", "updated_at", "archived_at"], properties: { id: { type: "string" }, type: { const: "environment" }, name: { type: "string" }, config: ref("JsonObject"), created_at: { type: "string", format: "date-time" }, updated_at: { type: "string", format: "date-time" }, archived_at: { type: ["string", "null"], format: "date-time" } } },
|
|
170
|
+
CreateEnvironmentRequest: { type: "object", required: ["name", "config"], properties: { name: { type: "string", minLength: 1 }, config: ref("JsonObject"), metadata: ref("StringMap") }, additionalProperties: false },
|
|
171
|
+
Environment: { type: "object", required: ["id", "type", "name", "config", "metadata", "created_at", "updated_at", "archived_at"], properties: { id: { type: "string" }, type: { const: "environment" }, name: { type: "string" }, config: ref("JsonObject"), metadata: ref("StringMap"), created_at: { type: "string", format: "date-time" }, updated_at: { type: "string", format: "date-time" }, archived_at: { type: ["string", "null"], format: "date-time" } } },
|
|
172
172
|
DeletedEnvironment: { type: "object", required: ["type", "id"], properties: { type: { const: "environment_deleted" }, id: { type: "string" } }, additionalProperties: false },
|
|
173
173
|
EnvironmentNetworkingPresetCatalog: { type: "object", required: ["type", "deployment", "presets", "custom"], properties: { type: { const: "environment_networking_presets" }, deployment: { type: "object", required: ["provider", "egress_supported", "reason"], properties: { provider: { type: ["string", "null"] }, egress_supported: { type: "boolean" }, reason: { type: ["string", "null"] } }, additionalProperties: false }, presets: { type: "array", items: { type: "object", required: ["id", "version", "name", "description", "networking"], properties: { id: { type: "string" }, version: { type: "integer", minimum: 1 }, name: { type: "string" }, description: { type: "string" }, networking: { type: "object", required: ["type", "allowed_hosts"], properties: { type: { const: "limited" }, allowed_hosts: { type: "array", items: { type: "string" } } }, additionalProperties: false } }, additionalProperties: false } }, custom: { type: "object", required: ["https_only", "wildcard_matches_bare_domain"], properties: { https_only: { const: true }, wildcard_matches_bare_domain: { const: false } }, additionalProperties: false } }, additionalProperties: false },
|
|
174
174
|
ValidateEnvironmentNetworkingHostsRequest: { type: "object", required: ["allowed_hosts"], properties: { allowed_hosts: { type: "array", items: { type: "string" } } }, additionalProperties: false },
|
package/dist/src/main.js
CHANGED
|
@@ -50,7 +50,9 @@ export async function startAppliance(env = process.env, opts = {}) {
|
|
|
50
50
|
const resolved = resolveApplianceEnv(env);
|
|
51
51
|
const port = parseAppliancePort(resolved.OMA_PORT);
|
|
52
52
|
const host = resolved.OMA_HOST ?? "127.0.0.1";
|
|
53
|
-
const plane = createDeploymentControlPlane(resolved
|
|
53
|
+
const plane = createDeploymentControlPlane(resolved, {
|
|
54
|
+
...(opts.onboarding === undefined ? {} : { consoleBootstrap: opts.onboarding.bootstrap }),
|
|
55
|
+
});
|
|
54
56
|
const { app, stores, authMode } = plane;
|
|
55
57
|
let server;
|
|
56
58
|
try {
|
|
@@ -70,7 +72,13 @@ export async function startAppliance(env = process.env, opts = {}) {
|
|
|
70
72
|
// skip minting — an unprinted key locking the operator out. A key that
|
|
71
73
|
// prints but never serves only costs a retry; the reverse costs the
|
|
72
74
|
// quickstart.
|
|
73
|
-
const
|
|
75
|
+
const onboardingKey = authMode === "api-key" && opts.onboarding !== undefined
|
|
76
|
+
? mintOnboardingKey(stores.workspaces)
|
|
77
|
+
: undefined;
|
|
78
|
+
if (onboardingKey !== undefined) {
|
|
79
|
+
opts.onboarding.bootstrap.bindResumeAuthority(onboardingKey.plaintextKey, opts.onboarding.controlToken);
|
|
80
|
+
}
|
|
81
|
+
const minted = onboardingKey === undefined && authMode === "api-key" && stores.workspaces.countApiKeys() === 0
|
|
74
82
|
? stores.workspaces.mintKey(DEFAULT_WORKSPACE_ID, "first-boot")
|
|
75
83
|
: undefined;
|
|
76
84
|
const baseUrl = `http://${host}:${boundPort}`;
|
|
@@ -93,13 +101,26 @@ export async function startAppliance(env = process.env, opts = {}) {
|
|
|
93
101
|
}
|
|
94
102
|
return {
|
|
95
103
|
port: boundPort,
|
|
104
|
+
baseUrl,
|
|
105
|
+
...(onboardingKey === undefined
|
|
106
|
+
? {}
|
|
107
|
+
: {
|
|
108
|
+
onboarding: {
|
|
109
|
+
workspaceKey: onboardingKey.plaintextKey,
|
|
110
|
+
bootstrapNonce: opts.onboarding.bootstrap.issue(onboardingKey.plaintextKey),
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
96
113
|
close: async () => {
|
|
114
|
+
opts.onboarding?.bootstrap.clear();
|
|
115
|
+
if (onboardingKey !== undefined)
|
|
116
|
+
stores.workspaces.revokeKey(onboardingKey.keySha256);
|
|
97
117
|
await closeServer(bound.server);
|
|
98
118
|
await plane.close();
|
|
99
119
|
},
|
|
100
120
|
};
|
|
101
121
|
}
|
|
102
122
|
catch (error) {
|
|
123
|
+
opts.onboarding?.bootstrap.clear();
|
|
103
124
|
// Failed startups must release everything (notably .oma.lock) so the
|
|
104
125
|
// operator's retry is a clean boot, not a lock error.
|
|
105
126
|
if (server !== undefined) {
|
|
@@ -109,6 +130,14 @@ export async function startAppliance(env = process.env, opts = {}) {
|
|
|
109
130
|
throw error;
|
|
110
131
|
}
|
|
111
132
|
}
|
|
133
|
+
function mintOnboardingKey(workspaces) {
|
|
134
|
+
for (const key of workspaces.listKeys(DEFAULT_WORKSPACE_ID)) {
|
|
135
|
+
if (key.label === "onboarding-console" && key.revoked_at === null) {
|
|
136
|
+
workspaces.revokeKey(key.key_sha256);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return workspaces.mintKey(DEFAULT_WORKSPACE_ID, "onboarding-console");
|
|
140
|
+
}
|
|
112
141
|
function closeServer(server) {
|
|
113
142
|
return new Promise((resolvePromise, reject) => {
|
|
114
143
|
server.close((err) => (err ? reject(err) : resolvePromise()));
|
|
@@ -299,6 +299,10 @@ export function loginConsoleWorkspace(apiKey) {
|
|
|
299
299
|
return request("/console/auth/workspace", { method: "POST", body: { api_key: apiKey } });
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
export function consumeConsoleBootstrap(nonce) {
|
|
303
|
+
return request("/console/auth/bootstrap", { method: "POST", body: { nonce } });
|
|
304
|
+
}
|
|
305
|
+
|
|
302
306
|
export function loginConsoleAdmin(adminKey) {
|
|
303
307
|
return request("/console/auth/admin", { method: "POST", body: { admin_key: adminKey } });
|
|
304
308
|
}
|
|
@@ -707,6 +711,9 @@ function toUiEnvironment(environment) {
|
|
|
707
711
|
allowedHosts,
|
|
708
712
|
created: shortDate(environment.created_at),
|
|
709
713
|
config: environment.config ?? {},
|
|
714
|
+
metadata: environment.metadata && typeof environment.metadata === "object"
|
|
715
|
+
? environment.metadata
|
|
716
|
+
: {},
|
|
710
717
|
archived: Boolean(environment.archived_at),
|
|
711
718
|
};
|
|
712
719
|
}
|
|
@@ -1010,6 +1017,7 @@ if (typeof window !== "undefined") {
|
|
|
1010
1017
|
revokeKey,
|
|
1011
1018
|
getConsoleAuthStatus,
|
|
1012
1019
|
loginConsoleWorkspace,
|
|
1020
|
+
consumeConsoleBootstrap,
|
|
1013
1021
|
loginConsoleAdmin,
|
|
1014
1022
|
selectConsoleWorkspace,
|
|
1015
1023
|
logoutConsole,
|
|
@@ -127,6 +127,19 @@ function readRouteTarget(sessions, agents) {
|
|
|
127
127
|
return null;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
function takeBootstrapHandoff() {
|
|
131
|
+
const rawHash = window.location.hash.replace(/^#/, '');
|
|
132
|
+
const params = new URLSearchParams(rawHash);
|
|
133
|
+
const nonce = params.get('bootstrap');
|
|
134
|
+
if (!nonce) return null;
|
|
135
|
+
params.delete('bootstrap');
|
|
136
|
+
const nextHash = params.toString();
|
|
137
|
+
// Clear the bearer value before the network exchange so reload, copy/paste,
|
|
138
|
+
// browser history, and later application code cannot recover it.
|
|
139
|
+
window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}${nextHash ? `#${nextHash}` : '#start'}`);
|
|
140
|
+
return nonce;
|
|
141
|
+
}
|
|
142
|
+
|
|
130
143
|
function App() {
|
|
131
144
|
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
|
|
132
145
|
const demoMode = new URLSearchParams(window.location.search).get('mode') === 'demo';
|
|
@@ -191,7 +204,12 @@ function App() {
|
|
|
191
204
|
if (target) setRoute(target);
|
|
192
205
|
return () => { alive = false; };
|
|
193
206
|
}
|
|
194
|
-
|
|
207
|
+
const bootstrapNonce = takeBootstrapHandoff();
|
|
208
|
+
const bootstrap = bootstrapNonce
|
|
209
|
+
? OmaConsoleApi.consumeConsoleBootstrap(bootstrapNonce)
|
|
210
|
+
: Promise.resolve(undefined);
|
|
211
|
+
bootstrap
|
|
212
|
+
.then(() => OmaConsoleApi.getConsoleAuthStatus())
|
|
195
213
|
.then((status) => {
|
|
196
214
|
if (!alive) return undefined;
|
|
197
215
|
setAuth({ phase: status.workspace || !status.auth_required || status.admin ? 'ready' : 'login', admin:status.admin, workspace:status.workspace, error:null, busy:false });
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-managed-agents",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Self-hostable implementation of Anthropic's Managed Agents API surface.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/oneryalcin/open-managed-agents.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/oneryalcin/open-managed-agents#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/oneryalcin/open-managed-agents/issues"
|
|
12
|
+
},
|
|
5
13
|
"type": "module",
|
|
6
14
|
"bin": {
|
|
7
15
|
"oma": "bin/oma.mjs"
|
|
@@ -21,6 +29,7 @@
|
|
|
21
29
|
"typecheck": "tsc --noEmit",
|
|
22
30
|
"build:package": "node scripts/package-build.mjs",
|
|
23
31
|
"package:verify": "node scripts/package-verify.mjs",
|
|
32
|
+
"npm:publish": "node scripts/npm-publish.mjs",
|
|
24
33
|
"prepack": "npm run build:package",
|
|
25
34
|
"alpha:smoke": "node scripts/alpha-smoke.mjs",
|
|
26
35
|
"alpha:smoke:providers": "node scripts/alpha-provider-smoke-matrix.mjs",
|
|
@@ -28,11 +37,13 @@
|
|
|
28
37
|
"ui:dev": "node ui/managed-agents-console/serve.mjs"
|
|
29
38
|
},
|
|
30
39
|
"dependencies": {
|
|
40
|
+
"@clack/prompts": "1.7.0",
|
|
31
41
|
"@earendil-works/pi-coding-agent": "0.80.6",
|
|
32
42
|
"@hono/node-server": "2.0.0",
|
|
33
43
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
34
44
|
"hono": "4.12.29",
|
|
35
45
|
"node-forge": "1.4.0",
|
|
46
|
+
"picocolors": "1.1.1",
|
|
36
47
|
"proper-lockfile": "4.1.2",
|
|
37
48
|
"typebox": "1.1.29",
|
|
38
49
|
"undici": "7.28.0",
|