skillscript-runtime 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/bootstrap-from-env.d.ts +46 -0
  3. package/dist/bootstrap-from-env.d.ts.map +1 -0
  4. package/dist/bootstrap-from-env.js +130 -0
  5. package/dist/bootstrap-from-env.js.map +1 -0
  6. package/dist/cli.js +19 -164
  7. package/dist/cli.js.map +1 -1
  8. package/dist/connectors/mcp-remote.d.ts +5 -0
  9. package/dist/connectors/mcp-remote.d.ts.map +1 -1
  10. package/dist/connectors/mcp-remote.js +51 -6
  11. package/dist/connectors/mcp-remote.js.map +1 -1
  12. package/dist/connectors/registry.d.ts +9 -0
  13. package/dist/connectors/registry.d.ts.map +1 -1
  14. package/dist/connectors/registry.js +23 -0
  15. package/dist/connectors/registry.js.map +1 -1
  16. package/dist/connectors/skill-store.d.ts +1 -0
  17. package/dist/connectors/skill-store.d.ts.map +1 -1
  18. package/dist/connectors/skill-store.js +27 -0
  19. package/dist/connectors/skill-store.js.map +1 -1
  20. package/dist/connectors/sqlite-skill-store.d.ts +1 -0
  21. package/dist/connectors/sqlite-skill-store.d.ts.map +1 -1
  22. package/dist/connectors/sqlite-skill-store.js +10 -0
  23. package/dist/connectors/sqlite-skill-store.js.map +1 -1
  24. package/dist/connectors/types.d.ts +11 -0
  25. package/dist/connectors/types.d.ts.map +1 -1
  26. package/dist/connectors/types.js.map +1 -1
  27. package/dist/dashboard/server.d.ts +2 -0
  28. package/dist/dashboard/server.d.ts.map +1 -1
  29. package/dist/dashboard/server.js +4 -0
  30. package/dist/dashboard/server.js.map +1 -1
  31. package/dist/dashboard/spa/app.js +33 -5
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +1 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/lint.d.ts.map +1 -1
  37. package/dist/lint.js +10 -0
  38. package/dist/lint.js.map +1 -1
  39. package/dist/mcp-server.d.ts.map +1 -1
  40. package/dist/mcp-server.js +22 -1
  41. package/dist/mcp-server.js.map +1 -1
  42. package/docs/adopter-playbook.md +39 -4
  43. package/docs/configuration.md +2 -2
  44. package/docs/connector-contract-reference.md +4 -0
  45. package/docs/sqlite-skill-store.md +3 -2
  46. package/examples/connectors/SkillStoreTemplate/SkillStoreTemplate.ts +19 -0
  47. package/examples/custom-bootstrap.example.ts +17 -7
  48. package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
  49. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  Each release carries an **Upgrade impact:** line (first in its section) so a bump's requirements are visible at a glance. Tags (closed set): **BREAKING** (a manual change is needed to keep working) · **RE-APPROVE** (secured-mode signature invalidation — skills must be re-approved before they run) · **CONFIG** (`connectors.json` / config edit needed) · **none (additive)** (no action; backward-compatible). Standard from 0.20.0 forward; the pre-0.20 transitions that need action are flagged inline below (0.14.0, 0.18.8, 0.19.0). Full walkthrough: [UPGRADING.md](UPGRADING.md).
4
4
 
