rippletide-package 0.1.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.
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import {
6
+ DEFAULT_EVIDENCE_BUDGET,
7
+ collectAgentContext,
8
+ } from "../../connect/evidence.js";
9
+
10
+ function usage() {
11
+ return `Rippletide Codex Native
12
+
13
+ Usage:
14
+ rippletide-codex prepare [path] [--evidence-budget 140000]
15
+
16
+ What it does:
17
+ Collects local agent evidence and writes Codex-ready review artifacts.
18
+ It does not call Rippletide APIs or external model APIs.
19
+ `;
20
+ }
21
+
22
+ function getOptionValue(args, name) {
23
+ const equalsPrefix = `${name}=`;
24
+ const inline = args.find((arg) => arg.startsWith(equalsPrefix));
25
+ if (inline) return inline.slice(equalsPrefix.length);
26
+
27
+ const index = args.indexOf(name);
28
+ if (index >= 0 && args[index + 1] && !args[index + 1].startsWith("-")) {
29
+ return args[index + 1];
30
+ }
31
+
32
+ return null;
33
+ }
34
+
35
+ function getTargetPath(args) {
36
+ const optionValues = new Set();
37
+ const evidenceBudget = getOptionValue(args, "--evidence-budget");
38
+ if (evidenceBudget) optionValues.add(evidenceBudget);
39
+ return args.find((arg) => !arg.startsWith("-") && !optionValues.has(arg)) ?? ".";
40
+ }
41
+
42
+ function displayPath(filePath) {
43
+ const relative = path.relative(process.cwd(), filePath);
44
+ if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
45
+ return relative;
46
+ }
47
+ return filePath;
48
+ }
49
+
50
+ function createRunId() {
51
+ return new Date().toISOString().replace(/[:.]/g, "-");
52
+ }
53
+
54
+ function reviewerInstructions() {
55
+ return `# Rippletide Codex-Native Reviewer Instructions
56
+
57
+ You are running Rippletide inside Codex.
58
+
59
+ Use the evidence pack as the source of truth. First, silently understand the agent:
60
+ - identity
61
+ - intended job
62
+ - instructions
63
+ - tools/actions
64
+ - permissions and credentials
65
+ - runtime behavior
66
+ - memory/context
67
+ - gates and policies
68
+ - proof/tests/evals
69
+ - business/system footprint
70
+
71
+ Do not show a long dossier unless the user explicitly asks for it.
72
+
73
+ Your user-facing job is to produce useful findings only.
74
+
75
+ Good findings:
76
+ - are specific to this agent
77
+ - connect the agent's intended job to what the code can actually do
78
+ - avoid obvious facts the builder already knows
79
+ - name a concrete mismatch, missing proof, unclear gate, or behavior boundary
80
+ - cite exact file:line evidence
81
+ - include the smallest useful next action
82
+ - sound like a senior teammate, not a compliance report
83
+ - lead with impact, not implementation detail
84
+ - help the user decide what to do next in under 30 seconds
85
+
86
+ Hard rules:
87
+ - Do not invent runtime proof.
88
+ - Do not create findings from absence alone; tie each finding to an expected action shown in the evidence.
89
+ - Do not use generic advice like "add tests" or "validate inputs" unless it is grounded in this agent's behavior.
90
+ - Return at most 3 findings.
91
+ - Keep each finding short: title plus 4 compact fields.
92
+ - Use backticks only in the evidence line unless a code symbol is essential for understanding.
93
+ - Do not make the title a raw code symptom like "env var is string"; make it an impact statement like "Search limits can be misread at runtime."
94
+ - If there are no strong findings, say so and explain what evidence would be needed.
95
+
96
+ Return this shape:
97
+
98
+ Rippletide Review
99
+
100
+ Agent:
101
+ Found: <N> findings worth your attention.
102
+ Summary: <one sentence about what kind of agent this is and what matters most before shipping>
103
+
104
+ [1] <impact-first title>
105
+ Impact: <what can break, surprise a user, cost money/time, mutate external systems, lose trust, or hide a real failure>
106
+ Why it happens: <plain-language mechanism, no dense code narration>
107
+ Evidence: <2-4 compact file:line citations>
108
+ Suggested action: <smallest proof or fix>
109
+
110
+ [2] ...
111
+
112
+ Next: Which finding should I propose a fix for? Reply 1, 2, 3, or skip.
113
+
114
+ After the review, mention the saved Rippletide artifact folder in one short line.`;
115
+ }
116
+
117
+ function actionInstructions() {
118
+ return `# Rippletide Codex-Native Action Instructions
119
+
120
+ Use this only after the user chooses a finding or asks you to act.
121
+
122
+ Inputs:
123
+ - selected finding
124
+ - evidence-pack.md
125
+ - reviewer-instructions.md
126
+ - relevant source files from the target agent repo
127
+
128
+ Goal:
129
+ Propose the smallest useful edit or manual check for the selected finding.
130
+
131
+ Rules:
132
+ - Do not broaden scope beyond the selected finding.
133
+ - Do not rewrite architecture.
134
+ - Do not make cosmetic/doc-only edits unless the finding is a documentation mismatch.
135
+ - If the finding needs product judgment, external credentials, a live account, or runtime proof, return a manual check instead of editing.
136
+ - Before editing in Codex-native mode, show the proposed action and wait for user approval unless the user already explicitly asked you to apply it.
137
+ - The approval question is the MVP action hook: the user can apply, comment, refuse, or skip.
138
+ - When editing, use Codex's normal file-edit tools and report exactly what changed.
139
+ - After editing, run the smallest safe local proof you can actually execute.
140
+ - Prefer behavior proof: a test/eval/demo assertion that exercises the changed agent behavior.
141
+ - Treat syntax/type/compile checks as code checks only; do not call them behavior proof.
142
+ - If no safe local proof exists, say "not proven" and give the manual check. Do not claim the finding is fixed from reasoning alone.
143
+ - After proof, offer to re-run Rippletide Codex prepare/review to see whether findings changed.
144
+
145
+ Use this shape when proposing an action:
146
+
147
+ Rippletide Action
148
+
149
+ Finding: <title>
150
+ Confidence: high|medium|low
151
+ Impact: <why acting on this matters>
152
+ Proposed edit: <smallest edit or manual check>
153
+ Files: <files likely touched>
154
+ Proof: <behavior|code|manual, passed|failed|not proven, command or manual check>
155
+
156
+ Decision: Apply this edit, comment, refuse, or skip?`;
157
+ }
158
+
159
+ function readme({ root, runDir, evidencePath, reviewInstructionsPath, actionInstructionsPath }) {
160
+ return `# Rippletide Codex Run
161
+
162
+ Mode: Codex-native
163
+ Agent root: ${root}
164
+
165
+ This run prepared local evidence for Codex. It did not call Rippletide APIs or external model APIs.
166
+
167
+ ## Files
168
+
169
+ - Evidence pack: ${evidencePath}
170
+ - Reviewer instructions: ${reviewInstructionsPath}
171
+ - Action instructions: ${actionInstructionsPath}
172
+
173
+ ## Next
174
+
175
+ Codex should read the reviewer instructions and evidence pack, then produce the user-facing Rippletide Review.
176
+
177
+ If the user chooses a finding to fix, Codex should read the action instructions before proposing edits.
178
+
179
+ Run folder: ${runDir}
180
+ `;
181
+ }
182
+
183
+ async function prepare(targetPath, options = {}) {
184
+ const evidenceBudget = options.evidenceBudget ?? DEFAULT_EVIDENCE_BUDGET;
185
+ const context = await collectAgentContext(targetPath, {
186
+ evidenceBudget,
187
+ title: "Rippletide Codex Evidence Pack",
188
+ description: "This is local evidence for Codex-native agent review. It was prepared without calling Rippletide APIs or external model APIs.",
189
+ citationRule: "Citation rule: cite facts as `path/to/file.ext:line` using the line numbers shown in the excerpts.",
190
+ });
191
+ const runDir = path.join(context.root, ".rippletide", "codex", createRunId());
192
+ await mkdir(runDir, { recursive: true });
193
+
194
+ const evidencePath = path.join(runDir, "evidence-pack.md");
195
+ const reviewInstructionsPath = path.join(runDir, "reviewer-instructions.md");
196
+ const actionInstructionsPath = path.join(runDir, "action-instructions.md");
197
+ const readmePath = path.join(runDir, "README.md");
198
+ const metaPath = path.join(runDir, "run.json");
199
+
200
+ await writeFile(evidencePath, context.evidencePack, "utf8");
201
+ await writeFile(reviewInstructionsPath, reviewerInstructions(), "utf8");
202
+ await writeFile(actionInstructionsPath, actionInstructions(), "utf8");
203
+ await writeFile(readmePath, readme({
204
+ root: context.root,
205
+ runDir,
206
+ evidencePath,
207
+ reviewInstructionsPath,
208
+ actionInstructionsPath,
209
+ }), "utf8");
210
+ await writeFile(metaPath, `${JSON.stringify({
211
+ mode: "codex-native",
212
+ root: context.root,
213
+ filesFound: context.files.length,
214
+ excerptsIncluded: context.sources.length,
215
+ evidenceBudget,
216
+ runDir,
217
+ evidencePath,
218
+ reviewInstructionsPath,
219
+ actionInstructionsPath,
220
+ createdAt: new Date().toISOString(),
221
+ }, null, 2)}\n`, "utf8");
222
+
223
+ return {
224
+ ...context,
225
+ actionInstructionsPath,
226
+ evidencePath,
227
+ readmePath,
228
+ reviewInstructionsPath,
229
+ runDir,
230
+ };
231
+ }
232
+
233
+ async function main() {
234
+ const [, , command, ...args] = process.argv;
235
+
236
+ if (!command || command === "--help" || command === "-h") {
237
+ console.log(usage());
238
+ return;
239
+ }
240
+
241
+ if (command !== "prepare") {
242
+ console.error(`Unknown command: ${command}\n`);
243
+ console.error(usage());
244
+ process.exit(1);
245
+ }
246
+
247
+ if (args.includes("--help") || args.includes("-h")) {
248
+ console.log(usage());
249
+ return;
250
+ }
251
+
252
+ const evidenceBudgetValue = getOptionValue(args, "--evidence-budget");
253
+ const evidenceBudget = evidenceBudgetValue ? Number(evidenceBudgetValue) : DEFAULT_EVIDENCE_BUDGET;
254
+
255
+ if (!Number.isFinite(evidenceBudget) || evidenceBudget < 10_000) {
256
+ throw new Error("--evidence-budget must be a number greater than or equal to 10000.");
257
+ }
258
+
259
+ const result = await prepare(getTargetPath(args), { evidenceBudget });
260
+
261
+ console.log("Rippletide Codex Native");
262
+ console.log(`Mode: Codex-native, no Rippletide API call`);
263
+ console.log(`Agent: ${displayPath(result.root)}`);
264
+ console.log(`Collected: ${result.files.length} source/config/doc files and ${result.sources.length} readable excerpts`);
265
+ console.log(`Saved: ${displayPath(result.runDir)}`);
266
+ console.log("");
267
+ console.log("Next for Codex:");
268
+ console.log(`1. Read ${displayPath(result.reviewInstructionsPath)}`);
269
+ console.log(`2. Read ${displayPath(result.evidencePath)}`);
270
+ console.log("3. Produce the Rippletide Review for the user");
271
+ console.log(`4. For fixes, read ${displayPath(result.actionInstructionsPath)}`);
272
+ }
273
+
274
+ main().catch((error) => {
275
+ console.error(error instanceof Error ? error.message : String(error));
276
+ process.exit(1);
277
+ });
package/tide/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # tide — plain-English runtime policy for coding agents
2
+
3
+ > Feature flag: `--tide` on `rippletide-package`. **Status: draft / v1 scoped to Codex.**
4
+ > See [`PRD.md`](PRD.md) for the full design.
5
+
6
+ ## What it is
7
+
8
+ Write your guardrails in plain English in a `POLICY.md`. `rippletide --tide` compiles
9
+ that policy — **once**, with an LLM — into deterministic, reviewable rules, and then
10
+ **enforces them at the agent's tool-call boundary at runtime** (no LLM in the hot path).
11
+
12
+ It's the [`tide-test`](https://github.com/rippletideco/tide-test) model (prose → rules →
13
+ deterministic `allow`/`deny`/`escalate` before an action runs), but applied to an agent's
14
+ **native tools** (shell, file writes, network) instead of the **MCP transport** — so it
15
+ covers the actions that actually matter, and doesn't depend on the agent using MCP.
16
+
17
+ **v1 enforces on Codex.** Claude Code, OpenCode, and Pi ship as documented adapter
18
+ placeholders (same interface, stubbed).
19
+
20
+ ```
21
+ POLICY.md ──compile (gpt-5.4, once)──► rules.policy.yaml + ontology + golden tests
22
+
23
+ enforce (deterministic)
24
+
25
+ Codex PreToolUse hook → allow / deny / escalate
26
+ ```
27
+
28
+ ## Two phases
29
+
30
+ 1. **Compile (authoring, LLM once).** `POLICY.md` + the agent's tool manifest → `gpt-5.4`
31
+ → `ontology/actions.yaml`, `policies/rules.policy.yaml`, `tests/golden.scenarios.yaml`,
32
+ `findings.md` (the `tide-test` schema). Validated with `compile` + `test`; you review.
33
+ 2. **Enforce (runtime, deterministic).** A ported `tide-test` engine evaluates the rules on
34
+ every Codex tool call via a `PreToolUse` hook. **No LLM is called at runtime.**
35
+
36
+ ## ⚠️ The LLM step: whose key & compute?
37
+
38
+ The **compile** step (phase 1 only) calls an LLM (`gpt-5.4`) to turn prose into rules. This
39
+ is a **one-time authoring** call — **enforcement never calls an LLM**. Still, it raises a
40
+ real question: whose API key runs it, and where does the policy + tool data go? Three
41
+ options, each with a different trust/cost profile:
42
+
43
+ ### Option 1 — user's key, on the user's machine ✅ **chosen for v1**
44
+ The package uses the **user's own** `gpt-5.4` key and makes the call **locally**.
45
+ - **Pros:** nothing leaves the user's environment except to *their own* OpenAI account; no
46
+ Rippletide compute cost; simplest to ship.
47
+ - **Cons:** the user must have a `gpt-5.4` key; their `POLICY.md` and tool manifest are sent
48
+ to OpenAI under their account (acceptable for most, but it *is* egress).
49
+
50
+ ### Option 2 — send to Rippletide, generate server-side
51
+ The package uploads `POLICY.md` + the tool manifest to Rippletide, which runs the LLM and
52
+ returns the compiled bundle.
53
+ - **Pros:** no user key needed; one consistent model + prompt; we can improve generation
54
+ quality centrally and version it.
55
+ - **Cons:** the user's policy and tool inventory **leave their machine to us** — a real
56
+ privacy/trust ask, especially for enterprises; we bear the compute cost; adds a network
57
+ dependency to setup.
58
+
59
+ ### Option 3 — Rippletide's key & compute, executed on the user's side
60
+ We ship a scoped/short-lived Rippletide key (or a thin proxy) so the call runs locally on
61
+ the user's machine but on *our* account.
62
+ - **Pros:** no user key required; policy/tool data stays local (goes only to OpenAI, not to
63
+ us); we control the model.
64
+ - **Cons:** key management and leakage risk (a shipped key is exposed on the client);
65
+ cost-attribution and abuse concerns; more moving parts.
66
+
67
+ **Decision for v1: Option 1.** It's the least surprising and keeps the user in full control
68
+ of both their key and their data. We can revisit Option 2/3 once there's demand for
69
+ key-less setup or centrally-managed policy generation — the compile step is isolated behind
70
+ one interface, so switching later is a contained change.
71
+
72
+ ## Usage (v1 — Codex)
73
+
74
+ Node package (unified with the `rippletide` npm CLI). Locally: `cd tide && npm install`,
75
+ then use `node bin/tide.js <cmd>` (or the `tide` bin). Wired into the parent it's
76
+ `rippletide tide <cmd>`.
77
+
78
+ ```bash
79
+ tide compile --policy POLICY.md --project .tide # POLICY.md -> rules (LLM) + golden tests
80
+ tide test --project .tide # run the golden scenarios (deterministic)
81
+ tide check --project .tide --command "rm -rf x" # try a single command -> ALLOW/DENY
82
+ tide install --project .tide # install the Codex PreToolUse hook
83
+ tide status --project .tide # is it live? + allowed/blocked tally
84
+ ```
85
+
86
+ Then run Codex:
87
+ - **App:** *Settings → Hooks → Trust* the hook, then start a new chat.
88
+ - **CLI:** `codex --dangerously-bypass-hook-trust -c features.hooks=true`
89
+
90
+ Now a command that violates the policy is **blocked before it runs** (the reason is shown to
91
+ the model, which adapts); everything else passes. The compiled policy lives in `.tide/`
92
+ (`rules.policy.yaml`, `ontology/actions.yaml`, `tests/golden.scenarios.yaml`, `findings.md`)
93
+ — all human-readable and reviewable. `.tide/.runtime/events.jsonl` records every firing.
94
+
95
+ **Identity is simple — no export needed.** `tide compile` auto-detects the OpenAI
96
+ credential you already use — your `OPENAI_API_KEY` env var, or **your Codex login**
97
+ (`~/.codex/auth.json`) — and asks before reusing it:
98
+
99
+ ```
100
+ tide: detected you're already using OpenAI via your Codex login (~/.codex/auth.json), model gpt-5.5 (key sk-pro…6H0A).
101
+ Use the same to build your rules? [Y/n]
102
+ ```
103
+
104
+ It also matches the model your Codex uses. The key is read locally and used only for the
105
+ one-time compile call; it never leaves your machine except to OpenAI under your own account.
106
+ Override with `--api-key` / `--model`, or decline the prompt to skip the LLM. Enforcement is
107
+ local and needs no credentials.
108
+
109
+ ## Status
110
+
111
+ - **v1 shipped — Codex (Node):** compile (your key, model auto-matched) → deterministic
112
+ engine → PreToolUse hook. Verified end-to-end against real `codex` instances:
113
+ file-modifying commands **and** `apply_patch` edits are blocked; reads run; unit +
114
+ golden + e2e tests pass. Implemented in Node/ESM (`src/`) to unify with the `rippletide`
115
+ npm package — one dependency (`js-yaml`).
116
+ - **Placeholders:** Claude Code / OpenCode / Pi adapters are stubbed with their target seam
117
+ (see [`PRD.md`](PRD.md) §8). **Escalate** currently holds by blocking-with-reason (a live
118
+ approval queue is future work).
119
+
120
+ **Requirements:** Node ≥ 18, Codex installed. Tests: `tide/tests/engine.test.mjs`
121
+ (`node --test`, deterministic) and `tide/tests/e2e_codex.mjs` (spawns real `codex`; needs
122
+ an OpenAI credential).
123
+
124
+ ## Related
125
+
126
+ - [`tide-test`](https://github.com/rippletideco/tide-test) — the original MCP-gateway prototype (schema + deterministic engine we reuse).
127
+ - [`tototozip/x-ray`](https://github.com/tototozip/x-ray) — the X-ray `tide` package; its Codex `PreToolUse` hook is the basis for the Codex adapter here.
128
+ - `rippletide-platform` — the hosted Tide decision path (`/v1/tide/:agentId/decisions`), same `allow/deny/escalate` contract.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ // Tide CLI entry. Also the `--tide` feature entry point for the parent rippletide package.
3
+ import { main } from "../src/cli.js";
4
+ process.exit(await main(process.argv.slice(2)));
@@ -0,0 +1,3 @@
1
+ # AC Agent policy
2
+
3
+ Do not modify any files. The agent may read files and run read-only commands, but it must not create, edit, overwrite, move, or delete any file.
@@ -0,0 +1,173 @@
1
+ // The `tide` CLI — and the `--tide` feature entry point for the parent rippletide package.
2
+ // tide compile | test | check | install | uninstall | status
3
+ // The parent wires `rippletide --tide <args>` to `main(args)`.
4
+
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import readline from "node:readline";
8
+ import * as codex from "./codex.js";
9
+ import { compilePolicy, DEFAULT_MODEL } from "./compile.js";
10
+ import { Engine } from "./engine.js";
11
+ import { detectOpenAI, maskKey } from "./credentials.js";
12
+ import { DEFAULT_DIR, loadBundle, loadScenarios, writeBundle } from "./schema.js";
13
+
14
+ const PLACEHOLDER_SEAMS = {
15
+ claude_code: "PreToolUse hook in settings.json -> permissionDecision allow/deny/ask",
16
+ opencode: "permission rules (its hooks can't block) -> DeniedError",
17
+ pi: "a pi.on('tool_call') extension returning {block, reason}",
18
+ };
19
+
20
+ function adapter(agent) {
21
+ if (agent === "codex") return codex;
22
+ const seam = PLACEHOLDER_SEAMS[agent];
23
+ if (seam) throw new Error(`the tide adapter for '${agent}' is a placeholder in v1. Planned seam: ${seam}. Only 'codex' is supported so far.`);
24
+ throw new Error(`unknown agent '${agent}' (supported: codex; planned: ${Object.keys(PLACEHOLDER_SEAMS).join(", ")})`);
25
+ }
26
+
27
+ function parseArgs(argv) {
28
+ const cmd = argv[0];
29
+ const opts = { agent: "codex", project: DEFAULT_DIR, policy: "POLICY.md", model: null, apiKey: null, command: null };
30
+ for (let i = 1; i < argv.length; i++) {
31
+ const a = argv[i];
32
+ const next = () => argv[++i];
33
+ if (a === "--agent") opts.agent = next();
34
+ else if (a === "--project") opts.project = next();
35
+ else if (a === "--policy") opts.policy = next();
36
+ else if (a === "--model") opts.model = next();
37
+ else if (a === "--api-key") opts.apiKey = next();
38
+ else if (a === "--command") opts.command = next();
39
+ }
40
+ return { cmd, opts };
41
+ }
42
+
43
+ function confirm(question) {
44
+ if (!process.stdin.isTTY) return Promise.resolve(true); // auto-yes when non-interactive
45
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
46
+ return new Promise((resolve) => rl.question(`${question} [Y/n] `, (ans) => {
47
+ rl.close();
48
+ resolve((ans.trim().toLowerCase() || "y") === "y" || ans.trim().toLowerCase() === "yes");
49
+ }));
50
+ }
51
+
52
+ async function resolveCredential(opts) {
53
+ if (opts.apiKey) return { apiKey: opts.apiKey, model: opts.model };
54
+ const det = detectOpenAI();
55
+ if (!det) return { apiKey: null, model: opts.model };
56
+ const modelTxt = det.model ? `, model ${det.model}` : "";
57
+ console.log(`tide: detected you're already using OpenAI via ${det.source}${modelTxt} (key ${maskKey(det.apiKey)}).`);
58
+ if (await confirm(" Use the same to build your rules?")) {
59
+ return { apiKey: det.apiKey, model: opts.model || det.model };
60
+ }
61
+ console.log("tide: ok — not using it. Pass --api-key or set OPENAI_API_KEY, or rules fall back to heuristics.");
62
+ return { apiKey: null, model: opts.model };
63
+ }
64
+
65
+ function runScenarios(bundle, scenarios) {
66
+ const engine = new Engine(bundle);
67
+ let passed = 0;
68
+ const failures = [];
69
+ for (const s of scenarios) {
70
+ const d = engine.decide(s.action, s.params, s.context, s.session);
71
+ let ok = d.verdict === s.expect;
72
+ if (s.expect_rules?.length) {
73
+ const seen = [...d.fired, ...d.shadow].map((f) => f.id);
74
+ ok = ok && s.expect_rules.every((r) => seen.includes(r));
75
+ }
76
+ if (ok) passed++;
77
+ else failures.push(`${s.name}: expected ${s.expect}, got ${d.verdict} on ${JSON.stringify(s.params)}`);
78
+ }
79
+ return { passed, total: scenarios.length, failures };
80
+ }
81
+
82
+ async function cmdCompile(opts) {
83
+ const { apiKey, model } = await resolveCredential(opts);
84
+ if (!apiKey) console.log("tide: no OpenAI credential — using local heuristics for now.");
85
+ const { bundle, scenarios, diagnostics, source, findings } =
86
+ await compilePolicy({ policyPath: opts.policy, agent: opts.agent, projectDir: opts.project, model, apiKey });
87
+ writeBundle(opts.project, bundle, scenarios, findings);
88
+ console.log(`tide: compiled ${bundle.rules.length} rule(s) from ${opts.policy} via ${source} -> ${opts.project}/`);
89
+ for (const r of bundle.rules) console.log(` - ${r.id} [${r.mode}/${r.verdict}] ${r.when || "requires " + JSON.stringify(r.requires)}`);
90
+ const errs = diagnostics.filter((d) => d.level === "error");
91
+ if (errs.length) { console.log(`\ntide: ${errs.length} validation error(s):`); for (const d of errs) console.log(` ! ${d.where}: ${d.message}`); return 1; }
92
+ const { passed, total, failures } = runScenarios(bundle, scenarios);
93
+ console.log(`\ntide: golden tests ${passed}/${total} passed`);
94
+ for (const f of failures) console.log(` ! ${f}`);
95
+ console.log(`tide: review ${opts.project}/findings.md, then \`tide install\`.`);
96
+ return failures.length ? 1 : 0;
97
+ }
98
+
99
+ function cmdTest(opts) {
100
+ const scenarios = loadScenarios(opts.project);
101
+ if (!scenarios.length) { console.log(`tide: no golden scenarios in ${opts.project}/tests/. Run \`tide compile\` first.`); return 1; }
102
+ const { passed, total, failures } = runScenarios(loadBundle(opts.project), scenarios);
103
+ console.log(`tide: ${passed}/${total} golden tests passed`);
104
+ for (const f of failures) console.log(` ! ${f}`);
105
+ return failures.length ? 1 : 0;
106
+ }
107
+
108
+ function cmdCheck(opts) {
109
+ if (!opts.command) { console.error("tide: --command is required"); return 1; }
110
+ const bundle = loadBundle(opts.project);
111
+ const action = "shell" in bundle.actions ? "shell" : opts.agent;
112
+ const d = new Engine(bundle).decide(action, { command: opts.command }, {}, []);
113
+ console.log(`${d.verdict.toUpperCase()} ${opts.command}`);
114
+ if (d.reasons.length) console.log(` reason: ${d.reasons.join("; ")}`);
115
+ if (d.fired.length) console.log(` rules: ${d.fired.map((f) => f.id).join(", ")}`);
116
+ return d.verdict === "allow" ? 0 : 2;
117
+ }
118
+
119
+ function cmdInstall(opts) {
120
+ if (!fs.existsSync(path.join(opts.project, "policies", "rules.policy.yaml"))) {
121
+ console.log(`tide: no compiled policy at ${opts.project}/. Run \`tide compile\` first.`); return 1;
122
+ }
123
+ const p = adapter(opts.agent).install(opts.project);
124
+ console.log(`tide: enforcement installed for ${opts.agent} -> ${p}`);
125
+ console.log(`tide: policy project = ${path.resolve(opts.project)}`);
126
+ console.log("\nTo run your agent with the guard active:");
127
+ console.log(" - Codex app: open Settings -> Hooks and click Trust, then start a new chat.");
128
+ console.log(" - Codex CLI: codex --dangerously-bypass-hook-trust -c features.hooks=true");
129
+ return 0;
130
+ }
131
+
132
+ function cmdUninstall(opts) { adapter(opts.agent).uninstall(); console.log(`tide: enforcement removed for ${opts.agent}`); return 0; }
133
+
134
+ function cmdStatus(opts) {
135
+ const installed = adapter(opts.agent).isInstalled();
136
+ console.log(`tide: enforcement for ${opts.agent}: ${installed ? "INSTALLED" : "not installed"}`);
137
+ try { const b = loadBundle(opts.project); console.log(`tide: ${b.rules.length} rule(s) in ${path.resolve(opts.project)}`); }
138
+ catch { console.log("tide: no compiled policy found"); }
139
+ const events = path.join(opts.project, ".runtime", "events.jsonl");
140
+ const tally = { allow: 0, deny: 0, escalate: 0, error: 0 };
141
+ const recent = [];
142
+ if (fs.existsSync(events)) {
143
+ for (const line of fs.readFileSync(events, "utf8").split("\n")) {
144
+ if (!line.trim()) continue;
145
+ try { const e = JSON.parse(line); tally[e.decision] = (tally[e.decision] || 0) + 1; recent.push(e); } catch { /* skip */ }
146
+ }
147
+ }
148
+ console.log(`tide: firings — allowed ${tally.allow} · blocked ${tally.deny} · escalated ${tally.escalate} · errors ${tally.error}`);
149
+ for (const e of recent.slice(-5)) {
150
+ const mark = { allow: "✓", deny: "⛔", escalate: "✋", error: "!" }[e.decision] || "?";
151
+ console.log(` ${mark} ${e.decision.padEnd(9)} ${(e.command || "").split("\n")[0].slice(0, 70)}`);
152
+ }
153
+ return 0;
154
+ }
155
+
156
+ const HELP = `tide — compile a plain-English POLICY.md into deterministic guardrails on a coding agent's tool calls.
157
+
158
+ Usage: tide <command> [--policy POLICY.md] [--project .tide] [--agent codex] [--model M] [--api-key K]
159
+ compile POLICY.md + tools -> rules (uses your OpenAI credential; auto-detected)
160
+ test run the golden scenarios through the deterministic engine
161
+ check --command "..." evaluate a single command -> ALLOW/DENY
162
+ install install the enforcement hook into the agent (codex)
163
+ uninstall remove the hook
164
+ status show enforcement state + pass/block tally`;
165
+
166
+ export async function main(argv) {
167
+ const { cmd, opts } = parseArgs(argv);
168
+ const cmds = { compile: cmdCompile, test: cmdTest, check: cmdCheck, install: cmdInstall, uninstall: cmdUninstall, status: cmdStatus };
169
+ if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help") { console.log(HELP); return cmd ? 0 : 1; }
170
+ if (!cmds[cmd]) { console.error(`tide: unknown command '${cmd}'\n\n${HELP}`); return 1; }
171
+ try { return await cmds[cmd](opts); }
172
+ catch (e) { console.error(`tide: ${e.message}`); return 1; }
173
+ }
@@ -0,0 +1,65 @@
1
+ // Codex adapter — install/remove the Tide PreToolUse hook (from x-ray's tide.js).
2
+ // Writes a PreToolUse hook into $CODEX_HOME/hooks.json that runs this package's hook.js
3
+ // under the current Node, pointed at the compiled policy via TIDE_PROJECT.
4
+ //
5
+ // Codex 0.142 requires persisted hook trust:
6
+ // app: Trust the hook in Settings -> Hooks (one click);
7
+ // CLI/exec/tests: pass --dangerously-bypass-hook-trust.
8
+
9
+ import fs from "node:fs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const HOOK_PATH = fileURLToPath(new URL("./hook.js", import.meta.url));
15
+ const MARKER = "rippletide-tide";
16
+
17
+ export function codexHome() {
18
+ return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
19
+ }
20
+ export function hooksPath() {
21
+ return path.join(codexHome(), "hooks.json");
22
+ }
23
+
24
+ function shq(s) { return `'${String(s).replace(/'/g, "'\\''")}'`; }
25
+
26
+ export function hookCommand(projectDir) {
27
+ // marker lets us recognise our own hook; TIDE_PROJECT points at the compiled policy
28
+ return `TIDE_PROJECT=${shq(path.resolve(projectDir))} ${shq(process.execPath)} ${shq(HOOK_PATH)} --${MARKER}`;
29
+ }
30
+
31
+ export function install(projectDir) {
32
+ const home = codexHome();
33
+ fs.mkdirSync(home, { recursive: true });
34
+ const p = hooksPath();
35
+ if (fs.existsSync(p)) {
36
+ const existing = fs.readFileSync(p, "utf8");
37
+ if (!existing.includes(MARKER) && !fs.existsSync(p + ".tide-bak")) {
38
+ fs.writeFileSync(p + ".tide-bak", existing);
39
+ }
40
+ }
41
+ const entry = {
42
+ hooks: {
43
+ PreToolUse: [
44
+ { hooks: [{ type: "command", command: hookCommand(projectDir),
45
+ statusMessage: "Tide: checking this command against your policy" }] },
46
+ ],
47
+ },
48
+ };
49
+ fs.writeFileSync(p, JSON.stringify(entry, null, 2));
50
+ return p;
51
+ }
52
+
53
+ export function uninstall() {
54
+ const p = hooksPath();
55
+ if (!fs.existsSync(p)) return;
56
+ if (!fs.readFileSync(p, "utf8").includes(MARKER)) return; // not ours
57
+ const bak = p + ".tide-bak";
58
+ if (fs.existsSync(bak)) { fs.copyFileSync(bak, p); fs.rmSync(bak); }
59
+ else fs.rmSync(p);
60
+ }
61
+
62
+ export function isInstalled() {
63
+ const p = hooksPath();
64
+ return fs.existsSync(p) && fs.readFileSync(p, "utf8").includes(MARKER);
65
+ }