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.
- package/PUBLISHING.md +144 -0
- package/README.md +44 -0
- package/bin/rippletide.js +27 -0
- package/counter/README.md +37 -0
- package/counter/frontend/window.jxa +601 -0
- package/counter/src/cli.js +22 -0
- package/counter/src/counter.js +165 -0
- package/counter/src/proxy.js +243 -0
- package/package.json +64 -0
- package/reviewer/.env.example +5 -0
- package/reviewer/README.md +137 -0
- package/reviewer/bin/rippletide-review.js +16 -0
- package/reviewer/bin/rippletide.js +8 -0
- package/reviewer/codex/rippletide-reviewer/README.md +28 -0
- package/reviewer/codex/rippletide-reviewer/scripts/rippletide-codex +16 -0
- package/reviewer/codex/rippletide-reviewer/scripts/rippletide-codex.mjs +24 -0
- package/reviewer/codex/rippletide-reviewer/scripts/rippletide-reviewer +37 -0
- package/reviewer/src/connect/evidence.js +235 -0
- package/reviewer/src/integrations/cli/rippletide.js +1595 -0
- package/reviewer/src/integrations/codex/prepare.js +277 -0
- package/tide/README.md +128 -0
- package/tide/bin/tide.js +4 -0
- package/tide/examples/no-file-writes/POLICY.md +3 -0
- package/tide/src/cli.js +173 -0
- package/tide/src/codex.js +65 -0
- package/tide/src/compile.js +193 -0
- package/tide/src/credentials.js +41 -0
- package/tide/src/engine.js +115 -0
- package/tide/src/expr.js +260 -0
- package/tide/src/hook.js +71 -0
- package/tide/src/ontology.js +28 -0
- package/tide/src/schema.js +87 -0
package/tide/src/hook.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Codex PreToolUse hook — where Tide sits between the agent and the user.
|
|
2
|
+
// Rule-driven: loads the compiled bundle and evaluates each shell command through the
|
|
3
|
+
// deterministic engine. Codex passes JSON on stdin:
|
|
4
|
+
// {"cwd":"...","tool_name":"Bash","tool_input":{"command":"..."}}
|
|
5
|
+
// Output contract (Codex 0.142): print {"decision":"block","reason":...} to block, else {}.
|
|
6
|
+
// Every firing is appended to <project>/.runtime/events.jsonl so it's easy to see.
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { Engine } from "./engine.js";
|
|
11
|
+
import { loadBundle } from "./schema.js";
|
|
12
|
+
|
|
13
|
+
function projectDir() {
|
|
14
|
+
return process.env.TIDE_PROJECT || path.join(process.cwd(), ".tide");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function logEvent(dir, event) {
|
|
18
|
+
try {
|
|
19
|
+
const runtime = path.join(dir, ".runtime");
|
|
20
|
+
fs.mkdirSync(runtime, { recursive: true });
|
|
21
|
+
fs.appendFileSync(path.join(runtime, "events.jsonl"), JSON.stringify(event) + "\n");
|
|
22
|
+
} catch { /* never block on logging */ }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function readStdin() {
|
|
26
|
+
const chunks = [];
|
|
27
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
28
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function decideCommand(dir, toolName, command) {
|
|
32
|
+
const bundle = loadBundle(dir);
|
|
33
|
+
const engine = new Engine(bundle);
|
|
34
|
+
const action = "shell" in bundle.actions ? "shell" : (toolName || "shell");
|
|
35
|
+
return engine.decide(action, { command }, {}, []);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function main() {
|
|
39
|
+
let payload;
|
|
40
|
+
try { payload = JSON.parse(await readStdin()); } catch { process.stdout.write("{}"); return; }
|
|
41
|
+
|
|
42
|
+
const toolName = String(payload.tool_name ?? "");
|
|
43
|
+
const command = String(payload.tool_input?.command ?? "");
|
|
44
|
+
const dir = projectDir();
|
|
45
|
+
|
|
46
|
+
let verdict, reason, ruleIds;
|
|
47
|
+
try {
|
|
48
|
+
const d = decideCommand(dir, toolName, command);
|
|
49
|
+
verdict = d.verdict;
|
|
50
|
+
reason = d.reasons.join("; ") || "blocked by Tide policy";
|
|
51
|
+
ruleIds = d.fired.map((f) => f.id);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
logEvent(dir, { ts: Date.now(), decision: "error", tool: toolName, command: command.slice(0, 200), error: String(e.message || e) });
|
|
54
|
+
process.stdout.write("{}"); // fail open, but recorded
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
logEvent(dir, { ts: Date.now(), decision: verdict, tool: toolName, command: command.slice(0, 200),
|
|
59
|
+
rules: ruleIds, reason: verdict === "allow" ? "" : reason });
|
|
60
|
+
|
|
61
|
+
if (verdict === "deny") {
|
|
62
|
+
process.stdout.write(JSON.stringify({ decision: "block", reason: `Tide blocked this: ${reason}` }));
|
|
63
|
+
} else if (verdict === "escalate") {
|
|
64
|
+
process.stdout.write(JSON.stringify({ decision: "block",
|
|
65
|
+
reason: `Tide: this needs human approval (${reason}). Ask the user before proceeding.` }));
|
|
66
|
+
} else {
|
|
67
|
+
process.stdout.write("{}");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Tool manifests per agent — the action vocabulary rules are written against.
|
|
2
|
+
//
|
|
3
|
+
// Codex's PreToolUse hook fires for the shell/command tool; its payload is
|
|
4
|
+
// {tool_name:"Bash", tool_input:{command}, cwd}. So the single governed action is the
|
|
5
|
+
// shell command — the primary risky surface (file writes, deletes, network, secret reads
|
|
6
|
+
// all happen as shell commands). Codex ALSO edits files through this action via
|
|
7
|
+
// apply_patch, whose body carries markers like "*** Add File:". We tell the LLM both.
|
|
8
|
+
|
|
9
|
+
export const CODEX_ACTIONS = {
|
|
10
|
+
shell: {
|
|
11
|
+
description:
|
|
12
|
+
"Runs a shell command in the user's workspace (Codex's command/Bash tool). This is " +
|
|
13
|
+
"the action Codex's PreToolUse hook gates before execution. Everything the agent does " +
|
|
14
|
+
"on the machine — reading/writing/deleting files, network calls, running programs — " +
|
|
15
|
+
"happens through this command string. IMPORTANT: Codex also EDITS FILES through this " +
|
|
16
|
+
"same action via apply_patch; those calls arrive as a patch body containing markers " +
|
|
17
|
+
"like '*** Begin Patch', '*** Add File:', '*** Update File:', '*** Delete File:', " +
|
|
18
|
+
"'*** Move to:'. Any policy about creating/editing/deleting files MUST match BOTH " +
|
|
19
|
+
"ordinary shell write commands (rm, mv, cp, touch, mkdir, tee, sed -i, output " +
|
|
20
|
+
"redirection with > or >>, chmod, editors) AND these apply_patch markers.",
|
|
21
|
+
params: { command: "string — the full shell command line or apply_patch body about to run" },
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function actionsFor(agent) {
|
|
26
|
+
if (agent === "codex") return structuredClone(CODEX_ACTIONS);
|
|
27
|
+
throw new Error(`no tool manifest for agent '${agent}' yet (Codex only in v1)`);
|
|
28
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Read/write a Tide policy project — the tide-test schema, on disk (YAML via js-yaml).
|
|
2
|
+
//
|
|
3
|
+
// <project>/
|
|
4
|
+
// tide.yaml name, version
|
|
5
|
+
// ontology/actions.yaml {actions: {name: {description, params}}}
|
|
6
|
+
// policies/rules.policy.yaml [ {id, action, when|requires, verdict, mode, reason, to} ]
|
|
7
|
+
// tests/golden.scenarios.yaml [ {name, session, action, params, context, expect, expect_rules} ]
|
|
8
|
+
// findings.md human-review summary
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import yaml from "js-yaml";
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_DIR = ".tide";
|
|
15
|
+
|
|
16
|
+
function readYaml(file, fallback) {
|
|
17
|
+
try { return yaml.load(fs.readFileSync(file, "utf8")) ?? fallback; } catch { return fallback; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function dump(file, data) {
|
|
21
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
|
+
fs.writeFileSync(file, yaml.dump(data, { lineWidth: 100, noRefs: true }));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function loadBundle(projectDir) {
|
|
26
|
+
const meta = readYaml(path.join(projectDir, "tide.yaml"), {}) || {};
|
|
27
|
+
const ont = readYaml(path.join(projectDir, "ontology", "actions.yaml"), {}) || {};
|
|
28
|
+
const actions = (ont && typeof ont === "object" && ont.actions) ? ont.actions : {};
|
|
29
|
+
const rules = readYaml(path.join(projectDir, "policies", "rules.policy.yaml"), []) || [];
|
|
30
|
+
return {
|
|
31
|
+
name: meta.name || path.basename(path.resolve(projectDir)),
|
|
32
|
+
version: String(meta.version || "1"),
|
|
33
|
+
actions,
|
|
34
|
+
rules: rules.map(normalizeRule),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeRule(r) {
|
|
39
|
+
return {
|
|
40
|
+
id: String(r.id ?? ""),
|
|
41
|
+
action: String(r.action ?? ""),
|
|
42
|
+
verdict: r.verdict ?? "deny",
|
|
43
|
+
mode: r.mode ?? "enforce",
|
|
44
|
+
when: r.when ?? null,
|
|
45
|
+
requires: r.requires ?? null,
|
|
46
|
+
reason: r.reason ?? null,
|
|
47
|
+
to: r.to ?? null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function loadScenarios(projectDir) {
|
|
52
|
+
const raw = readYaml(path.join(projectDir, "tests", "golden.scenarios.yaml"), []) || [];
|
|
53
|
+
return raw.map((s) => ({
|
|
54
|
+
name: String(s.name ?? "scenario"),
|
|
55
|
+
action: String(s.action ?? ""),
|
|
56
|
+
params: s.params ?? {},
|
|
57
|
+
context: s.context ?? {},
|
|
58
|
+
expect: String(s.expect ?? "allow"),
|
|
59
|
+
expect_rules: s.expect_rules ?? [],
|
|
60
|
+
session: s.session ?? [],
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ruleToYaml(r) {
|
|
65
|
+
const out = { id: r.id, action: r.action };
|
|
66
|
+
if (r.when) out.when = r.when;
|
|
67
|
+
if (r.requires && r.requires.length) out.requires = r.requires;
|
|
68
|
+
out.verdict = r.verdict;
|
|
69
|
+
out.mode = r.mode;
|
|
70
|
+
if (r.reason) out.reason = r.reason;
|
|
71
|
+
if (r.to) out.to = r.to;
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function writeBundle(projectDir, bundle, scenarios, findings = "") {
|
|
76
|
+
dump(path.join(projectDir, "tide.yaml"), { name: bundle.name, version: bundle.version });
|
|
77
|
+
dump(path.join(projectDir, "ontology", "actions.yaml"), { actions: bundle.actions });
|
|
78
|
+
dump(path.join(projectDir, "policies", "rules.policy.yaml"), bundle.rules.map(ruleToYaml));
|
|
79
|
+
dump(path.join(projectDir, "tests", "golden.scenarios.yaml"), scenarios.map((s) => ({
|
|
80
|
+
name: s.name, session: s.session, action: s.action, params: s.params,
|
|
81
|
+
context: s.context, expect: s.expect, expect_rules: s.expect_rules,
|
|
82
|
+
})));
|
|
83
|
+
if (findings) {
|
|
84
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
85
|
+
fs.writeFileSync(path.join(projectDir, "findings.md"), findings);
|
|
86
|
+
}
|
|
87
|
+
}
|