model-orchestrator 1.0.0 → 1.0.1
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/CHANGELOG.md +16 -1
- package/README.md +2 -0
- package/SECURITY.md +7 -3
- package/docs/catalog.md +8 -6
- package/docs/install.md +7 -1
- package/docs/security-review-history.md +1 -0
- package/package.json +1 -1
- package/src/README.md +1 -0
- package/src/aunx.js +42 -32
- package/src/bounded-file.js +31 -0
- package/src/catalog.js +4 -4
- package/src/install.js +5 -3
- package/src/uninstall.js +3 -2
- package/templates/advanced/vm/ENVIRONMENT.md +8 -0
- package/templates/advanced/vm/docker-compose.yml +2 -1
- package/templates/advanced/vm/jobs/README.md +28 -1
- package/templates/advanced/vm/jobs/weekly-audit.service +4 -2
- package/templates/advanced/vm/jobs/weekly-audit.sh +22 -15
- package/templates/common/protocols/acceptance-checks.md +1 -0
- package/templates/tools/codecalc/CODECALC.md +4 -4
- package/templates/tools/codecalc/mcp/agy.mcp_config.json +1 -1
- package/templates/tools/codecalc/mcp/codex.config.toml +1 -1
- package/templates/tools/codecalc/mcp/mcpServers.json +1 -1
- package/templates/tools/codecalc/mcp/vscode.mcp.json +1 -1
- package/templates/tools/codecalc/mcp/zed.settings.json +1 -1
- package/templates/tools/context7/CONTEXT7.md +6 -10
- package/templates/tools/obsidian-tc/OBSIDIAN-TC.md +2 -2
- package/templates/tools/obsidian-tc/mcp/obsidian-tc.agy.mcp_config.json +1 -1
- package/templates/tools/obsidian-tc/mcp/obsidian-tc.codex.config.toml +1 -1
- package/templates/tools/obsidian-tc/mcp/obsidian-tc.mcpServers.json +1 -1
- package/templates/tools/obsidian-tc/mcp/obsidian-tc.vscode.mcp.json +1 -1
- package/templates/tools/obsidian-tc/mcp/obsidian-tc.zed.settings.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to this project are documented here. The format follows [Kee
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.0.1] - 2026-09-27
|
|
8
|
+
|
|
9
|
+
### Security
|
|
10
|
+
|
|
11
|
+
- `aunx route-metrics` always runs the packaged metrics implementation. A checkout can no longer substitute its own JavaScript when a user requests a summary.
|
|
12
|
+
- Installer and uninstall manifests use bounded regular-file reads with symlink refusal. Acceptance-check and routing JSON reads share the same bounded reader.
|
|
13
|
+
- Acceptance-check timeouts and interrupts clean up the command's process group or Windows process tree, including ordinary descendants.
|
|
14
|
+
- Weekly gateway probes pass credentials through stdin instead of temporary files, reject multiline keys, and remove gateway/provider credentials from audit subprocesses. The service now reads a separate `weekly-audit.env`; see the generated jobs README when upgrading.
|
|
15
|
+
- Newly generated companion launch commands use the catalog's exact versions, and npm publication uses a reviewed exact npm version. Existing client entries are preserved; follow the [companion upgrade steps](docs/install.md#upgrading-companion-launch-commands).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Security and installation documentation now match current activation rules, model inheritance, companion setup and credential boundaries.
|
|
20
|
+
|
|
7
21
|
## [1.0.0] - 2026-09-27
|
|
8
22
|
|
|
9
23
|
### Added
|
|
@@ -499,7 +513,8 @@ First release.
|
|
|
499
513
|
- Tests: a case per fix, judges proven to go red, mutation checks; `npm test` prints the current count.
|
|
500
514
|
- Adversarial audit: two Codex rounds plus a two-engine review (Codex, Antigravity); findings and fixes in `docs/audit-brief.md`. After the review: subagents go to the project root (`--project`), snippet paths computed from `--dir`, lane sections rendered from the selection, a primary agent required, level 3 asks for API keys separately from CLIs, images and CLI installs pinned, an activation summary at the end of every install.
|
|
501
515
|
|
|
502
|
-
[Unreleased]: https://github.com/aunysillyme/model-orchestrator/compare/v1.0.
|
|
516
|
+
[Unreleased]: https://github.com/aunysillyme/model-orchestrator/compare/v1.0.1...HEAD
|
|
517
|
+
[1.0.1]: https://github.com/aunysillyme/model-orchestrator/compare/v1.0.0...v1.0.1
|
|
503
518
|
[1.0.0]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.35...v1.0.0
|
|
504
519
|
[0.1.35]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.34...v0.1.35
|
|
505
520
|
[0.1.34]: https://github.com/aunysillyme/model-orchestrator/compare/v0.1.33...v0.1.34
|
package/README.md
CHANGED
|
@@ -209,6 +209,8 @@ Run `npx model-orchestrator --uninstall --dir ./ai-orchestrator --project .` (ad
|
|
|
209
209
|
|
|
210
210
|
Node 18 or newer, with zero runtime dependencies. Works on macOS and Linux; the level 3 box templates assume Ubuntu. Windows: CI runs the suite on `windows-latest` (Node 18, 20, 22), including lane execution end to end through `cli-run` against a fake CLI installed the same way npm installs a real one (a `.cmd` shim). `cli-run` never runs a lane through `cmd.exe`: it resolves the shim to the Node script underneath and spawns Node directly, so a prompt reaching a real lane never passes through a Windows shell. A `.cmd` or `.bat` lane that cannot be resolved that way (an old or hand-edited shim) is refused with exit 13 and a message saying how to fix it, rather than run through `cmd.exe`: a batch file re-reads its arguments after `cmd.exe` has parsed them once, and no escaping fully contains a prompt through both passes. Install, detection, the hooks and `cli-run`'s `taskkill` tree kill are tested on Windows too, including SIGTERM/SIGINT to the wrapper (Windows has no OS-level signals: both terminate it unconditionally, verified there rather than treated the same as POSIX). The Windows skip list covers POSIX behavior, with each skip pinned by `test/prose.test.js`: `statSync().mode`'s executable bit (NTFS has none, so that one assertion is conditional inside a test that otherwise runs everywhere); a lane dying mid-run from a real POSIX signal (a real Windows lane cannot die "by signal"); running `weekly-audit.sh`'s watchdog functions for real under Git Bash's job control, both the end-to-end run and the `bounded()` timeout check (the script itself only ever runs on the Ubuntu box it targets); and a `mkfifo` FIFO at the rules path, the one case that proves `route-gate.mjs` cannot HANG on a non-regular file, since Windows has no `mkfifo` to build one (the guard behind it is covered on every OS by a directory at the same path); and an untracked `mkfifo` FIFO in the repository `cli-run --audit` sizes, the case that proves `--effort auto` never opens a non-regular file (the symlink half of that test runs on every OS). `test/prose.test.js` counts every `skip:` in the suite and requires this list to document each one.
|
|
211
211
|
|
|
212
|
+
Additional security regressions skip Windows for the project-hook symlink and manifest FIFO fixtures (symlink privileges and POSIX special files), the POSIX shell descendant timeout fixture (the argv equivalent still runs on Windows), and four weekly credential and report lifecycle runtime checks (the Ubuntu watchdog requires POSIX process-tree semantics). Their configuration and generated syntax remain covered on every platform.
|
|
213
|
+
|
|
212
214
|
**Privacy.** The installer sends no telemetry and makes no network call of its own once it is running. Two things around that are worth being exact about:
|
|
213
215
|
|
|
214
216
|
- `npx model-orchestrator` is itself a download: npm fetches this package from the registry before any of it runs. `npm install -g model-orchestrator` once, then run `model-orchestrator`, if you would rather that happen exactly one time.
|
package/SECURITY.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# Security policy
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The installer writes files inside the folders you name (`--dir` and `--project`). It never installs third-party packages, runs vendor shell scripts or writes credentials. Companion tools are opt-in; their guides and launch snippets use the exact versions in `src/catalog.js`. You run any third-party installation yourself. Home-level agent configuration is outside the installer scope.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Existing documents stay by default. `--force` explicitly replaces them; `--update-docs` replaces only unchanged managed documents. Machine-owned manifests and lane configuration refresh on every run, and unchanged managed runtime files upgrade automatically. `--upgrade-runtime` explicitly replaces runtime files. Project activation merges supported rules, hooks and companion entries with backups; interactive confirmation enables it by default, while `--yes` requires `--apply-snippets`. See [installation and ownership rules](docs/install.md).
|
|
6
|
+
|
|
7
|
+
`bin/cli-run.mjs` spawns the agent CLI you name with your prompt in its own process group and kills that group on timeout, overrun or signal. Vendor CLIs and companion tools may access the network and local data under their own permissions. The separately invoked level 3 setup and weekly audit scripts have their own installation and network behavior; inspect their generated instructions before running them.
|
|
8
|
+
|
|
9
|
+
Completed reviews and current guarantees: [security review history](docs/security-review-history.md), [guarantees](docs/guarantees.md) and [changelog](CHANGELOG.md).
|
|
6
10
|
|
|
7
11
|
## Reporting a vulnerability
|
|
8
12
|
|
|
@@ -14,4 +18,4 @@ Findings are reproduced before they are acted on; `CLEAN` is an acceptable outco
|
|
|
14
18
|
|
|
15
19
|
## Scope
|
|
16
20
|
|
|
17
|
-
In scope: `bin/`, `src/`, `scripts/`, the templates, and the files the installer writes from them (the weekly audit job, compose file, gateway config and setup script included). Out of scope: the agent CLIs, models and companion tools this package
|
|
21
|
+
In scope: `bin/`, `src/`, `scripts/`, the templates, and the files the installer writes from them (the weekly audit job, compose file, gateway config and setup script included). Out of scope: the agent CLIs, models and companion tools this package links to; report those to their own projects.
|
package/docs/catalog.md
CHANGED
|
@@ -308,27 +308,29 @@ Generated from `src/catalog.js`. Do not hand-edit; `npm run gen:catalog` rewrite
|
|
|
308
308
|
|
|
309
309
|
## Companion tools
|
|
310
310
|
|
|
311
|
+
Model-orchestrator project activation merges supported MCP entries for selected companions when activation is enabled. The vendor command auto-registration field below describes what the listed vendor command does when you run it yourself. Global client configuration remains manual.
|
|
312
|
+
|
|
311
313
|
### `codecalc` · codecalc (calculator, code runner, logic checker for your agent)
|
|
312
314
|
|
|
313
315
|
- **Repo:** https://github.com/The-40-Thieves/codecalc
|
|
314
316
|
- **Gives:** exact arithmetic, code execution in 31 languages, SMT logic checks, complexity and equivalence proofs; offline, no key, no telemetry
|
|
315
|
-
- **Install:** `uvx 'codecalc[full]' setup --write` (needs uv (https://docs.astral.sh/uv/) and Python 3.10+)
|
|
316
|
-
- **
|
|
317
|
+
- **Install:** `uvx 'codecalc[full]==0.5.0' setup --write` (needs uv (https://docs.astral.sh/uv/) and Python 3.10+)
|
|
318
|
+
- **Vendor command auto-registration:** Claude Code, Claude Desktop, Cursor, VS Code, Zed; snippets for the rest are written to `mcp/`
|
|
317
319
|
- **Default:** not selected
|
|
318
320
|
|
|
319
321
|
### `obsidian-tc` · obsidian-tc (governed memory: an agent-ready MCP server over an Obsidian vault)
|
|
320
322
|
|
|
321
323
|
- **Repo:** https://github.com/The-40-Thieves/obsidian-tc
|
|
322
324
|
- **Gives:** durable memory and record for your agents: hybrid retrieval (BM25 + dense + link graph), backlinks, compare-and-swap writes with a confirmation gate, folder ACLs, a poison scan on inferred writes; 163 tools, local by default
|
|
323
|
-
- **Install:** `npm install -g obsidian-tc && obsidian-tc /path/to/your/vault` (needs an Obsidian vault folder (the Obsidian app itself is only needed for live plugin bridges); Node 24+ or Bun 1.1+ (stricter than this installer); Ollama with `nomic-embed-text` for local embeddings, or a cloud embeddings key; the Local REST API plugin only for bridge tools)
|
|
324
|
-
- **
|
|
325
|
+
- **Install:** `npm install -g obsidian-tc@1.26.0 && obsidian-tc /path/to/your/vault` (needs an Obsidian vault folder (the Obsidian app itself is only needed for live plugin bridges); Node 24+ or Bun 1.1+ (stricter than this installer); Ollama with `nomic-embed-text` for local embeddings, or a cloud embeddings key; the Local REST API plugin only for bridge tools)
|
|
326
|
+
- **Vendor command auto-registration:** Cursor, VS Code; snippets for the rest are written to `mcp/`
|
|
325
327
|
- **Default:** not selected
|
|
326
328
|
|
|
327
329
|
### `context7` · Context7 (Upstash: version-aware docs for the libraries your agent calls)
|
|
328
330
|
|
|
329
331
|
- **Repo:** https://github.com/upstash/context7
|
|
330
332
|
- **Gives:** up-to-date, version-specific documentation and code examples for libraries, SDKs, APIs and CLIs, pulled into the prompt; tells the agent what the code is SUPPOSED to do. Paired with codecalc, which runs the code and proves what it actually does: docs never stand as proof, and where they disagree the run wins
|
|
331
|
-
- **Install:** `npx
|
|
332
|
-
- **
|
|
333
|
+
- **Install:** `npx -y @upstash/context7-mcp@4.1.1` (needs Node.js 18+ for the local server or the ctx7 CLI; a free CONTEXT7_API_KEY is optional, for higher rate limits (it works anonymously at the base rate))
|
|
334
|
+
- **Vendor command auto-registration:** none; use project activation or merge the supplied snippets in `mcp/` using the tool guide
|
|
333
335
|
- **Default:** not selected
|
|
334
336
|
|
package/docs/install.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Everything the installer asks, writes and accepts as a flag. The short version is in the [README](../README.md).
|
|
4
4
|
|
|
5
|
+
## Upgrading companion launch commands
|
|
6
|
+
|
|
7
|
+
Version 1.0.1 pins newly generated companion launch commands. Existing MCP server entries remain yours and are not silently replaced. Preview your existing selection and paths with `--update-docs --dry`, then apply that update after checking the plan. Unchanged managed snippets in `mcp/` refresh; edited snippets are preserved and named. Compare the refreshed snippets with each client's configuration and manually replace any preserved floating package command with its catalog-pinned form. Keep unrelated server settings and credentials unchanged.
|
|
8
|
+
|
|
9
|
+
For a level 3 weekly job, also follow the generated `vm/jobs/README.md` migration to the separate `weekly-audit.env` file and reload any copied systemd unit.
|
|
10
|
+
|
|
5
11
|
## Install walkthrough
|
|
6
12
|
|
|
7
13
|

|
|
@@ -71,7 +77,7 @@ An install has two targets, and a scripted run should set both.
|
|
|
71
77
|
| `--dir` | `./ai-orchestrator` | the docs, protocols and (level 2+) `bin/cli-run.mjs`. Named after what it contains, not after this package, so a project can hold one without looking like a checkout of it. Pass `--dir ./model-orchestrator` if you prefer the package name. |
|
|
72
78
|
| `--project` | the current directory | the main agent's supported subagent definitions: `.claude/agents/` for Claude Code or `.agents/agents/` for Antigravity; Claude Code's hook scripts in `.claude/hooks/`. With activation enabled, the catalog-supported rules file, Claude Code's `.claude/settings.json` and selected companion entries in its `.mcp.json` are merged with backups. |
|
|
73
79
|
|
|
74
|
-
`--project`
|
|
80
|
+
`--project` defaults to the current directory. Home-level agent configuration is refused: a Claude Code install targeting your home folder cannot write its subagents, hooks or project settings there. Choose a project folder below your home directory and set `--project` explicitly. The installer prints the resolved project path in the plan and says when you left it at the default.
|
|
75
81
|
|
|
76
82
|
Rules inside the project use project-relative snippet paths, so moving the whole project preserves them. Rules outside the project use absolute paths and carry a relocation note. After moving those rules, re-run the installer or set `MODEL_ORCHESTRATOR_RULES_DIR` for the installed rule-reading hooks, and update your agent instruction paths. An absolute override names the new folder; a relative override is relative to `CLAUDE_PROJECT_DIR`. `route-metrics` reads no rules and keeps its home-directory log. The separately installed Claude Code plugin keeps its existing default-path lookup.
|
|
77
83
|
|
|
@@ -6,6 +6,7 @@ This page summarizes completed reviews recorded in the [changelog](../CHANGELOG.
|
|
|
6
6
|
|
|
7
7
|
| Release | Review recorded | What the review found | What changed and where it is checked |
|
|
8
8
|
|---|---|---|---|
|
|
9
|
+
| 1.0.1 | Codex Security scan and regression-backed remediation | A metrics summary executed project code; manifest reads and check descendants needed bounds; weekly jobs inherited gateway credentials | Packaged metrics dispatch, bounded file readers, check process-tree cleanup, stdin-only gateway probe and separate audit environment; `test/security-cli.test.js`, `test/security-vm.test.js`, `test/security-pins.test.js` |
|
|
9
10
|
| 0.1.0 | Initial review and follow-up round, with a second model-family review | Paths could escape the write roots, partial writes could remain, malformed arguments could proceed, and empty results could appear successful | Containment preflight, exclusive writes with rollback, strict flag parsing and vendor-specific result checks; `test/install.test.js`, `test/cli.test.js`, `test/judges.test.js` |
|
|
10
11
|
| 0.1.1 | Follow-up on issues #1 through #10 | Child processes could survive timeouts; a failed scheduled run could replace a good report; logs could contain provider text | Process-group cleanup, temporary report plus rename, bounded probes and fixed log codes; `test/cli.test.js`, `test/install.test.js` |
|
|
11
12
|
| 0.1.2 | Follow-up on issues #12 through #15 | Interrupt cleanup, split UTF-8 output and stale existing files could produce misleading results | Interrupt handlers, streaming decoders and a pre-run snapshot for `--expect-file`; `test/cli.test.js` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "model-orchestrator",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Model router for AI coding agents: installs routing rules, 8 subagents, hooks and a CLI runner so your AI picks model and effort per task and saves tokens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| File | Job |
|
|
4
4
|
|---|---|
|
|
5
|
+
| `bounded-file.js` | shared regular-file reader for manifests and check configuration: no-follow/nonblocking open, identity checks and a fixed byte cap even if a file grows. Unsafe files are refused; callers decide how to handle missing or malformed data. |
|
|
5
6
|
| `catalog.js` | the single list of levels and AIs. Add an AI here and the prompts, docs tables, delegation matrix, gateway config and installer all pick it up. Nothing else lists AIs. |
|
|
6
7
|
| `roles.js` | pure role assignment from selected catalog capability facts, billing and selection order. Renders the stack table and manifest roles, and infers the main agent from its supported surfaces. Unknown facts remain unverified; review requires a known different model family and private work requires local execution. |
|
|
7
8
|
| `aunx.js` | command dispatch for briefs, context, checks, routing and runner calls. Route suggestions read manifest roles through a capped regular-file JSON reader; symlinks and malformed files are ignored. Route lookup executes no project code. A project's runner requires explicit `--dir`. |
|
package/src/aunx.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { spawn
|
|
2
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, parse, resolve, sep } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { windowsSpawnPlan } from '../bin/cli-run.mjs';
|
|
5
|
+
import { killTree, windowsSpawnPlan } from '../bin/cli-run.mjs';
|
|
6
|
+
import { MANIFEST_BYTE_CAP, readRegularFile } from './bounded-file.js';
|
|
6
7
|
import { byId } from './catalog.js';
|
|
7
8
|
import { ROLE_SPECS } from './roles.js';
|
|
8
9
|
|
|
@@ -85,13 +86,7 @@ export function scaffold(template, target) {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
function readChecks(path) {
|
|
88
|
-
const
|
|
89
|
-
let config;
|
|
90
|
-
try {
|
|
91
|
-
const stat = fstatSync(fd);
|
|
92
|
-
if (!stat.isFile() || stat.size > 1024 * 1024) throw new Error('checks file must be a regular JSON file of at most 1 MiB');
|
|
93
|
-
config = JSON.parse(readFileSync(fd, 'utf8'));
|
|
94
|
-
} finally { closeSync(fd); }
|
|
89
|
+
const config = JSON.parse(readRegularFile(path, MANIFEST_BYTE_CAP).toString('utf8'));
|
|
95
90
|
if (!config || config.version !== 1 || !Array.isArray(config.checks) || !config.checks.length) throw new Error('expected version: 1 and a non-empty checks array');
|
|
96
91
|
const ids = new Set();
|
|
97
92
|
for (const check of config.checks) {
|
|
@@ -105,7 +100,39 @@ function readChecks(path) {
|
|
|
105
100
|
return config.checks;
|
|
106
101
|
}
|
|
107
102
|
|
|
108
|
-
|
|
103
|
+
function runCheck(command, args, options, cwd, timeoutMs) {
|
|
104
|
+
return new Promise(resolveCheck => {
|
|
105
|
+
let child, timer, settled = false, timedOut = false, interrupted;
|
|
106
|
+
const stop = () => {
|
|
107
|
+
if (!child?.pid) return;
|
|
108
|
+
try { killTree(child.pid); }
|
|
109
|
+
catch { try { child.kill('SIGKILL'); } catch { /* already exited */ } }
|
|
110
|
+
};
|
|
111
|
+
const finish = (status, signal, error) => {
|
|
112
|
+
if (settled) return;
|
|
113
|
+
settled = true;
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
process.off('SIGINT', onInterrupt);
|
|
116
|
+
process.off('SIGTERM', onTerminate);
|
|
117
|
+
stop();
|
|
118
|
+
resolveCheck({ status, signal, error: timedOut ? { code: 'ETIMEDOUT' } : error, interrupted });
|
|
119
|
+
};
|
|
120
|
+
const onSignal = signal => { interrupted ||= signal; stop(); };
|
|
121
|
+
const onInterrupt = () => onSignal('SIGINT');
|
|
122
|
+
const onTerminate = () => onSignal('SIGTERM');
|
|
123
|
+
process.on('SIGINT', onInterrupt);
|
|
124
|
+
process.on('SIGTERM', onTerminate);
|
|
125
|
+
try {
|
|
126
|
+
child = spawn(command, args, { ...options, cwd, stdio: 'inherit', detached: process.platform !== 'win32' });
|
|
127
|
+
child.once('error', error => finish(null, null, error));
|
|
128
|
+
child.once('exit', (status, signal) => finish(status, signal));
|
|
129
|
+
timer = setTimeout(() => { timedOut = true; stop(); }, timeoutMs);
|
|
130
|
+
if (interrupted) stop();
|
|
131
|
+
} catch (error) { finish(null, null, error); }
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function runChecks(file) {
|
|
109
136
|
const path = resolve(file);
|
|
110
137
|
// Validate the whole file before the first command can mutate anything.
|
|
111
138
|
const checks = readChecks(path);
|
|
@@ -133,10 +160,11 @@ export function runChecks(file) {
|
|
|
133
160
|
}
|
|
134
161
|
({ command, args, options = {} } = plan);
|
|
135
162
|
}
|
|
136
|
-
const result =
|
|
163
|
+
const result = await runCheck(command, args, options, cwd, check.timeoutMs || 30000);
|
|
137
164
|
const pass = !result.error && result.status === 0;
|
|
138
165
|
failed ||= !pass;
|
|
139
166
|
console.log(`${pass ? 'PASS' : 'FAIL'} ${check.id}: ${result.error ? result.error.code : result.signal ? `signal ${result.signal}` : `exit ${result.status}`}`);
|
|
167
|
+
if (result.interrupted) return result.interrupted === 'SIGINT' ? 130 : 143;
|
|
140
168
|
}
|
|
141
169
|
return failed ? 1 : 0;
|
|
142
170
|
}
|
|
@@ -149,25 +177,10 @@ export function readManifestRoles({ dir, cwd = process.cwd() } = {}) {
|
|
|
149
177
|
join(cwd, 'ai-orchestrator', 'MANIFEST.json'),
|
|
150
178
|
join(cwd, 'MANIFEST.json')
|
|
151
179
|
];
|
|
152
|
-
const maxBytes = 1024 * 1024;
|
|
153
180
|
const roleIds = new Set(ROLE_SPECS.map(spec => spec.id));
|
|
154
181
|
for (const path of new Set(candidates)) {
|
|
155
|
-
let fd;
|
|
156
182
|
try {
|
|
157
|
-
const
|
|
158
|
-
if (!before.isFile() || before.isSymbolicLink() || before.size > maxBytes) continue;
|
|
159
|
-
fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
|
|
160
|
-
const after = fstatSync(fd);
|
|
161
|
-
if (!after.isFile() || after.size > maxBytes || after.dev !== before.dev || after.ino !== before.ino) continue;
|
|
162
|
-
const buffer = Buffer.alloc(maxBytes + 1);
|
|
163
|
-
let length = 0;
|
|
164
|
-
while (length < buffer.length) {
|
|
165
|
-
const count = readSync(fd, buffer, length, buffer.length - length, null);
|
|
166
|
-
if (!count) break;
|
|
167
|
-
length += count;
|
|
168
|
-
}
|
|
169
|
-
if (length > maxBytes) continue;
|
|
170
|
-
const manifest = JSON.parse(buffer.toString('utf8', 0, length));
|
|
183
|
+
const manifest = JSON.parse(readRegularFile(path, MANIFEST_BYTE_CAP).toString('utf8'));
|
|
171
184
|
const roles = manifest?.roles;
|
|
172
185
|
if (!roles || typeof roles !== 'object' || Array.isArray(roles) || !Object.keys(roles).length) continue;
|
|
173
186
|
if (Object.entries(roles).some(([id, role]) => !roleIds.has(id) || !role || typeof role !== 'object' || Array.isArray(role)
|
|
@@ -177,8 +190,6 @@ export function readManifestRoles({ dir, cwd = process.cwd() } = {}) {
|
|
|
177
190
|
return roles;
|
|
178
191
|
} catch {
|
|
179
192
|
// Missing, invalid, non-regular or unreadable JSON is an absent manifest.
|
|
180
|
-
} finally {
|
|
181
|
-
if (fd !== undefined) closeSync(fd);
|
|
182
193
|
}
|
|
183
194
|
}
|
|
184
195
|
return null;
|
|
@@ -233,8 +244,7 @@ export async function main(args) {
|
|
|
233
244
|
return runNode(join(ROOT, 'bin', 'cli-run.mjs'), parsed.rest);
|
|
234
245
|
}
|
|
235
246
|
if (command === 'route-metrics') {
|
|
236
|
-
|
|
237
|
-
return runNode(regular(local) ? local : join(ROOT, 'templates', 'agents', 'snippets', 'route-metrics.mjs'), rest.length ? rest : ['--summary']);
|
|
247
|
+
return runNode(join(ROOT, 'templates', 'agents', 'snippets', 'route-metrics.mjs'), rest.length ? rest : ['--summary']);
|
|
238
248
|
}
|
|
239
249
|
if (command === 'brief' && rest.length === 0) { process.stdout.write(readFileSync(join(COMMON, 'TASK_BRIEF.md'), 'utf8')); return 0; }
|
|
240
250
|
if (['brief', 'context', 'checks'].includes(command)) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
export const MANIFEST_BYTE_CAP = 1024 * 1024;
|
|
4
|
+
const refused = message => Object.assign(new Error(message), { code: 'UNSAFE_FILE' });
|
|
5
|
+
|
|
6
|
+
// A fixed buffer also bounds files that grow after the initial size check.
|
|
7
|
+
export function readBounded(fd, maxBytes) {
|
|
8
|
+
const stat = fstatSync(fd);
|
|
9
|
+
if (!stat.isFile()) throw refused('expected a regular file');
|
|
10
|
+
if (stat.size > maxBytes) throw refused(`file exceeds byte limit ${maxBytes}`);
|
|
11
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
12
|
+
let length = 0;
|
|
13
|
+
while (length < buffer.length) {
|
|
14
|
+
const count = readSync(fd, buffer, length, buffer.length - length, null);
|
|
15
|
+
if (!count) break;
|
|
16
|
+
length += count;
|
|
17
|
+
}
|
|
18
|
+
if (length > maxBytes) throw refused(`file exceeds byte limit ${maxBytes}`);
|
|
19
|
+
return buffer.subarray(0, length);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function readRegularFile(path, maxBytes) {
|
|
23
|
+
const before = lstatSync(path);
|
|
24
|
+
if (!before.isFile() || before.isSymbolicLink()) throw refused('expected a regular file, not a symlink');
|
|
25
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
|
|
26
|
+
try {
|
|
27
|
+
const after = fstatSync(fd);
|
|
28
|
+
if (!after.isFile() || after.dev !== before.dev || after.ino !== before.ino) throw refused('file changed during inspection');
|
|
29
|
+
return readBounded(fd, maxBytes);
|
|
30
|
+
} finally { closeSync(fd); }
|
|
31
|
+
}
|
package/src/catalog.js
CHANGED
|
@@ -400,7 +400,7 @@ export const TOOLS = [
|
|
|
400
400
|
name: 'codecalc (calculator, code runner, logic checker for your agent)',
|
|
401
401
|
repo: 'https://github.com/The-40-Thieves/codecalc',
|
|
402
402
|
role: 'exact arithmetic, code execution in 31 languages, SMT logic checks, complexity and equivalence proofs; offline, no key, no telemetry',
|
|
403
|
-
install
|
|
403
|
+
get install() { return `uvx 'codecalc[full]==${this.pin}' setup --write`; },
|
|
404
404
|
pin: '0.5.0',
|
|
405
405
|
mcpSnippets: { 'claude-code': 'mcp/mcpServers.json', codex: 'mcp/codex.config.toml', agy: 'mcp/agy.mcp_config.json', qwen: 'mcp/mcpServers.json' },
|
|
406
406
|
requires: 'uv (https://docs.astral.sh/uv/) and Python 3.10+',
|
|
@@ -413,7 +413,7 @@ export const TOOLS = [
|
|
|
413
413
|
name: 'obsidian-tc (governed memory: an agent-ready MCP server over an Obsidian vault)',
|
|
414
414
|
repo: 'https://github.com/The-40-Thieves/obsidian-tc',
|
|
415
415
|
role: 'durable memory and record for your agents: hybrid retrieval (BM25 + dense + link graph), backlinks, compare-and-swap writes with a confirmation gate, folder ACLs, a poison scan on inferred writes; 163 tools, local by default',
|
|
416
|
-
install
|
|
416
|
+
get install() { return `npm install -g obsidian-tc@${this.pin} && obsidian-tc /path/to/your/vault`; },
|
|
417
417
|
pin: '1.26.0',
|
|
418
418
|
mcpSnippets: { 'claude-code': 'mcp/obsidian-tc.mcpServers.json', codex: 'mcp/obsidian-tc.codex.config.toml', agy: 'mcp/obsidian-tc.agy.mcp_config.json', qwen: 'mcp/obsidian-tc.mcpServers.json' },
|
|
419
419
|
requires: 'an Obsidian vault folder (the Obsidian app itself is only needed for live plugin bridges); Node 24+ or Bun 1.1+ (stricter than this installer); Ollama with `nomic-embed-text` for local embeddings, or a cloud embeddings key; the Local REST API plugin only for bridge tools',
|
|
@@ -426,11 +426,11 @@ export const TOOLS = [
|
|
|
426
426
|
name: 'Context7 (Upstash: version-aware docs for the libraries your agent calls)',
|
|
427
427
|
repo: 'https://github.com/upstash/context7',
|
|
428
428
|
role: 'up-to-date, version-specific documentation and code examples for libraries, SDKs, APIs and CLIs, pulled into the prompt; tells the agent what the code is SUPPOSED to do. Paired with codecalc, which runs the code and proves what it actually does: docs never stand as proof, and where they disagree the run wins',
|
|
429
|
-
install
|
|
429
|
+
get install() { return `npx -y @upstash/context7-mcp@${this.pin}`; },
|
|
430
430
|
pin: '4.1.1',
|
|
431
431
|
mcpSnippets: { 'claude-code': 'mcp/context7.claude-code.mcp.json', codex: 'mcp/context7.codex.config.toml', agy: 'mcp/context7.agy.mcp_config.json', qwen: 'mcp/context7.qwen.settings.json' },
|
|
432
432
|
requires: 'Node.js 18+ for the local server or the ctx7 CLI; a free CONTEXT7_API_KEY is optional, for higher rate limits (it works anonymously at the base rate)',
|
|
433
|
-
autoClients: [
|
|
433
|
+
autoClients: [], // The pinned MCP server does not register itself; merge its snippets.
|
|
434
434
|
recommended: false,
|
|
435
435
|
optionalNote: 'Optional, and from a different maintainer than codecalc and obsidian-tc (Upstash, not The-40-Thieves). Needs a network call even at the anonymous rate; skip it offline. MIT.'
|
|
436
436
|
}
|
package/src/install.js
CHANGED
|
@@ -9,6 +9,7 @@ import { ROLE_SPECS, assignRoles, roleTable, roleRoute, manifestRoles, inferPrim
|
|
|
9
9
|
import { LANE_FLAGS } from '../bin/cli-run.mjs';
|
|
10
10
|
import { AIS, LEVELS, TOOLS, PROVIDERS, IMAGES, byId, toolById, providerById, npmSpec, summaryWithEvidence } from './catalog.js';
|
|
11
11
|
import { companionRegistrationSteps } from './apply-companions.js';
|
|
12
|
+
import { MANIFEST_BYTE_CAP, readRegularFile } from './bounded-file.js';
|
|
12
13
|
|
|
13
14
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
export const GENERATOR_VERSION = JSON.parse(readFileSync(join(HERE, '..', 'package.json'), 'utf8')).version;
|
|
@@ -1007,10 +1008,11 @@ export function fileClass(rel, separator = sep) {
|
|
|
1007
1008
|
|
|
1008
1009
|
export function readManifest(dir) {
|
|
1009
1010
|
try {
|
|
1010
|
-
const j = JSON.parse(
|
|
1011
|
+
const j = JSON.parse(readRegularFile(join(resolve(dir), 'MANIFEST.json'), MANIFEST_BYTE_CAP).toString('utf8'));
|
|
1011
1012
|
return j && typeof j === 'object' ? j : null;
|
|
1012
|
-
} catch {
|
|
1013
|
-
return null;
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
if (error.code === 'ENOENT' || error instanceof SyntaxError) return null;
|
|
1015
|
+
throw error;
|
|
1014
1016
|
}
|
|
1015
1017
|
}
|
|
1016
1018
|
|
package/src/uninstall.js
CHANGED
|
@@ -4,6 +4,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32 } fr
|
|
|
4
4
|
import { dirProblems, globalConfigProblem, realRoot } from './install.js';
|
|
5
5
|
import { START, END } from './apply-snippets.js';
|
|
6
6
|
import { validateActivationOwnership } from './activation-ownership.js';
|
|
7
|
+
import { MANIFEST_BYTE_CAP, readBounded } from './bounded-file.js';
|
|
7
8
|
|
|
8
9
|
const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
9
10
|
const object = (value) => value && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -70,7 +71,7 @@ function readRegular(item) {
|
|
|
70
71
|
try {
|
|
71
72
|
const actual = fstatSync(fd);
|
|
72
73
|
if (!actual.isFile() || actual.dev !== expected.dev || actual.ino !== expected.ino) throw refused(`file changed during inspection: ${item.abs}`);
|
|
73
|
-
return { bytes: readFileSync(fd), stat: actual };
|
|
74
|
+
return { bytes: item.maxBytes ? readBounded(fd, item.maxBytes) : readFileSync(fd), stat: actual };
|
|
74
75
|
} finally { closeSync(fd); }
|
|
75
76
|
}
|
|
76
77
|
|
|
@@ -176,7 +177,7 @@ function applyRemoval(item, plan) {
|
|
|
176
177
|
// inventory, never authority to expand the two roots supplied by the caller.
|
|
177
178
|
export function uninstallFiles({ dir, project, dry = false }) {
|
|
178
179
|
const roots = { dir: targetRoot(dir), project: targetRoot(project) };
|
|
179
|
-
const manifest = entry('MANIFEST.json', roots);
|
|
180
|
+
const manifest = { ...entry('MANIFEST.json', roots), maxBytes: MANIFEST_BYTE_CAP };
|
|
180
181
|
const saved = readRegular(manifest);
|
|
181
182
|
if (!saved) throw refused(`missing manifest: ${join(resolve(dir), 'MANIFEST.json')}`);
|
|
182
183
|
let data;
|
|
@@ -10,6 +10,14 @@ These are variable **names**. The values live in a secrets manager and are injec
|
|
|
10
10
|
|
|
11
11
|
`GATEWAY_MASTER_KEY` is the bearer every client presents to the gateway. Generate it once (`openssl rand -hex 32`), store it in the manager, never paste it into a file here. It must be a single token matching `^[A-Za-z0-9._-]+$`: the audit job interpolates it into curl's config grammar and refuses anything else.
|
|
12
12
|
|
|
13
|
+
## Gateway and scheduled audit environments
|
|
14
|
+
|
|
15
|
+
Inject the provider names above only into the environment used to launch the gateway with Compose. The weekly audit service instead reads `~/.config/ai-orchestrator/weekly-audit.env`, outside the installation and mode 600, containing only `GATEWAY_MASTER_KEY`. Do not point that service at the gateway's provider-key file. Existing installations must update and reload their copied systemd unit when adopting this template.
|
|
16
|
+
|
|
17
|
+
The audit script removes `GATEWAY_MASTER_KEY`, `LITELLM_MASTER_KEY`, and the gateway provider names `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `XAI_API_KEY`, and `OPENROUTER_API_KEY` from child environments. It supplies the gateway header to curl through stdin, without a credential temp file or an argv value. Newlines anywhere in the key are rejected before collection.
|
|
18
|
+
|
|
19
|
+
Vendor version probes and the report worker retain `HOME`, `PATH`, stored vendor sign-ins, and unrelated authentication variables. Configure the selected lane's own sign-in in the systemd user's account, such as `hermes auth add <provider>` for Hermes. An API-only lane relying solely on one of the removed provider variables needs vendor-supported stored authentication before this scheduled job can run; gateway keys are not a substitute for that setup.
|
|
20
|
+
|
|
13
21
|
## Rules
|
|
14
22
|
|
|
15
23
|
- Never print a value in a terminal or a log. Verify by length or by a hash prefix.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# This gateway receives provider credentials from its own launch environment.
|
|
2
|
+
# The weekly audit uses a separate environment containing only the gateway bearer.
|
|
2
3
|
# It is bound to loopback. Put it behind a private mesh network if other
|
|
3
4
|
# machines need it; never publish it on 0.0.0.0.
|
|
4
5
|
services:
|
|
@@ -21,7 +21,27 @@ systemctl --user list-timers # it should be listed with a next-run time
|
|
|
21
21
|
loginctl enable-linger "$USER" # so user timers run without a login session
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
The service reads `GATEWAY_MASTER_KEY` from
|
|
24
|
+
The service reads only `GATEWAY_MASTER_KEY` from `~/.config/ai-orchestrator/weekly-audit.env`, outside this folder with mode 600. Provision that audit-only file from your secrets manager and point `EnvironmentFile=` at it before installing. Keep the gateway's provider-key environment separate. Existing installs must replace or edit their copied service, then run `systemctl --user daemon-reload`; updating the source template alone does not update the installed unit. The key must be a single token matching `^[A-Za-z0-9._-]+$`; any embedded or trailing newline is refused.
|
|
25
|
+
|
|
26
|
+
Sign the selected vendor CLI in under the same user before enabling the timer. The job preserves `HOME`, `PATH`, stored sign-in state and unrelated authentication variables, but removes `GATEWAY_MASTER_KEY`, `LITELLM_MASTER_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `XAI_API_KEY`, and `OPENROUTER_API_KEY` from all child environments. An API-only lane dependent on a removed variable needs vendor-supported stored authentication, such as `hermes auth add <provider>`, before scheduling it. Those provider keys belong in the gateway launch environment.
|
|
27
|
+
|
|
28
|
+
## Invocation, dependencies, reads and writes
|
|
29
|
+
|
|
30
|
+
The Monday timer starts the user service, which runs Bash on `vm/jobs/weekly-audit.sh`. The script queries the local gateway using curl, collects user timers and installed CLI versions, assembles a brief, and invokes `node bin/cli-run.mjs` with the selected audit lane. Dependencies are Bash, Node, curl, jq, pgrep, standard shell utilities, the systemd user session, and the selected signed-in CLI. The gateway probe may fail without preventing the report: that section becomes `UNVERIFIED`.
|
|
31
|
+
|
|
32
|
+
The script reads `protocols/gap-analysis.md`, `DELEGATION_MATRIX.md` (falling back to `ORCHESTRATOR.md`), and collected live state. It writes `reports/live-state-<stamp>.md`, the `live-state.md` symlink, `audit-brief-<stamp>.md`, and a dated report or retained failed output. Credential headers travel only through a pipe into curl's stdin. No gateway credential temp file is created.
|
|
33
|
+
|
|
34
|
+
## The closed loop
|
|
35
|
+
|
|
36
|
+
The timer schedules the job; the script records unknown probes in its brief; the worker returns a report; a clean, non-empty result replaces the dated report. The journal records its exit code and report path. **Watched by: nothing.** The operator must inspect the journal and report after a failed service or add a notifier, then update the table above. Automatic notification and remediation are not configured.
|
|
37
|
+
|
|
38
|
+
## Failure modes
|
|
39
|
+
|
|
40
|
+
- **Invalid key:** exit 2 before collection; correct the audit-only environment through the secrets manager.
|
|
41
|
+
- **Missing environment file:** systemd cannot start the service; provision the file and check its path and permissions.
|
|
42
|
+
- **Failed probe:** the live-state section says `UNVERIFIED`; check the named dependency and retry.
|
|
43
|
+
- **Missing lane or sign-in:** cli-run returns its failure code, retaining failed output and preserving the last good report. Configure that user's vendor sign-in and PATH.
|
|
44
|
+
- **Timeout:** the watchdog stops probe descendants, and the service's whole-job deadline kills its cgroup. A killed run can leave a non-secret report temp file; a later run sweeps report temps older than a day.
|
|
25
45
|
|
|
26
46
|
## Verify a job ran
|
|
27
47
|
|
|
@@ -31,11 +51,18 @@ journalctl --user -u weekly-audit.service -n 50
|
|
|
31
51
|
ls -la {{INSTALL_DIR}}/reports/
|
|
32
52
|
```
|
|
33
53
|
|
|
54
|
+
For a manual check, start `systemctl --user start weekly-audit.service`, then run the checks above. Confirm a new dated report contains the expected gap analysis and read any `UNVERIFIED` sections before treating the run as healthy. Verify the service uses the audit-only environment by inspecting its `EnvironmentFile=` path, never by printing environment values. Test coverage in the package's `test/security-vm.test.js` uses synthetic runtime credentials and stub CLIs to check stdin delivery, child environment isolation, invalid keys, and report preservation; it calls no vendor service.
|
|
55
|
+
|
|
34
56
|
## What the job guarantees
|
|
35
57
|
|
|
36
58
|
- **Bounded:** every probe (gateway, `systemctl`, each CLI `--version`) runs under a 10 s watchdog; the model call under 600 s; the unit under `TimeoutStartSec=900`, which kills the whole cgroup.
|
|
37
59
|
- **Previous report preserved:** output goes to a temp file and is renamed over `audit-<date>.md` only on a clean, non-empty run. A failed run leaves `failed-audit-<stamp>-rc<N>.md` beside it and the last good report untouched.
|
|
38
60
|
- **Boundary:** the lane runs with the strongest restriction it offers ({{AUDIT_LANE_BOUNDARY_NOTE}}). The brief's denied-actions list is an instruction, not an enforcement, for lanes without a sandbox flag.
|
|
39
61
|
- **Honest unknowns:** a probe that times out writes an `UNVERIFIED` line, which the brief tells the lane to treat as unknown, never clean.
|
|
62
|
+
- **Credential separation:** the gateway bearer is never written to a temp file or passed on argv, and gateway/provider keys are absent from version probes and the report worker's environment. Stored vendor sign-ins remain available.
|
|
40
63
|
|
|
41
64
|
A timer that has never been seen to fire is not known to work. Run `systemctl --user start weekly-audit.service` once by hand and read the journal before trusting the schedule.
|
|
65
|
+
|
|
66
|
+
## Source of truth
|
|
67
|
+
|
|
68
|
+
The installed `vm/jobs/weekly-audit.sh`, `weekly-audit.service`, and `weekly-audit.timer` define behavior; the copied unit in `~/.config/systemd/user/` is what systemd runs. This README describes that job end to end. Live scheduling and vendor authentication remain UNVERIFIED until the manual run succeeds on your VM.
|
|
@@ -8,8 +8,10 @@ WorkingDirectory={{INSTALL_DIR_SYSTEMD}}
|
|
|
8
8
|
# Add absolute Node and vendor CLI directories if they live outside these defaults.
|
|
9
9
|
# systemd does not expand shell variables in Environment=.
|
|
10
10
|
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
11
|
-
#
|
|
12
|
-
|
|
11
|
+
# Audit-only file OUTSIDE this repo, mode 600: GATEWAY_MASTER_KEY only.
|
|
12
|
+
# Provider credentials stay in the separate gateway/Compose environment.
|
|
13
|
+
# Vendor CLIs use this user's stored sign-in state. Edit the path if needed.
|
|
14
|
+
EnvironmentFile=%h/.config/ai-orchestrator/weekly-audit.env
|
|
13
15
|
ExecStart=/bin/bash "{{INSTALL_DIR_SYSTEMD}}/vm/jobs/weekly-audit.sh"
|
|
14
16
|
# Whole-job deadline: collection probes (6 x 10 s) + the 600 s model call + cleanup.
|
|
15
17
|
# On expiry systemd kills the whole cgroup, so nothing the job spawned survives.
|
|
@@ -14,6 +14,20 @@ AUDIT_LANE="{{AUDIT_LANE}}"
|
|
|
14
14
|
AUDIT_LANE_FLAGS="{{AUDIT_LANE_FLAGS}}"
|
|
15
15
|
PROBE_SECS="${PROBE_SECS:-10}" # per collection probe
|
|
16
16
|
RUNNER_SECS="${RUNNER_SECS:-600}" # the model call; TimeoutStartSec in the unit covers the whole job
|
|
17
|
+
# Keep the probe credential in this shell only. Gateway provider keys belong to
|
|
18
|
+
# Compose, not to collection tools or the scheduled vendor CLI. Stored vendor
|
|
19
|
+
# sign-ins, HOME, PATH, and unrelated authentication variables remain available.
|
|
20
|
+
KEY="${GATEWAY_MASTER_KEY:-}"
|
|
21
|
+
export -n KEY
|
|
22
|
+
unset GATEWAY_MASTER_KEY LITELLM_MASTER_KEY ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY XAI_API_KEY OPENROUTER_API_KEY
|
|
23
|
+
|
|
24
|
+
# A shell pattern checks the whole value, including embedded/trailing newlines.
|
|
25
|
+
# Line-oriented grep accepts a valid line even when another line is malformed.
|
|
26
|
+
case "$KEY" in
|
|
27
|
+
*[!A-Za-z0-9._-]*)
|
|
28
|
+
echo "weekly-audit: GATEWAY_MASTER_KEY must match ^[A-Za-z0-9._-]+$ (generate it with: openssl rand -hex 32)" >&2
|
|
29
|
+
exit 2 ;;
|
|
30
|
+
esac
|
|
17
31
|
{{AUDIT_LANE_GUARD}}
|
|
18
32
|
cd "$INSTALL_DIR" || { echo "weekly-audit: $INSTALL_DIR missing" >&2; exit 2; }
|
|
19
33
|
mkdir -p reports
|
|
@@ -47,7 +61,9 @@ killtree() {
|
|
|
47
61
|
bounded() {
|
|
48
62
|
local secs="$1"; shift
|
|
49
63
|
local fired; fired="$(mktemp "${TMPDIR:-/tmp}/wa-fired.XXXXXX" 2>/dev/null)" && rm -f "$fired"
|
|
50
|
-
|
|
64
|
+
# Bash otherwise replaces stdin with /dev/null for asynchronous commands.
|
|
65
|
+
# Preserve the caller's pipe explicitly so curl can read its config on stdin.
|
|
66
|
+
( "$@" ) <&0 & local pid=$!
|
|
51
67
|
( sleep "$secs"; [ -n "$fired" ] && : > "$fired"; killtree "$pid" ) >/dev/null 2>&1 & local wd=$!
|
|
52
68
|
wait "$pid" 2>/dev/null; local rc=$?
|
|
53
69
|
killtree "$wd" >/dev/null 2>&1; wait "$wd" 2>/dev/null
|
|
@@ -55,29 +71,19 @@ bounded() {
|
|
|
55
71
|
return $rc
|
|
56
72
|
}
|
|
57
73
|
|
|
58
|
-
# The gateway key must be a single token: it is interpolated into curl's config
|
|
59
|
-
# grammar, and a quote or newline in it would become a second directive.
|
|
60
|
-
KEY="${GATEWAY_MASTER_KEY:-}"
|
|
61
|
-
if [ -n "$KEY" ] && ! printf '%s' "$KEY" | grep -Eq '^[A-Za-z0-9._-]+$'; then
|
|
62
|
-
echo "weekly-audit: GATEWAY_MASTER_KEY must match ^[A-Za-z0-9._-]+$ (generate it with: openssl rand -hex 32)" >&2
|
|
63
|
-
exit 2
|
|
64
|
-
fi
|
|
65
|
-
|
|
66
74
|
DATE="$(date -u +%F)"
|
|
67
75
|
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
68
76
|
{
|
|
69
77
|
echo "# live state $DATE"; echo
|
|
70
78
|
echo "## gateway lanes"
|
|
71
79
|
if [ -n "$KEY" ]; then
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if ! bounded "$PROBE_SECS" curl -s --connect-timeout 5 --max-time "$PROBE_SECS" --max-filesize 1048576 --config "$CFG" http://127.0.0.1:4000/v1/models \
|
|
80
|
+
# No secret file or argv value: the builtin printf feeds curl through a pipe.
|
|
81
|
+
# curl's own timeouts AND the watchdog bound it, including a hung reader.
|
|
82
|
+
if ! printf 'header = "Authorization: Bearer %s"\n' "$KEY" \
|
|
83
|
+
| bounded "$PROBE_SECS" curl -s --connect-timeout 5 --max-time "$PROBE_SECS" --max-filesize 1048576 --config - http://127.0.0.1:4000/v1/models \
|
|
77
84
|
| jq -r '.data[].id' 2>/dev/null; then
|
|
78
85
|
echo "UNVERIFIED: gateway unreachable or timed out within ${PROBE_SECS}s"
|
|
79
86
|
fi
|
|
80
|
-
rm -f "$CFG"
|
|
81
87
|
else
|
|
82
88
|
echo "UNVERIFIED: GATEWAY_MASTER_KEY not set; gateway not queried"
|
|
83
89
|
fi
|
|
@@ -91,6 +97,7 @@ STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
|
91
97
|
fi
|
|
92
98
|
done
|
|
93
99
|
} > "reports/live-state-$STAMP.md"
|
|
100
|
+
unset KEY
|
|
94
101
|
ln -sfn "live-state-$STAMP.md" reports/live-state.md
|
|
95
102
|
|
|
96
103
|
# The brief the worker actually reads: the protocol, the intended configuration,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
- Scaffold a checks file with `aunx checks`. Replace the intentionally failing example with a real verifier before using the file.
|
|
5
5
|
- Prefer an argv array for `command`, for example `["node", "--test", "test/example.test.js"]`. String commands run through the local shell, so review them as executable code before running a checks file.
|
|
6
6
|
- Set `cwd` relative to the checks file. Keep checks inside the task's authorized scope.
|
|
7
|
+
- Checks run sequentially with inherited terminal output. A timeout stops the command and its ordinary descendants; interrupts stop the run. A finished check must not leave a background service running. This lifecycle cleanup is not a sandbox for hostile programs that escape their process group.
|
|
7
8
|
- Demonstrate a failing case for each new gate before trusting a passing result.
|
|
8
9
|
- Include availability facts when later work depends on a tool, permission or service remaining accessible.
|
|
9
10
|
- Run `aunx checks run ACCEPTANCE_CHECKS.json` against the final artifact. A failed command gives the gate exit code 1.
|
|
@@ -9,13 +9,13 @@ What it gives every agent in this folder: exact arithmetic (`evaluate_expression
|
|
|
9
9
|
Needs `uv` (https://docs.astral.sh/uv/) and Python 3.10+.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
uvx 'codecalc[full]' setup # prints what it would do, changes nothing
|
|
13
|
-
uvx 'codecalc[full]' setup --write # merges the codecalc entry into your client's config, copies the skill
|
|
12
|
+
uvx 'codecalc[full]=={{CODECALC_PIN}}' setup # prints what it would do, changes nothing
|
|
13
|
+
uvx 'codecalc[full]=={{CODECALC_PIN}}' setup --write # merges the codecalc entry into your client's config, copies the skill
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
`setup` detects Claude Desktop, Claude Code, Cursor, VS Code and Zed (`--client=NAME` if several), runs two real canaries and ends in one verdict: `ready` / `degraded` / `not-ready`. It backs up the client config it touches. `uvx 'codecalc[full]' doctor` prints a config block with your absolute paths.
|
|
16
|
+
`setup` detects Claude Desktop, Claude Code, Cursor, VS Code and Zed (`--client=NAME` if several), runs two real canaries and ends in one verdict: `ready` / `degraded` / `not-ready`. It backs up the client config it touches. `uvx 'codecalc[full]=={{CODECALC_PIN}}' doctor` prints a config block with your absolute paths.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
These commands and the shipped launch snippets use the catalog version this installer was released with. Review a newer release before changing that pin. `[full]` is the edition that actually runs everything documented (about 120 MB). Base `codecalc` is execution only; symbolic tools then return a `dependency_missing` error naming the extra, never a silent failure.
|
|
19
19
|
|
|
20
20
|
## Register more agents with `mcp/` snippets
|
|
21
21
|
|
|
@@ -35,19 +35,15 @@ Two ways to connect, remote first (Context7's own documented default, and the on
|
|
|
35
35
|
# (mcp.context7.com/mcp/oauth), as an alternative to an API key, not required either.
|
|
36
36
|
|
|
37
37
|
# Local alternative: runs the MCP server on your machine over stdio.
|
|
38
|
-
npx -y @upstash/context7-mcp
|
|
39
|
-
|
|
40
|
-
# Or the one-command setup Context7 itself ships, which authenticates via OAuth,
|
|
41
|
-
# writes an API key, and can install a CLI-based skill instead of MCP:
|
|
42
|
-
npx ctx7 setup
|
|
38
|
+
npx -y @upstash/context7-mcp@{{CONTEXT7_PIN}}
|
|
43
39
|
```
|
|
44
40
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
`npx ctx7 setup` is upstream's own guided installer; it is not run by this installer, only documented here, the same way this project never runs a vendor script for you.
|
|
41
|
+
The local command and shipped launch snippets use the catalog version this installer was released against. Review a newer release before changing that pin. The remote endpoint is operated by Upstash and cannot be version-pinned by this package. This installer runs neither connection nor a third-party setup command for you.
|
|
48
42
|
|
|
49
43
|
## Register it with your agent (snippets in `mcp/`)
|
|
50
44
|
|
|
45
|
+
When Context7 is selected and project activation is enabled, model-orchestrator merges its entry into a supported project MCP configuration, including Claude Code's `.mcp.json`. Existing conflicting entries are kept for review. With activation disabled or a global client configuration, merge the relevant snippet manually.
|
|
46
|
+
|
|
51
47
|
Every snippet below ships **keyless**: the remote ones point at the hosted endpoint with no `Authorization` header at all (Qwen Code's snippet keeps the non-credential `Accept` header upstream itself ships), and both Zed snippets run the local, version-pinned `npx` server with no key in its `env` block. That is deliberate, not an oversight: see "Higher rate limits" next for why a header is not shipped by default.
|
|
52
48
|
|
|
53
49
|
| Agent | File to edit | Snippet |
|
|
@@ -87,12 +83,12 @@ Anonymous works. If you hit the rate limit and want a key, add it the correct wa
|
|
|
87
83
|
- **Codex CLI**: under the `[mcp_servers.context7]` table in `~/.codex/config.toml`, add a `bearer_token_env_var` entry naming the environment variable `CONTEXT7_API_KEY`. Codex reads the token from that variable at connect time and sends it as the `Authorization` header itself; the config file never holds the value ([Codex MCP docs](https://developers.openai.com/codex/mcp)).
|
|
88
84
|
- **Claude Code** (`mcp/context7.claude-code.mcp.json`): add a `headers` object to the `context7` entry with an `Authorization` field whose value is `Bearer` followed by a `${CONTEXT7_API_KEY}` reference. Claude Code expands `${VAR}` references in a remote server's `headers` at load time, and `CONTEXT7_API_KEY` is not one of the credential names it deliberately reads as empty (those are Claude/Anthropic-specific). Set the variable in your environment before launching; an unset variable still loads with the literal, unexpanded reference sent as the header, and Context7 answers every call with "Invalid API key" instead of running anonymously ([Claude Code MCP docs](https://code.claude.com/docs/en/mcp)). That failure, not a missing feature, is why this snippet ships with no header at all.
|
|
89
85
|
- **Claude Desktop**: the Connectors UI has its own key field; use it there rather than editing a file.
|
|
90
|
-
- **Any local `npx` connection** (Zed, or the local alternative for any other client): export `CONTEXT7_API_KEY` in the shell that launches your editor or agent. A spawned stdio child process inherits its parent's environment by default, so `npx -y @upstash/context7-mcp` picks it up with no config edit; this is the same environment variable name Context7's own Docker MCP Toolkit config and its GitHub Copilot integration use to feed the server a key. If your client does not pass its environment through to the child (uncommon), either stay anonymous, or check whether that client's own config format has an `env` block that itself supports an environment-variable reference (Claude Code's does, described above; not every client's does) rather than typing the key in.
|
|
86
|
+
- **Any local `npx` connection** (Zed, or the local alternative for any other client): export `CONTEXT7_API_KEY` in the shell that launches your editor or agent. A spawned stdio child process inherits its parent's environment by default, so `npx -y @upstash/context7-mcp@{{CONTEXT7_PIN}}` picks it up with no config edit; this is the same environment variable name Context7's own Docker MCP Toolkit config and its GitHub Copilot integration use to feed the server a key. If your client does not pass its environment through to the child (uncommon), either stay anonymous, or check whether that client's own config format has an `env` block that itself supports an environment-variable reference (Claude Code's does, described above; not every client's does) rather than typing the key in.
|
|
91
87
|
- **Cursor, VS Code, Qwen Code, Antigravity `agy`, or any other client using the `mcpServers.json`/`vscode.mcp.json`/`qwen.settings.json`/`agy.mcp_config.json` snippet**: check that client's own docs for whether it expands an environment-variable reference inside a remote server's `headers` before adding one. This is not confirmed for any of them here. If it does not expand, the literal, unexpanded text becomes the header value and every call fails with "Invalid API key" instead of running anonymously, which is worse than shipping no header at all.
|
|
92
88
|
|
|
93
89
|
## Security posture, read before you send anything through it
|
|
94
90
|
|
|
95
|
-
|
|
91
|
+
Library names and query text are sent to Context7's API. If an agent includes source code, credentials or private context in a query, that content leaves the machine too. Review what your agent sends and use public library names and non-sensitive queries. The local stdio server still calls the remote service; it does not make lookups private or offline. The docs it indexes are community-contributed, not vetted by this installer; report suspicious results upstream. Keep API keys in your environment or your client's supported credential store, never in committed files.
|
|
96
92
|
|
|
97
93
|
## Level 3
|
|
98
94
|
|
|
@@ -29,7 +29,7 @@ A durable, searchable, governed store that the protocols can call by name:
|
|
|
29
29
|
## Install
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
npm install -g obsidian-tc@{{OBSIDIAN_TC_PIN}} #
|
|
32
|
+
npm install -g obsidian-tc@{{OBSIDIAN_TC_PIN}} # reviewed catalog version; review an upgrade before changing the pin
|
|
33
33
|
ollama pull nomic-embed-text
|
|
34
34
|
obsidian-tc /path/to/your/vault # zero-config: one vault named "main", local only
|
|
35
35
|
obsidian-tc plugin install --vault /path/to/your/vault # optional companion plugin, then enable it in Obsidian
|
|
@@ -68,7 +68,7 @@ Every snippet here spawns `npx`, and on Windows `npx` is a batch file (`npx.cmd`
|
|
|
68
68
|
|
|
69
69
|
The SDK point covers more than these two clients: every published `@modelcontextprotocol/sdk` from 1.23.0 through 1.30.0 depends on `cross-spawn ^7.0.5` and uses it in the stdio client, so any client that connects through the stock TypeScript SDK inherits the same `PATHEXT` resolution. `shell: false` in that transport is not the whole story, and reading only that line is how a client gets mistaken for one that cannot start `npx`.
|
|
70
70
|
|
|
71
|
-
One Windows case can still fail, and it is not about `.cmd`. Zed prefers PowerShell for the system shell (`get_windows_system_shell` in `crates/gpui_util/src/lib.rs` falls back to `cmd.exe` only when PowerShell is missing), and PowerShell resolves a bare `npx` to npm's `npx.ps1` shim when one is installed. Under the `Restricted` execution policy that is Windows' client default, running a `.ps1` is blocked. If Zed reports that the server would not start, check `Get-ExecutionPolicy` first, and if that is the cause, change the Zed entry by hand to `"command": "cmd"` with `"args": ["/d", "/c", "npx", "-y", "obsidian-tc"]`, keeping the rest of the block. `/d` is there on purpose: it skips any Command Processor `AutoRun` command, which would otherwise run first and can print non-JSON into the protocol stream.
|
|
71
|
+
One Windows case can still fail, and it is not about `.cmd`. Zed prefers PowerShell for the system shell (`get_windows_system_shell` in `crates/gpui_util/src/lib.rs` falls back to `cmd.exe` only when PowerShell is missing), and PowerShell resolves a bare `npx` to npm's `npx.ps1` shim when one is installed. Under the `Restricted` execution policy that is Windows' client default, running a `.ps1` is blocked. If Zed reports that the server would not start, check `Get-ExecutionPolicy` first, and if that is the cause, change the Zed entry by hand to `"command": "cmd"` with `"args": ["/d", "/c", "npx", "-y", "obsidian-tc@{{OBSIDIAN_TC_PIN}}"]`, keeping the rest of the block. `/d` is there on purpose: it skips any Command Processor `AutoRun` command, which would otherwise run first and can print non-JSON into the protocol stream.
|
|
72
72
|
|
|
73
73
|
None of this was run on a Windows machine by this project. The five verdicts are from each client's own shipped code; the PowerShell case is from Zed's shell choice plus documented `Restricted` behaviour, and is the one worth reporting back if you hit it.
|
|
74
74
|
|