Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/amico-run/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export * from "./schemas.js";
export * from "./event_queue.js";
export * from "./local_executor.js";
export * from "./scheduler.js";
export * from "./remote_config.js";
export * from "./remote_executor.js";
22 changes: 17 additions & 5 deletions packages/amico-run/src/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { parse as parseToml } from "smol-toml";
import { LocalExecutor } from "./local_executor.js";
import { ConfigError, type Finished, type SubmitOpts } from "./types.js";
import { RemoteExecutor } from "./remote_executor.js";
import { ConfigError, type Executor, type Finished, type SubmitOpts } from "./types.js";
import { readAuthoring } from "./authoring.js";
import { runGate } from "./gate.js";
import { runVerification } from "./verify.js";
Expand All @@ -23,7 +24,7 @@ function readTomlSafe(fp: string): Record<string, unknown> | undefined {
}
}

const USAGE = `usage: amico-run <script.jl> [--executor local] [--lab <id-or-path>]
const USAGE = `usage: amico-run <script.jl> [--executor local|remote] [--lab <id-or-path>]
[--runs-root <path>] [--julia <path>] [--project <path>] [--sysimage <path>]
[--spec <solvespec.json>] (spec C: validate + gate before launch)
amico-run resolve --platform <p> --kind <k> --size <n> (tier resolution → JSON)
Expand Down Expand Up @@ -99,10 +100,20 @@ export async function launch(argv: string[]): Promise<number> {
console.error(`amico-run: no script given\n${USAGE}`);
return 64;
}
if (executor !== "local") {
console.error(`amico-run: only --executor local is supported in β`);
if (executor !== "local" && executor !== "remote") {
console.error(`amico-run: unknown --executor ${executor} (supported: local, remote)`);
return 64;
}
if (executor === "remote" && specPath !== undefined) {
// Named seam: the gate could run pre-submit, but free-tier re-rollout
// verification (runVerification) replays LOCAL artifacts the mirror
// doesn't have. Reject loudly rather than half-verify.
console.error(`amico-run: --spec with --executor remote is not supported yet (verification is local-only)`);
return 64;
}
if (executor === "remote" && (opts.julia!.julia || opts.julia!.project || opts.julia!.sysimage)) {
console.error(`amico-run: --julia/--project/--sysimage are ignored with --executor remote (the runner image owns the environment)`);
}

// ── spec C: the launch gate. Failures leave NO run dir and exit 64. ──
if (specPath) {
Expand Down Expand Up @@ -152,7 +163,8 @@ export async function launch(argv: string[]): Promise<number> {

let handle;
try {
handle = await new LocalExecutor().submit(script, opts);
const exec: Executor = executor === "remote" ? new RemoteExecutor() : new LocalExecutor();
handle = await exec.submit(script, opts);
} catch (e) {
if (e instanceof ConfigError) {
console.error(`amico-run: ${e.message}`);
Expand Down
7 changes: 0 additions & 7 deletions packages/amico-run/src/local_executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,3 @@ export class LocalExecutor implements Executor {
return { runId, runDir, events, finished, abort };
}
}

/** Spec §3: interface seam only — implementation is post-β. */
export class RemoteExecutor implements Executor {
submit(): Promise<RunHandle> {
return Promise.reject(new Error("RemoteExecutor: not implemented in β (D9 plan, Phase 2+)"));
}
}
48 changes: 48 additions & 0 deletions packages/amico-run/src/remote_config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// packages/amico-run/src/remote_config.ts
// Cloud solve-service config (Δ8) — the authoring.ts idiom (env override →
// ~/.amico file). SECURITY: the token value must never appear in an error
// message or log line (llm_creds.mjs stance — the secret never enters
// amico's surfaces beyond the Authorization header itself).
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { ConfigError } from "./types.js";

export interface RemoteConfig {
baseUrl: string; // e.g. https://solves.staging.harmoniqs.co (no trailing slash)
token: string; // per-user Δ2 credential
}

/** $AMICO_CLOUD_FILE overrides the path (tests) — authoring.ts:37-41 idiom. */
export function cloudConfigFile(env: NodeJS.ProcessEnv = process.env): string {
const v = env.AMICO_CLOUD_FILE;
if (v && v.trim() !== "") return v;
return join(homedir(), ".amico", "cloud.json");
}

/** Resolution order: AMICO_CLOUD_URL+AMICO_CLOUD_TOKEN env pair → cloud.json.
* Any failure is ConfigError — exit-64 class (types.ts:50): nothing ran. */
export function readRemoteConfig(env: NodeJS.ProcessEnv = process.env): RemoteConfig {
const url = env.AMICO_CLOUD_URL;
const token = env.AMICO_CLOUD_TOKEN;
if (url && token) return { baseUrl: url.replace(/\/+$/, ""), token };
if (url || token)
throw new ConfigError(
"cloud config: set BOTH AMICO_CLOUD_URL and AMICO_CLOUD_TOKEN (or neither, to use cloud.json)",
);
const file = cloudConfigFile(env);
if (!existsSync(file))
throw new ConfigError(
`cloud config not found: ${file} (write {"base_url","token"} or set AMICO_CLOUD_URL/AMICO_CLOUD_TOKEN)`,
);
let raw: unknown;
try {
raw = JSON.parse(readFileSync(file, "utf8"));
} catch {
throw new ConfigError(`malformed cloud config at ${file}`);
}
const d = (typeof raw === "object" && raw !== null ? raw : {}) as Record<string, unknown>;
if (typeof d.base_url !== "string" || d.base_url === "" || typeof d.token !== "string" || d.token === "")
throw new ConfigError(`cloud config at ${file} needs non-empty string keys "base_url" and "token"`);
return { baseUrl: d.base_url.replace(/\/+$/, ""), token: d.token };
}
Loading
Loading