@isomorph.ai/cli 0.10.3 → 0.10.4
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/dist/packages/harbour-cli/src/agent-setup.js +1 -1
- package/dist/packages/harbour-cli/src/check.js +0 -7
- package/dist/packages/harbour-cli/src/dev.js +2 -2
- package/dist/packages/harbour-cli/src/guide.js +3 -3
- package/dist/packages/harbour-cli/src/job-fixture.js +4 -21
- package/dist/packages/harbour-cli/src/jobs.js +3 -24
- package/package.json +1 -1
|
@@ -123,7 +123,7 @@ Rules live beside this file: \`core.md\` before the first edit; \`integrations.m
|
|
|
123
123
|
| "ship it", "let my team try it" | show the person the three values in \`.isomorph/app.json\` (name, description, audience — "only you" when empty) and correct the file as they say; then \`isomorph deploy --app-root . --json\` (it runs the checks when needed and refuses once naming every blocker and its fix: relay that line; the app works meanwhile). Give them \`result.deployment.protectedUrl\`. \`--session-notes\`: what cost you time in Isomorph, or \`""\`. |
|
|
124
124
|
| "make it live" | only after they have tried the preview: \`isomorph promote --operation <ref> --app-root . --confirm-tested --json\`. Report the production link, or that operator approval is pending. |
|
|
125
125
|
| "stop it" | \`isomorph stop --app-root .\` (data kept); \`isomorph dev --app-root . --reset\` only when they ask to start over. |
|
|
126
|
-
| "every day at 2pm", "send this automatically" | read \`jobs.md\` beside this skill first (
|
|
126
|
+
| "every day at 2pm", "send this automatically" | read \`jobs.md\` beside this skill first (the owner must enable background use of their account); write the job, run it once with \`isomorph jobs run\`, and say its sentence. |
|
|
127
127
|
| "call this API", "a key for it", "run it in the background" | read \`server.md\` beside this skill first; server code goes in \`actions/\` or \`jobs/\`, a key by its name only. |
|
|
128
128
|
|
|
129
129
|
A refusal names its layer, its reason and its fix: change that one thing, then run it again. A wait timeout is not a failure — work is still running: run the command it prints (\`${continueCommand("<ref>")}\`); never start a second deploy while one is running.
|
|
@@ -2,7 +2,6 @@ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { CliError } from "./output.js";
|
|
4
4
|
import { DEPENDENT_READ_OPERATIONS, READ_OPERATIONS, kitPaths, readDeclaration, readKitLock, resourceNames, sourceDigest } from "./kit.js";
|
|
5
|
-
import { jobIdentityProblems } from "./jobs.js";
|
|
6
5
|
import { LocalRuntime, allocatePorts, freePort, installDependencies, nodePackageCommand, readDevLock, runningOrigin } from "./local-runtime.js";
|
|
7
6
|
import { CLI_VERSION } from "./version.js";
|
|
8
7
|
/**
|
|
@@ -30,8 +29,6 @@ const DETAIL_CAP = 200;
|
|
|
30
29
|
export const CHECKS_FAILED_HINT = "Full detail is in .isomorph/check-report.json; fix what it reports, then run `isomorph check --app-root .` again.";
|
|
31
30
|
/** The name the gate reports operation coverage under — the pipeline's rule for it. */
|
|
32
31
|
export const FLOW_CHECK = "flow.check-failed";
|
|
33
|
-
/** The CLI's own check over `jobs/`: a company operation a job calls that would run as a person. */
|
|
34
|
-
export const JOBS_CHECK = "jobs.identity";
|
|
35
32
|
/**
|
|
36
33
|
* Runs the kit checks and writes `.isomorph/check-report.json`; a source
|
|
37
34
|
* edit changes sourceDigest and so invalidates the previous report. The app's
|
|
@@ -60,10 +57,6 @@ export async function runChecks(root, options) {
|
|
|
60
57
|
const gate = await runKitGate(root, { run, bundle, output, ...(options.fetch ? { fetch: options.fetch } : {}), ...(options.runtime ? { runtime: options.runtime } : {}), ...(options.pollMs ? { pollMs: options.pollMs } : {}) });
|
|
61
58
|
for (const check of gate.checks)
|
|
62
59
|
record(check);
|
|
63
|
-
// A job runs as the app: a company operation in `jobs/` that needs a person is refused here, not by the deployed schedule.
|
|
64
|
-
const jobs = await jobIdentityProblems(root, await readDeclaration(root));
|
|
65
|
-
if (jobs.length)
|
|
66
|
-
record({ name: JOBS_CHECK, status: "fail", detail: jobs.join("\n") });
|
|
67
60
|
let integrations = "not tested";
|
|
68
61
|
if (options.governance) {
|
|
69
62
|
// Reads that could not run are one check the report carries as not_run with
|
|
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { createForwarder } from "./forwarder.js";
|
|
6
6
|
import { GovernanceClient, ensureLinkedApp, fileDeclaredRequests, IDENTITY_WORDS, renderGrantGroup } from "./integrations.js";
|
|
7
7
|
import { acquireDevLock, allocatePorts, assertMigrationNames, installDependencies, LocalRuntime, nodePackageCommand, pidAlive, readDevLock, recordDevChildren, releaseDevLock, runCommand, hasServerModules } from "./local-runtime.js";
|
|
8
|
-
import { accessDigest, kitPaths,
|
|
8
|
+
import { accessDigest, kitPaths, readKitLock } from "./kit.js";
|
|
9
9
|
import { startJobFixture } from "./job-fixture.js";
|
|
10
10
|
import { CliError, safeError } from "./output.js";
|
|
11
11
|
/**
|
|
@@ -127,7 +127,7 @@ export async function startDev(root, options) {
|
|
|
127
127
|
// front of the session's gateway (job-fixture.ts: canned AI, company
|
|
128
128
|
// systems refused), exactly what `isomorph jobs run` gives one job.
|
|
129
129
|
if (await hasServerModules(root)) {
|
|
130
|
-
fixture = await startJobFixture({ upstreamPort: ports.gateway, real: false
|
|
130
|
+
fixture = await startJobFixture({ upstreamPort: ports.gateway, real: false });
|
|
131
131
|
if (await runtime.startKitRuntime(session, fixture.url, options.output))
|
|
132
132
|
options.output("Started the app's server side (actions/ and jobs/) on the kit runtime.");
|
|
133
133
|
}
|
|
@@ -91,11 +91,11 @@ const reply = await isomorph.ai.chat({ messages: [{ role: "user", content: \`Sum
|
|
|
91
91
|
The starter calls no AI; add the one call when the person asks. The check's generated ai journey makes a real governed call locally through the development route with your \`isomorph login\`; in the pipeline it is answered by a canned completion and reported as not tested.`,
|
|
92
92
|
jobs: `# Scheduled jobs
|
|
93
93
|
|
|
94
|
-
A job runs as the app
|
|
94
|
+
A job runs as the app. It may use Gmail or user-mode Slack after the app owner explicitly enables background use of their connected account for that app and environment. Without that permission, user-mode calls are refused.
|
|
95
95
|
|
|
96
|
-
Scheduled work lives only in \`jobs/<name>.ts\`: export one literal UTC cron as \`schedule\` and one default async handler. Never use \`setInterval\`, an effect or a browser timer as a scheduler; Isomorph runs the same declaration automatically after deployment on the app's server process, which owns the real clock. A job may call \`isomorph.ai.chat\` and
|
|
96
|
+
Scheduled work lives only in \`jobs/<name>.ts\`: export one literal UTC cron as \`schedule\` and one default async handler. Never use \`setInterval\`, an effect or a browser timer as a scheduler; Isomorph runs the same declaration automatically after deployment on the app's server process, which owns the real clock. A job may call \`isomorph.ai.chat\` and approved company operations. A job with no \`schedule\` runs only when enqueued (server.md). Test it immediately: start \`isomorph dev\`, then \`isomorph jobs run <name> --app-root . --scheduled-at <matching-UTC-time> --json\` — local data, canned AI, company systems refused, nothing sent. Use \`--real\` only when the person explicitly asks to test the company action now and the exact action has development approval.
|
|
97
97
|
|
|
98
|
-
A real scheduled send requires the
|
|
98
|
+
A real scheduled send requires the app owner to enable background use of their account, the operation and resource declared in \`.isomorph/integrations.json\`, and a grant for that environment; use one deterministic idempotency key for the business period and destination so a replay does not silently repost.
|
|
99
99
|
|
|
100
100
|
Tell them: "Your scheduled task is ready. I can test the app on your computer now. Company messages will start after the app is online and access is approved."`,
|
|
101
101
|
server: `# Server code: actions, queued runs, outside APIs
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { pipe, readBody, reject } from "./forwarder.js";
|
|
3
|
-
import { isOperationName, OPERATION_IDENTITIES } from "./kit.js";
|
|
4
3
|
/**
|
|
5
4
|
* The origin a job run talks to: a loopback server in front of the session's
|
|
6
5
|
* gateway (an ordinary run) or the dev origin (`--real`).
|
|
@@ -13,24 +12,11 @@ import { isOperationName, OPERATION_IDENTITIES } from "./kit.js";
|
|
|
13
12
|
* front of it proxies those paths to governance. Here, governed AI is answered
|
|
14
13
|
* with the pipeline's canned completion (transformbuild/local_ai_fixture.go),
|
|
15
14
|
* company systems are refused naming `--real` — a canned read would be data
|
|
16
|
-
* the job then acts on
|
|
17
|
-
*
|
|
18
|
-
* person, and `--real` carries the builder's own identity through the
|
|
19
|
-
* forwarder, so a scheduled Gmail send used to pass locally and fail deployed.
|
|
20
|
-
* `isomorph check` refuses the same calls statically (jobs.ts `jobIdentityProblems`).
|
|
15
|
+
* the job then acts on. Real user-mode calls are marked as background runs,
|
|
16
|
+
* so governance requires the same explicit account permission as production.
|
|
21
17
|
*/
|
|
22
18
|
export const AI_FIXTURE_COMPLETION = "AI is not exercised in the build fixture; this is a canned completion.";
|
|
23
19
|
const BODY_LIMIT = 1024 * 1024;
|
|
24
|
-
/** Whether a job's call to `operation` would run as a person: the request's `mode` or the declaration's identity says "user", or the catalog allows no app identity. */
|
|
25
|
-
export function runsAsPerson(operation, declaration, connection, mode) {
|
|
26
|
-
if (!isOperationName(operation))
|
|
27
|
-
return false;
|
|
28
|
-
const identities = OPERATION_IDENTITIES[operation];
|
|
29
|
-
const chosen = mode === "app" || mode === "user" ? mode : declaration.connections[connection]?.operations[operation]?.identity;
|
|
30
|
-
return !identities.includes("app") || chosen === "user";
|
|
31
|
-
}
|
|
32
|
-
/** The refusal a person-only operation gets from a job, in every mode and in `check`. */
|
|
33
|
-
export const personRefusal = (operation) => `${operation} runs as a person; a scheduled job runs as the app and has no person to act for, locally or deployed. Only app-identity operations run in a job.`;
|
|
34
20
|
export async function startJobFixture(options) {
|
|
35
21
|
const server = createServer((request, response) => {
|
|
36
22
|
const path = (request.url ?? "/").split("?")[0];
|
|
@@ -58,15 +44,12 @@ function execute(request, response, raw, options) {
|
|
|
58
44
|
}
|
|
59
45
|
catch { /* the upstream answers a malformed body */ }
|
|
60
46
|
const operation = String(body.operation ?? "");
|
|
61
|
-
if (runsAsPerson(operation, options.declaration, String(body.connection ?? ""), body.mode)) {
|
|
62
|
-
reject(response, 409, "CONFLICT", personRefusal(operation), "IDENTITY_MODE_MISMATCH", { operation, identity: "user" });
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
47
|
if (!options.real) {
|
|
66
48
|
reject(response, 409, "CONFLICT", `${operation || "This operation"} is not exercised in a local job run. Run \`isomorph jobs run <name> --real\` to use the company's approved development access; the call then really runs.`, "LOCAL_FIXTURE", { operation });
|
|
67
49
|
return;
|
|
68
50
|
}
|
|
69
|
-
|
|
51
|
+
const forwarded = Buffer.from(JSON.stringify({ ...body, backgroundRun: true }));
|
|
52
|
+
pipe(request, response, options.upstreamPort, {}, forwarded);
|
|
70
53
|
}
|
|
71
54
|
function aiFixture(path) {
|
|
72
55
|
return path === "/_harbour/ai/chat"
|
|
@@ -3,8 +3,8 @@ import { join } from "node:path";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { CliError } from "./output.js";
|
|
5
5
|
import { parseSessionEnv, readDevLock, readLocalSecrets, runCommand } from "./local-runtime.js";
|
|
6
|
-
import { kitPaths
|
|
7
|
-
import {
|
|
6
|
+
import { kitPaths } from "./kit.js";
|
|
7
|
+
import { startJobFixture } from "./job-fixture.js";
|
|
8
8
|
const jobName = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
9
9
|
const scheduleDeclaration = /^export\s+const\s+schedule\s*=\s*["']([^"']+)["']\s*;?\s*$/m;
|
|
10
10
|
const scheduleExport = /\bexport\s+const\s+schedule\b/;
|
|
@@ -48,7 +48,7 @@ export async function runJob(root, name, scheduledAt, run = runCommand, real = f
|
|
|
48
48
|
const runner = `const module = await import(${JSON.stringify(pathToFileURL(job.path).href)}); if (typeof module.default !== "function") throw new Error("job has no default handler"); await module.default({scheduledAt: process.env.ISOMORPH_SCHEDULED_AT});`;
|
|
49
49
|
// The job talks to a loopback origin of its own (job-fixture.ts) in front of
|
|
50
50
|
// the session's gateway, or of the dev origin under `--real`.
|
|
51
|
-
const fixture = await startJobFixture({ upstreamPort: real ? lock.ports.origin : lock.ports.gateway, real
|
|
51
|
+
const fixture = await startJobFixture({ upstreamPort: real ? lock.ports.origin : lock.ports.gateway, real });
|
|
52
52
|
let result;
|
|
53
53
|
try {
|
|
54
54
|
result = await run(process.execPath, ["--experimental-strip-types", "--input-type=module", "--eval", runner], { cwd: root, env: { ...env, ISOMORPH_GATEWAY_URL: fixture.url, ISOMORPH_SCHEDULED_AT: timestamp } });
|
|
@@ -75,24 +75,3 @@ export function jobFailureReason(stderr) {
|
|
|
75
75
|
return lines.find(line => /^[A-Z]\w*(?:Error|Exception)\b: \S/.test(line))
|
|
76
76
|
?? lines.find(line => /\b(?:error|exception|failed|cannot|unexpected|invalid|denied|refused)\b/i.test(line)) ?? lines[0] ?? "process exited non-zero";
|
|
77
77
|
}
|
|
78
|
-
const executeCall = /isomorph\s*\.\s*integrations\s*\.\s*execute\s*\(\s*["']([^"']+)["']\s*,\s*\{([^)]*)\)/gs;
|
|
79
|
-
/**
|
|
80
|
-
* Every company operation a job calls that would run as a person, one line
|
|
81
|
-
* each: what `isomorph check` refuses, so a scheduled Gmail send is caught
|
|
82
|
-
* before it is deployed and fails there (a job has no person). The same rule
|
|
83
|
-
* answers the call at run time (job-fixture.ts); this is the static half.
|
|
84
|
-
*/
|
|
85
|
-
export async function jobIdentityProblems(root, declaration) {
|
|
86
|
-
const problems = [];
|
|
87
|
-
for (const job of await discoverJobs(root)) {
|
|
88
|
-
const source = await readFile(job.path, "utf8");
|
|
89
|
-
for (const match of source.matchAll(executeCall)) {
|
|
90
|
-
const connection = match[1];
|
|
91
|
-
const operation = /operation\s*:\s*["']([^"']+)["']/.exec(match[2])?.[1] ?? "";
|
|
92
|
-
const mode = /mode\s*:\s*["']([^"']+)["']/.exec(match[2])?.[1];
|
|
93
|
-
if (runsAsPerson(operation, declaration, connection, mode))
|
|
94
|
-
problems.push(`jobs/${job.name}.ts: ${personRefusal(operation)}`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return problems;
|
|
98
|
-
}
|