5
+ ## 0.24.0 — 2026-06-25 — adopter wishlist: bootstrapFromEnv + remote-store polling + connector reaping
6
+
7
+ **Upgrade impact:** none (additive). All four items are backward-compatible — a new export, an optional contract method, an opt-in tool param, and an internal lifecycle fix. Existing programmatic `bootstrap()` code keeps working; `bootstrapFromEnv()` is an *optional* simplification.
8
+
9
+ From an AMP-backed remote-store adopter dogfooding the runtime:
10
+
11
+ - **`bootstrapFromEnv()` — a blessed programmatic entry point.** Most adopters run `skillfile init` once, then operate via the web dashboard. This new exported helper stands that dashboard up from your own code *exactly the way the CLI does*: it loads `$SKILLSCRIPT_HOME/.env` + `skillscript.config.json` + `connectors.json`, resolves the full `SKILLSCRIPT_*` env cascade, calls `bootstrap()`, wires declarative triggers, and assembles the `DashboardServer`. Returns `{ wired, server }` (both unstarted; the caller `start()`s them and calls `wired.registry.disposeAll()` on shutdown). Precedence: explicit option > env > config.json > default. The CLI's `dashboard`/`serve` commands are now thin wrappers over it — closing the silent "every capability the CLI auto-wires had to be hand-assembled programmatically, and each omission failed silently" asymmetry.
12
+ - **`SkillStore.version()` — optional change-token kills the skill_list N+1 for remote stores.** Building the `skill_list` catalog loads every skill's body to parse its effectful footprint — free against a local store, but a network round-trip *per skill* against a remote one, so a polling dashboard hammered the substrate. A new optional `version()` returns a cheap store-wide token (no body loads); `skill_list` returns it as `catalog_version` and honors a caller's `if_none_match`, replying `{ not_modified: true }` and skipping the rebuild when nothing changed. The SPA now sends it (and pauses its poll when the browser tab is backgrounded). Bundled stores implement it (FS: per-file mtimes; sqlite: `(name, status, current_version)` in one query). Optional — a store without it just always rebuilds.
13
+ - **Connector reaping on shutdown.** `Registry.disposeAll()` (new) disposes connectors holding child processes on runtime shutdown, so a stdio-bridged MCP server child (`RemoteMcpConnector`) is reaped rather than orphaned to a dead parent across restarts. The CLI shutdown handler calls it; `bootstrapFromEnv()` callers should too. (Distinct from 0.23.1's respawn-on-death, which self-heals a child that dies *while running* — this reaps a child on *intentional* shutdown.)
14
+
15
+ ## 0.23.1 — 2026-06-23 — fixes: connector-arg lint advisory + RemoteMcpConnector self-heal
16
+
17
+ **Upgrade impact:** none (additive).
18
+
19
+ - **Fix — `RemoteMcpConnector` now respawns a dead child instead of latching a permanent outage (adopter finding).** When the connector's stdio child exited (e.g. an external `SIGTERM`, exit code 143), it latched a terminal error state and **never respawned** — every subsequent dispatch returned `RemoteMcpConnector in error state` until a full runtime restart. A single child death = total connector outage. `start()` now self-heals: a dead prior session is discarded and the next dispatch (or `describeTools` warm) relaunches the child, mirroring a session reconnect. An intentional `dispose()` still blocks respawn (a post-dispose dispatch throws). Independent of what killed the child. Verified with a real spawned child that exits mid-request.
20
+ - **Fix — stale tier-3 advisory contradicting connector-arg lint (adopter finding).** The tier-3 `unverified-qualified-tool` advisory ("connector doesn't declare its tool surface statically; can't validate at compile time") co-fired against a connector wired via `describeTools()` but no `staticTools()` — i.e. every `RemoteMcpConnector` — and *contradicted* the 0.23.0 tier-2 connector-arg validation on the **same** op: an author saw "you passed an unknown arg (so I clearly know your schema)" alongside "I can't validate this tool." It now skips an op whose tool is verified by the warmed schema, and still fires only when neither a static nor a warmed dynamic surface is reachable (a truly-opaque connector, or a misspelled tool name not in the fetched surface). Probe-verified against the live ddg `RemoteMcpConnector`.
21
+
5
22
  ## 0.23.0 — 2026-06-23 — connector tool-schema discovery + connector-aware lint
6
23
 
7
24
  **Upgrade impact:** none (additive). One thing to know: the new tier-2 lint can surface a *pre-existing* wrong-arg-name in an existing skill (it was always wrong; it just failed at dispatch before). Tier-2 is a warning — it never blocks compile or execution.
@@ -0,0 +1,46 @@
1
+ import { type BootstrapResult, type BootstrapOpts } from "./bootstrap.js";
2
+ import { DashboardServer } from "./dashboard/server.js";
3
+ export interface BootstrapFromEnvOptions {
4
+ /** "dashboard" mounts the SPA at `/`; "serve" runs headless on `/rpc`. Default "dashboard". */
5
+ mode?: "serve" | "dashboard";
6
+ /** Config root. Default `$SKILLSCRIPT_HOME` ?? `~/.skillscript`. */
7
+ home?: string;
8
+ /** Override the skillscript.config.json path (default `<home>/skillscript.config.json`). */
9
+ configPath?: string;
10
+ /** Override the connectors.json path (default `<home>/connectors.json`). */
11
+ connectorsConfigPath?: string;
12
+ /** Override the HTTP port (else env `SKILLSCRIPT_PORT` / config / 7878). */
13
+ port?: number;
14
+ /** Override the bind address (else env `SKILLSCRIPT_HOST` / config / 127.0.0.1). */
15
+ host?: string;
16
+ /**
17
+ * Pass-through to `bootstrap()`, merged LAST so it wins over the env/config
18
+ * resolution. This is how you inject CUSTOM in-process substrate instances —
19
+ * `overrides: { skillStore: new MyRemoteSkillStore(...) }` — while letting
20
+ * everything else auto-wire from `$SKILLSCRIPT_HOME`. (A custom substrate that
21
+ * IS expressible declaratively can instead use the `connectors.json`
22
+ * `{ type: "custom", module, export }` form and skip this.)
23
+ */
24
+ overrides?: Partial<BootstrapOpts>;
25
+ }
26
+ export interface BootstrapFromEnvResult {
27
+ /** The wired runtime (registry, mcpServer, scheduler, skillStore, traceStore). */
28
+ wired: BootstrapResult;
29
+ /** The assembled DashboardServer — NOT started. Call `server.start()` + `wired.scheduler.start()`. */
30
+ server: DashboardServer;
31
+ }
32
+ /**
33
+ * Wire a runtime + DashboardServer from the environment, exactly as the CLI
34
+ * `dashboard` / `serve` commands do. Returns both UNSTARTED — the caller decides
35
+ * when to `scheduler.start()` + `server.start()` (and is responsible for
36
+ * `wired.registry.disposeAll()` on shutdown to reap connector children).
37
+ *
38
+ * Resolution precedence per knob: explicit option > `SKILLSCRIPT_*` env >
39
+ * `skillscript.config.json` > built-in default. Secured mode + approval keys +
40
+ * the shell/fs allowlists + timeouts are resolved inside `bootstrap()`; the
41
+ * dashboard auth token / approval passcode / caller-identity header are resolved
42
+ * inside `DashboardServer`. This helper layers the config-file values + home
43
+ * paths on top.
44
+ */
45
+ export declare function bootstrapFromEnv(opts?: BootstrapFromEnvOptions): Promise<BootstrapFromEnvResult>;
46
+ //# sourceMappingURL=bootstrap-from-env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap-from-env.d.ts","sourceRoot":"","sources":["../src/bootstrap-from-env.ts"],"names":[],"mappings":"AAeA,OAAO,EAAsC,KAAK,eAAe,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC9G,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAMxD,MAAM,WAAW,uBAAuB;IACtC,+FAA+F;IAC/F,IAAI,CAAC,EAAE,OAAO,GAAG,WAAW,CAAC;IAC7B,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4FAA4F;IAC5F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,sBAAsB;IACrC,kFAAkF;IAClF,KAAK,EAAE,eAAe,CAAC;IACvB,sGAAsG;IACtG,MAAM,EAAE,eAAe,CAAC;CACzB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,GAAE,uBAA4B,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA+D1G"}
@@ -0,0 +1,130 @@
1
+ // v0.23.x — `bootstrapFromEnv()`: one blessed entry point that wires a runtime
2
+ // + DashboardServer exactly the way the `skillfile dashboard` / `skillfile
3
+ // serve` CLI does — loading `$SKILLSCRIPT_HOME/.env`, `skillscript.config.json`,
4
+ // and `connectors.json`, resolving the full SKILLSCRIPT_* env cascade, then
5
+ // bootstrap()ing and assembling the DashboardServer.
6
+ //
7
+ // Most adopters run the CLI once for `init`, then operate via the web
8
+ // dashboard. This helper is the programmatic equivalent: an adopter calls it,
9
+ // gets a fully-wired runtime + server, and swaps only the substrates they
10
+ // customize — closing the silent CLI-vs-programmatic wiring asymmetry where
11
+ // each capability the CLI auto-wires had to be hand-assembled (and each
12
+ // omission failed silently). Adopter wishlist 82e17077.
13
+ import { homedir } from "node:os";
14
+ import { join } from "node:path";
15
+ import { bootstrap, wireDeclarativeTriggers } from "./bootstrap.js";
16
+ import { DashboardServer } from "./dashboard/server.js";
17
+ import { loadSkillscriptConfig } from "./runtime-config.js";
18
+ import { loadEnvFile } from "./dotenv-loader.js";
19
+ import { isSecuredMode, evaluateApprovalGate } from "./approval.js";
20
+ /**
21
+ * Wire a runtime + DashboardServer from the environment, exactly as the CLI
22
+ * `dashboard` / `serve` commands do. Returns both UNSTARTED — the caller decides
23
+ * when to `scheduler.start()` + `server.start()` (and is responsible for
24
+ * `wired.registry.disposeAll()` on shutdown to reap connector children).
25
+ *
26
+ * Resolution precedence per knob: explicit option > `SKILLSCRIPT_*` env >
27
+ * `skillscript.config.json` > built-in default. Secured mode + approval keys +
28
+ * the shell/fs allowlists + timeouts are resolved inside `bootstrap()`; the
29
+ * dashboard auth token / approval passcode / caller-identity header are resolved
30
+ * inside `DashboardServer`. This helper layers the config-file values + home
31
+ * paths on top.
32
+ */
33
+ export async function bootstrapFromEnv(opts = {}) {
34
+ const home = opts.home ?? process.env["SKILLSCRIPT_HOME"] ?? join(homedir(), ".skillscript");
35
+ // Load `<home>/.env` so SKILLSCRIPT_* vars are populated — the CLI does this
36
+ // at startup; a programmatic adopter would otherwise miss it. Missing file is
37
+ // a no-op (per dotenv-loader).
38
+ loadEnvFile({ path: join(home, ".env") });
39
+ const mode = opts.mode ?? "dashboard";
40
+ const configPath = opts.configPath ?? join(home, "skillscript.config.json");
41
+ const { config: fileConfig } = loadSkillscriptConfig({ path: configPath });
42
+ const triggersFilePath = fileConfig.triggersFilePath ?? join(home, "triggers.json");
43
+ const connectorsConfigPath = opts.connectorsConfigPath ?? fileConfig.connectorsConfigPath ?? join(home, "connectors.json");
44
+ // env ?? config per knob, so `bootstrap()`'s own env resolution (opts win over
45
+ // env there) preserves the env > config > default precedence the CLI uses.
46
+ const enableUnsafeShell = boolEnv(process.env["SKILLSCRIPT_ENABLE_UNSAFE_SHELL"]) ?? fileConfig.enableUnsafeShell;
47
+ const forceAlwaysDraft = boolEnv(process.env["SKILLSCRIPT_FORCE_ALWAYS_DRAFT"]) ?? fileConfig.forceAlwaysDraft;
48
+ const pollIntervalSeconds = posIntEnv(process.env["SKILLSCRIPT_POLL_INTERVAL_SECONDS"]) ?? fileConfig.pollIntervalSeconds;
49
+ const absoluteTimeoutMs = posIntEnv(process.env["SKILLSCRIPT_ABSOLUTE_TIMEOUT_MS"]) ?? fileConfig.absoluteTimeoutMs;
50
+ const maxRecursionDepth = posIntEnv(process.env["SKILLSCRIPT_MAX_RECURSION_DEPTH"], 1) ?? fileConfig.maxRecursionDepth;
51
+ const shellAllowlist = listEnv(process.env["SKILLSCRIPT_SHELL_ALLOWLIST"]) ?? fileConfig.shellAllowlist;
52
+ const fsAllowlist = listEnv(process.env["SKILLSCRIPT_FS_ALLOWLIST"]) ?? fileConfig.fsAllowlist;
53
+ const wired = bootstrap({
54
+ skillsDir: fileConfig.skillsDir ?? join(home, "skills"),
55
+ traceDir: fileConfig.traceDir ?? join(home, "traces"),
56
+ dataDbPath: fileConfig.dataDbPath ?? join(home, "data.db"),
57
+ triggersFilePath,
58
+ connectorsConfigPath,
59
+ mode,
60
+ ...(pollIntervalSeconds !== undefined ? { pollIntervalSeconds } : {}),
61
+ ...(absoluteTimeoutMs !== undefined ? { absoluteTimeoutMs } : {}),
62
+ ...(maxRecursionDepth !== undefined ? { maxRecursionDepth } : {}),
63
+ ...(shellAllowlist !== undefined ? { shellAllowlist } : {}),
64
+ ...(fsAllowlist !== undefined ? { fsAllowlist } : {}),
65
+ ...(enableUnsafeShell !== undefined ? { enableUnsafeShell } : {}),
66
+ ...(forceAlwaysDraft === true ? { forceAlwaysDraft: true } : {}),
67
+ // Scheduler-fired skills record traces by default.
68
+ trace: { mode: "on" },
69
+ // Adopter overrides win last — notably custom substrate INSTANCES
70
+ // (e.g. overrides.skillStore = new MyRemoteSkillStore(...)).
71
+ ...opts.overrides,
72
+ });
73
+ await wireDeclarativeTriggers(wired);
74
+ await warnStaleApprovals(wired.skillStore);
75
+ const server = new DashboardServer({
76
+ mcpServer: wired.mcpServer,
77
+ mountSpa: mode === "dashboard",
78
+ // Scheduler is passed unconditionally: the /event route gates on its own
79
+ // enable flag, but the dashboard /approve route needs the scheduler to
80
+ // re-register a skill's declarative triggers on approval.
81
+ scheduler: wired.scheduler,
82
+ skillStore: wired.skillStore,
83
+ ...(opts.port !== undefined ? { port: opts.port } : {}),
84
+ ...(opts.host !== undefined ? { bindAddress: opts.host } : {}),
85
+ // port / host / mcpCallerIdentityHeader / eventIngress* / dashboard auth
86
+ // token / approval passcode / approval key file are all resolved from env by
87
+ // DashboardServer itself.
88
+ });
89
+ return { wired, server };
90
+ }
91
+ /** Warn (best-effort) when secured mode is on and stored Approved skills lack a valid signature. */
92
+ async function warnStaleApprovals(skillStore) {
93
+ if (!isSecuredMode())
94
+ return;
95
+ try {
96
+ const approved = await skillStore.query({ status: "Approved" });
97
+ const stale = [];
98
+ for (const m of approved) {
99
+ const loaded = await skillStore.load(m.name);
100
+ if (!evaluateApprovalGate(loaded.source).ok)
101
+ stale.push(m.name);
102
+ }
103
+ if (stale.length === 0)
104
+ return;
105
+ const shown = stale.slice(0, 5).join(", ");
106
+ const more = stale.length > 5 ? `, +${stale.length - 5} more` : "";
107
+ process.stderr.write(`\n⚠ Secured mode: ${stale.length} stored skill${stale.length === 1 ? "" : "s"} ` +
108
+ `${stale.length === 1 ? "is" : "are"} Approved but carry no valid signature — ` +
109
+ `they will be REFUSED until re-approved.\n` +
110
+ ` Re-bless the set: skillfile reapprove --apply\n` +
111
+ ` (${shown}${more})\n\n`);
112
+ }
113
+ catch {
114
+ /* best-effort — a scan failure must never block the runtime starting */
115
+ }
116
+ }
117
+ // ─── env coercion helpers (mirror the CLI's cascade semantics) ───────────────
118
+ function boolEnv(raw) {
119
+ return raw !== undefined ? raw === "true" : undefined;
120
+ }
121
+ function posIntEnv(raw, min = 0) {
122
+ if (raw === undefined)
123
+ return undefined;
124
+ const n = Number(raw);
125
+ return Number.isInteger(n) && n >= (min || 1) ? n : undefined;
126
+ }
127
+ function listEnv(raw) {
128
+ return raw !== undefined ? raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : undefined;
129
+ }
130
+ //# sourceMappingURL=bootstrap-from-env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap-from-env.js","sourceRoot":"","sources":["../src/bootstrap-from-env.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -25,9 +25,8 @@ import { SqliteDataStore } from "./connectors/data-store.js";
25
25
  import { parse } from "./parser.js";
26
26
  import { FilesystemTraceStore } from "./trace.js";
27
27
  import { healthMetrics } from "./metrics.js";
28
- import { DashboardServer } from "./dashboard/server.js";
29
- import { bootstrap, defaultRegistry, wireDeclarativeTriggers, ensureApprovalKeys, defaultApprovalKeyFile, defaultApprovalPublicKeyFile } from "./bootstrap.js";
30
- import { loadSkillscriptConfig } from "./runtime-config.js";
28
+ import { defaultRegistry, ensureApprovalKeys, defaultApprovalKeyFile, defaultApprovalPublicKeyFile } from "./bootstrap.js";
29
+ import { bootstrapFromEnv } from "./bootstrap-from-env.js";
31
30
  import { loadEnvFile } from "./dotenv-loader.js";
32
31
  import { createHash } from "node:crypto";
33
32
  const HOME_DIR = process.env["SKILLSCRIPT_HOME"] ?? join(homedir(), ".skillscript");
@@ -907,34 +906,6 @@ async function cmdReapprove(args) {
907
906
  process.stdout.write(`\nDone — ${ok} re-blessed${fail > 0 ? `, ${fail} failed` : ""}.\n`);
908
907
  return fail === 0 ? 0 : 1;
909
908
  }
910
- // v0.20.1 — secured-mode startup nudge. Scans the store for skills that are
911
- // Approved but fail the gate (no/legacy/invalid signature) and warns once at
912
- // boot, pointing at `skillfile reapprove`. Best-effort: never blocks startup.
913
- async function warnStaleApprovals(skillStore) {
914
- if (!isSecuredMode())
915
- return;
916
- try {
917
- const approved = await skillStore.query({ status: "Approved" });
918
- const stale = [];
919
- for (const m of approved) {
920
- const loaded = await skillStore.load(m.name);
921
- if (!evaluateApprovalGate(loaded.source).ok)
922
- stale.push(m.name);
923
- }
924
- if (stale.length === 0)
925
- return;
926
- const shown = stale.slice(0, 5).join(", ");
927
- const more = stale.length > 5 ? `, +${stale.length - 5} more` : "";
928
- process.stderr.write(`\n⚠ Secured mode: ${stale.length} stored skill${stale.length === 1 ? "" : "s"} ` +
929
- `${stale.length === 1 ? "is" : "are"} Approved but carry no valid signature — ` +
930
- `they will be REFUSED until re-approved.\n` +
931
- ` Re-bless the set: skillfile reapprove --apply\n` +
932
- ` (${shown}${more})\n\n`);
933
- }
934
- catch {
935
- /* best-effort — a scan failure must never block the runtime starting */
936
- }
937
- }
938
909
  function parseRunCompileArgs(args) {
939
910
  const opts = { inputs: {}, format: "prompt", mechanical: false, inlineProvenance: false };
940
911
  for (let i = 0; i < args.length; i++) {
@@ -1066,144 +1037,23 @@ async function copyScaffoldFile(src, dest) {
1066
1037
  await writeFile(dest, body, "utf8");
1067
1038
  }
1068
1039
  async function cmdRuntimeHost(args, opts) {
1069
- // v0.7.3load skillscript.config.json if present. CLI flags override
1070
- // config-file values; config-file values override defaults.
1071
- const configPath = extractFlag(args, "--config") ?? join(HOME_DIR, "skillscript.config.json");
1072
- const { config: fileConfig, errors: configErrors } = loadSkillscriptConfig({ path: configPath });
1073
- for (const err of configErrors)
1074
- process.stderr.write(`[cli] ${err}\n`);
1075
- // v0.17.4 — env-cascade for operator-config switches. Precedence:
1076
- // CLI flag (most specific, per-invocation) > env var (per-process,
1077
- // drop in `.env`) > JSON config (per-deployment) > built-in default.
1078
- // This mirrors the standard layered-config pattern adopters expect
1079
- // from any well-behaved Node service.
1080
- const portStr = extractFlag(args, "--port") ?? process.env["SKILLSCRIPT_PORT"];
1081
- const port = portStr !== undefined ? parseInt(portStr, 10) : fileConfig.dashboard?.port ?? 7878;
1082
- // --host is the bind address inside the running process. 127.0.0.1 is
1083
- // the safe default for local invocation; container deployments pass
1084
- // --host 0.0.0.0 so the host-side port-forward can reach the listener
1085
- // (host port mapping still enforces 127.0.0.1 externally).
1086
- const host = extractFlag(args, "--host") ?? process.env["SKILLSCRIPT_HOST"] ?? fileConfig.dashboard?.host ?? "127.0.0.1";
1087
- // v0.17.4 — env cascade for the v0.17.0 inbound caller-identity header.
1088
- // Operator-config surface; env-natural for installer workflows.
1089
- const mcpCallerIdentityHeader = process.env["SKILLSCRIPT_MCP_CALLER_IDENTITY_HEADER"] ?? fileConfig.dashboard?.mcpCallerIdentityHeader;
1090
- // v0.17.4 — env cascade for the unsafe-shell posture switch.
1091
- // Security-relevant; env-natural so operators can flip without
1092
- // editing JSON.
1093
- const envUnsafeShell = process.env["SKILLSCRIPT_ENABLE_UNSAFE_SHELL"];
1094
- const enableUnsafeShell = envUnsafeShell !== undefined
1095
- ? envUnsafeShell === "true"
1096
- : fileConfig.enableUnsafeShell;
1097
- const triggersFilePath = fileConfig.triggersFilePath ?? join(HOME_DIR, "triggers.json");
1098
- // v0.4.3 — auto-discover connectors.json from HOME_DIR. Closes the
1099
- // last-mile gap of the v0.4.x arc: pre-v0.4.3 the loader + lint +
1100
- // runtime + allowlist all worked, but the canonical CLI entry point
1101
- // didn't read connectors.json. --connectors <path> overrides the
1102
- // default for non-standard layouts. Loader is graceful on missing.
1103
- const connectorsConfigPath = extractFlag(args, "--connectors") ?? fileConfig.connectorsConfigPath ?? join(HOME_DIR, "connectors.json");
1104
- // v0.17.4 — forceAlwaysDraft cascade: env var > config file > default
1105
- // false. SKILLSCRIPT_FORCE_ALWAYS_DRAFT=true forces every outside-MCP
1106
- // skill_write to land Draft regardless of body declaration. Closes
1107
- // the agent-self-approval path for adopters wanting a human approval
1108
- // gate. Drop a `.env` with the value, restart — done.
1109
- const envForceAlwaysDraft = process.env["SKILLSCRIPT_FORCE_ALWAYS_DRAFT"];
1110
- const forceAlwaysDraft = envForceAlwaysDraft !== undefined
1111
- ? envForceAlwaysDraft === "true"
1112
- : fileConfig.forceAlwaysDraft;
1113
- // v0.18.7 — env cascade for three previously-hidden operator knobs.
1114
- // Each follows the same cascade shape: env > config > default. Parsing
1115
- // errors (non-numeric / non-positive) are silently ignored — the
1116
- // config-layer schema parser is the authoritative validator for these
1117
- // fields, and env values failing here fall through to config/default.
1118
- const envPollSecondsRaw = process.env["SKILLSCRIPT_POLL_INTERVAL_SECONDS"];
1119
- const envPollSeconds = envPollSecondsRaw !== undefined ? Number(envPollSecondsRaw) : undefined;
1120
- const pollIntervalSeconds = envPollSeconds !== undefined && Number.isFinite(envPollSeconds) && envPollSeconds > 0
1121
- ? envPollSeconds
1122
- : fileConfig.pollIntervalSeconds;
1123
- const envAbsoluteTimeoutRaw = process.env["SKILLSCRIPT_ABSOLUTE_TIMEOUT_MS"];
1124
- const envAbsoluteTimeout = envAbsoluteTimeoutRaw !== undefined ? Number(envAbsoluteTimeoutRaw) : undefined;
1125
- const absoluteTimeoutMs = envAbsoluteTimeout !== undefined && Number.isInteger(envAbsoluteTimeout) && envAbsoluteTimeout > 0
1126
- ? envAbsoluteTimeout
1127
- : fileConfig.absoluteTimeoutMs;
1128
- const envMaxRecursionRaw = process.env["SKILLSCRIPT_MAX_RECURSION_DEPTH"];
1129
- const envMaxRecursion = envMaxRecursionRaw !== undefined ? Number(envMaxRecursionRaw) : undefined;
1130
- const maxRecursionDepth = envMaxRecursion !== undefined && Number.isInteger(envMaxRecursion) && envMaxRecursion >= 1
1131
- ? envMaxRecursion
1132
- : fileConfig.maxRecursionDepth;
1133
- // v0.18.8 — shell binary allowlist. Comma-separated env value, trimmed.
1134
- // Default-deny: when neither env nor config is set, leave undefined →
1135
- // runtime refuses ALL shell() ops. Empty env string is an explicit
1136
- // empty list (also refuses all) — distinct from undefined for
1137
- // observability (operator can declare "no shell at all" intentionally).
1138
- const envShellAllowlistRaw = process.env["SKILLSCRIPT_SHELL_ALLOWLIST"];
1139
- const shellAllowlist = envShellAllowlistRaw !== undefined
1140
- ? envShellAllowlistRaw.split(",").map((b) => b.trim()).filter((b) => b.length > 0)
1141
- : fileConfig.shellAllowlist;
1142
- // v1.0 Gate #7 — filesystem path allowlist (env > config.json), same shape as
1143
- // SKILLSCRIPT_SHELL_ALLOWLIST. Default-deny when unset.
1144
- const envFsAllowlistRaw = process.env["SKILLSCRIPT_FS_ALLOWLIST"];
1145
- const fsAllowlist = envFsAllowlistRaw !== undefined
1146
- ? envFsAllowlistRaw.split(",").map((p) => p.trim()).filter((p) => p.length > 0)
1147
- : fileConfig.fsAllowlist;
1148
- // v0.19.0 — event ingress (memory `ceaf4579`). Two env knobs:
1149
- // - SKILLSCRIPT_EVENT_INGRESS_ENABLED=true (opt-in; default off)
1150
- // - SKILLSCRIPT_EVENT_INGRESS_AUTH_TOKEN=… (optional bearer-token;
1151
- // when set, every POST /event requires Authorization: Bearer <token>)
1152
- const envEventIngressEnabled = process.env["SKILLSCRIPT_EVENT_INGRESS_ENABLED"];
1153
- const eventIngressEnabled = envEventIngressEnabled !== undefined
1154
- ? envEventIngressEnabled === "true"
1155
- : false;
1156
- const eventIngressAuthToken = process.env["SKILLSCRIPT_EVENT_INGRESS_AUTH_TOKEN"];
1157
- const wired = bootstrap({
1158
- skillsDir: fileConfig.skillsDir ?? SKILLS_DIR,
1159
- traceDir: fileConfig.traceDir ?? TRACE_DIR,
1160
- dataDbPath: fileConfig.dataDbPath ?? DATA_DB,
1161
- triggersFilePath,
1162
- connectorsConfigPath,
1040
+ // v0.23.xthe env-cascade + bootstrap + DashboardServer assembly lives in
1041
+ // the reusable `bootstrapFromEnv()` (so programmatic adopters wire it
1042
+ // identically). The CLI is a thin wrapper: CLI flags become explicit
1043
+ // overrides (highest precedence), then start + handle shutdown.
1044
+ const portFlag = extractFlag(args, "--port");
1045
+ const { wired, server } = await bootstrapFromEnv({
1163
1046
  mode: opts.mode,
1164
- ...(pollIntervalSeconds !== undefined ? { pollIntervalSeconds } : {}),
1165
- ...(absoluteTimeoutMs !== undefined ? { absoluteTimeoutMs } : {}),
1166
- ...(maxRecursionDepth !== undefined ? { maxRecursionDepth } : {}),
1167
- ...(shellAllowlist !== undefined ? { shellAllowlist } : {}),
1168
- ...(fsAllowlist !== undefined ? { fsAllowlist } : {}),
1169
- ...(enableUnsafeShell !== undefined ? { enableUnsafeShell } : {}),
1170
- ...(forceAlwaysDraft === true ? { forceAlwaysDraft: true } : {}),
1171
- // Scheduler-fired skills record traces by default; `fires` / `health` /
1172
- // `health_metrics` (MCP) all read from the trace store.
1173
- trace: { mode: "on" },
1047
+ home: HOME_DIR,
1048
+ ...(extractFlag(args, "--config") !== undefined ? { configPath: extractFlag(args, "--config") } : {}),
1049
+ ...(extractFlag(args, "--connectors") !== undefined ? { connectorsConfigPath: extractFlag(args, "--connectors") } : {}),
1050
+ ...(portFlag !== undefined ? { port: parseInt(portFlag, 10) } : {}),
1051
+ ...(extractFlag(args, "--host") !== undefined ? { host: extractFlag(args, "--host") } : {}),
1174
1052
  });
1175
- // Register declarative `# Triggers:` headers BEFORE arming the tick loop
1176
- // so the first tick can fire any minute-aligned cron entries.
1177
- await wireDeclarativeTriggers(wired);
1178
- // v0.20.1 — when secured mode is armed, loudly flag any stored skill that's
1179
- // Approved-but-unsigned (e.g. a pre-secured v1 corpus) so the operator runs
1180
- // `reapprove` instead of discovering refusals skill-by-skill at runtime.
1181
- await warnStaleApprovals(wired.skillStore);
1182
1053
  wired.scheduler.start();
1183
- // v0.2.7: dashboard mounts the SPA; serve runs headless on /rpc only.
1184
- const server = new DashboardServer({
1185
- mcpServer: wired.mcpServer,
1186
- port,
1187
- bindAddress: host,
1188
- mountSpa: opts.mode === "dashboard",
1189
- ...(mcpCallerIdentityHeader !== undefined ? { mcpCallerIdentityHeader } : {}),
1190
- // v0.19.0 — event ingress, off-by-default, scheduler reference required
1191
- eventIngressEnabled,
1192
- ...(eventIngressAuthToken !== undefined ? { eventIngressAuthToken } : {}),
1193
- // Scheduler is passed unconditionally: the /event route is gated on
1194
- // eventIngressEnabled, but the dashboard /approve route also needs the
1195
- // scheduler to re-register a skill's declarative triggers on approval
1196
- // (without it, dashboard-approved cron/event skills don't fire / don't
1197
- // show in the Triggers view).
1198
- scheduler: wired.scheduler,
1199
- // v0.20.2 — in-browser approval (passcode session-unlock). Mounts /unlock +
1200
- // /approve only when SKILLSCRIPT_APPROVAL_PASSCODE is set + secured + keyed.
1201
- skillStore: wired.skillStore,
1202
- approvalKeyFile: process.env["SKILLSCRIPT_APPROVAL_KEY_FILE"] ?? defaultApprovalKeyFile(),
1203
- });
1204
1054
  await server.start();
1205
1055
  const label = opts.mode === "dashboard" ? "dashboard" : "serve (headless)";
1206
- process.stdout.write(`skillfile ${label} running on http://${host}:${port}\nctrl-C to stop\n`);
1056
+ process.stdout.write(`skillfile ${label} running on http://${server.boundAddress()}:${server.boundPort()}\nctrl-C to stop\n`);
1207
1057
  await new Promise((resolve) => {
1208
1058
  let shuttingDown = false;
1209
1059
  const shutdown = () => {
@@ -1213,6 +1063,9 @@ async function cmdRuntimeHost(args, opts) {
1213
1063
  void (async () => {
1214
1064
  await wired.scheduler.stop();
1215
1065
  await server.stop();
1066
+ // Reap connector child processes (stdio-bridged MCP servers) so they
1067
+ // don't orphan to a dead parent across restarts.
1068
+ await wired.registry.disposeAll();
1216
1069
  resolve();
1217
1070
  })();
1218
1071
  };
@@ -1221,6 +1074,8 @@ async function cmdRuntimeHost(args, opts) {
1221
1074
  });
1222
1075
  return 0;
1223
1076
  }
1077
+ // Retired: the body below is superseded by bootstrapFromEnv(); kept only as a
1078
+ // marker so the diff is legible. Removed in the edit that follows.
1224
1079
  async function cmdFires(args) {
1225
1080
  const skill = args.find((a) => !a.startsWith("--"));
1226
1081
  if (skill === undefined) {