draftcat 0.13.0 → 0.15.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/README.md +11 -3
- package/npm/install.js +108 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -109,8 +109,11 @@ However your agent runs, draftcat sits between it and your customer systems as a
|
|
|
109
109
|
- **Token budgets** — per-step / pipeline / day; any breach halts the run immediately.
|
|
110
110
|
- **Cost budgets** — `per_day_cost` / `per_pipeline_cost` cap spend in money. On OpenRouter the caps are enforced on the charge the provider reports for each call (cached and reasoning tokens included); elsewhere on your configured per-1k rates. The approval prompt shows what the run has spent, and `/status` shows the day against every cap.
|
|
111
111
|
- **Human-in-the-loop** — every outbound action requires an explicit operator decision, made live or declared in advance.
|
|
112
|
-
- **Any operator channel** — the [`hitl/v0` protocol](docs/hitl-protocol.md) keeps draftcat as the gate and lets an untrusted relay own presentation. Teams runs through a Power Automate flow in your own tenant: no bot, no Azure app registration, no admin consent. Check yours with `draftcat hitl verify <relay-url>`.
|
|
112
|
+
- **Any operator channel** — the [`hitl/v0` protocol](docs/hitl-protocol.md) keeps draftcat as the gate and lets an untrusted relay own presentation. Teams runs through a Power Automate flow in your own tenant: no bot, no Azure app registration, no admin consent. Slack runs through [`contrib/slack-relay`](contrib/slack-relay/README.md), a small relay that verifies every click. Check yours with `draftcat hitl verify <relay-url>`.
|
|
113
113
|
- **Consume-once tool permits** - `POST /gate/tool-call` puts an agent's MCP or SDK call through the same gate as a pipeline step. Bearer authentication covers ask, poll, and consume. A stable `action_id` makes retries idempotent; the binding covers the exact arguments, policy, and expiry; only the first successful `POST /gate/tool-call/<id>/consume` carries `permit: execute`. See [`docs/tool-gate.md`](docs/tool-gate.md).
|
|
114
|
+
- **Agent harness hooks** - `draftcat gate install claude-code|codex|copilot-cli|gemini-cli` puts every shell, file-writing and MCP call of that harness through the gate as a pre-tool hook that fails closed; see [`docs/harness-hooks.md`](docs/harness-hooks.md).
|
|
115
|
+
- **Approved HTTP requests** - the `http_request` action sends one request after an approval step, bound to exactly the request the operator saw, with a credential draftcat holds and the agent never sees; see [`docs/http-action.md`](docs/http-action.md).
|
|
116
|
+
- **Trip and shadow** - `tool_gate.trip` freezes an agent the gate keeps refusing, and `tool_gate.shadow` decides every live call a second time under a candidate policy and records where the two differ, without touching the live decision.
|
|
114
117
|
- **Repeat guard** — inside `repeat_window` an identical tool call (same agent, tool, arguments) gets the gate's remembered answer instead of a new prompt: a denied call stays denied, an in-flight call joins the open prompt, and `max_repeats` stops a looping agent from paging you.
|
|
115
118
|
- **Denial notices** — a refusal the gate makes on its own (unlisted tool, argument outside a rule, repeat guard) is reported to the operator channel, one notice per agent, tool and reason per window, so nothing is refused silently.
|
|
116
119
|
- **Open gates** — `/pending` on the channel and `draftcat pending` on the host list every approval waiting on a human, pipeline steps and tool calls alike, with how long each has waited and how long it has left.
|
|
@@ -140,7 +143,7 @@ npm install -g draftcat
|
|
|
140
143
|
draftcat --help
|
|
141
144
|
```
|
|
142
145
|
|
|
143
|
-
The installer downloads the matching Linux, macOS, or Windows binary and verifies it against the checksums attached to the GitHub release. No Go toolchain is required.
|
|
146
|
+
The installer downloads the matching Linux, macOS, or Windows binary and verifies it against the checksums attached to the GitHub release. No Go toolchain is required. Each download has a two-minute elapsed limit across redirects and a 30-second inactivity limit. Interrupted downloads fail installation; the existing executable stays in place until its verified replacement is ready.
|
|
144
147
|
|
|
145
148
|
With a Go toolchain, install the CLI from the module:
|
|
146
149
|
|
|
@@ -258,6 +261,7 @@ pipelines:
|
|
|
258
261
|
| `pdf_extract` | Parse a PDF into text + per-fragment bounding boxes (pure-Go) |
|
|
259
262
|
| `pdf_verify_cite` | Resolve `<cite>` tags in AI output against the parsed PDF |
|
|
260
263
|
| `notify` | Send AI output to the operator channel |
|
|
264
|
+
| `http_request` | Send one approved HTTP request with a credential draftcat holds |
|
|
261
265
|
| `voice_*` / `dograh_*` | Voice plugin actions (`-tags voice`) |
|
|
262
266
|
|
|
263
267
|
Add an action by appending a `case` to the deterministic switch in `main.go` and registering its name in `internal/validate/`. See `internal/ghl/` and `internal/dograh/` for connector patterns.
|
|
@@ -369,12 +373,16 @@ draftcat receipts list # approval receipts and verification status (--js
|
|
|
369
373
|
draftcat receipts show <id> # one versioned receipt
|
|
370
374
|
draftcat receipts export # JSONL to stdout (--out path writes mode 0600)
|
|
371
375
|
draftcat receipts anchor # hash-chain anchor of every receipt, to keep outside the store
|
|
372
|
-
draftcat receipts verify-anchor <anchor> # name the first receipt changed since the anchor
|
|
376
|
+
draftcat receipts verify-anchor <anchor> # name the first receipt changed since the anchor (--log anchors.jsonl)
|
|
373
377
|
draftcat audit-verify # verify signed approval receipts
|
|
374
378
|
draftcat report [--since 30d] # oversight summary: who approved, skipped or let time out, per pipeline and step
|
|
375
379
|
draftcat gate check <tool> # what tool_gate decides for one call, without the engine (--args JSON)
|
|
376
380
|
draftcat gate test cases.yaml # a policy test suite for CI: exit 1 when a decision differs
|
|
377
381
|
draftcat gate verify-grant <grant> --tool <tool> # a tool checks its execution grant before acting
|
|
382
|
+
draftcat gate diff-definition pinned.json current.json # what changed in a tool definition (exit 1 if any)
|
|
383
|
+
draftcat gate shadow [--since 7d] # live calls a candidate policy would decide differently
|
|
384
|
+
draftcat gate unfreeze --agent <name> # lift one agent's freeze set by tool_gate.trip
|
|
385
|
+
draftcat gate install <harness> # put a harness's tool calls through the gate (claude-code, codex, copilot-cli, gemini-cli)
|
|
378
386
|
draftcat hitl verify <url> # run the hitl/v0 conformance suite against a relay
|
|
379
387
|
```
|
|
380
388
|
|
package/npm/install.js
CHANGED
|
@@ -40,43 +40,94 @@ function parseChecksums(contents) {
|
|
|
40
40
|
return checksums;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function download(url
|
|
44
|
-
if (redirects > 5) {
|
|
45
|
-
return Promise.reject(new Error("Too many redirects while downloading Draftcat"));
|
|
46
|
-
}
|
|
43
|
+
function download(url) {
|
|
47
44
|
return new Promise((resolve, reject) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
let activeRequest;
|
|
46
|
+
let settled = false;
|
|
47
|
+
const deadline = setTimeout(() => finish(new Error("Draftcat download exceeded 120 seconds")), 120_000);
|
|
48
|
+
|
|
49
|
+
function finish(error, data) {
|
|
50
|
+
if (settled) return;
|
|
51
|
+
settled = true;
|
|
52
|
+
clearTimeout(deadline);
|
|
53
|
+
if (activeRequest) activeRequest.setTimeout(0);
|
|
54
|
+
if (error) {
|
|
55
|
+
if (activeRequest) activeRequest.destroy();
|
|
56
|
+
reject(error);
|
|
57
|
+
} else {
|
|
58
|
+
resolve(data);
|
|
60
59
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function requestAsset(target, redirects) {
|
|
63
|
+
if (settled) return;
|
|
64
|
+
if (redirects > 5) {
|
|
65
|
+
finish(new Error("Too many redirects while downloading Draftcat"));
|
|
64
66
|
return;
|
|
65
67
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
68
|
+
try {
|
|
69
|
+
let followed = false;
|
|
70
|
+
const request = https.get(target, {
|
|
71
|
+
headers: { "User-Agent": `draftcat-npm/${version}` },
|
|
72
|
+
}, (response) => {
|
|
73
|
+
if (settled) { response.destroy(); return; }
|
|
74
|
+
response.on("error", (error) => { if (!followed) finish(error); });
|
|
75
|
+
response.on("aborted", () => { if (!followed) finish(new Error("Draftcat download was interrupted")); });
|
|
76
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
77
|
+
let next;
|
|
78
|
+
try {
|
|
79
|
+
next = new URL(response.headers.location, target);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
finish(error);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (next.protocol !== "https:") {
|
|
85
|
+
finish(new Error("Refusing a non-HTTPS release redirect"));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// The redirect body is irrelevant; retire its handlers before
|
|
89
|
+
// closing it so an old hop cannot fail the new request.
|
|
90
|
+
followed = true;
|
|
91
|
+
request.setTimeout(0);
|
|
92
|
+
response.destroy();
|
|
93
|
+
requestAsset(next, redirects + 1);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (response.statusCode !== 200) {
|
|
97
|
+
response.resume();
|
|
98
|
+
finish(new Error(`Download failed with HTTP ${response.statusCode}`));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const chunks = [];
|
|
102
|
+
let size = 0;
|
|
103
|
+
response.on("data", (chunk) => {
|
|
104
|
+
size += chunk.length;
|
|
105
|
+
if (size > maximumDownloadBytes) {
|
|
106
|
+
finish(new Error("Draftcat release asset is unexpectedly large"));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
chunks.push(chunk);
|
|
110
|
+
});
|
|
111
|
+
response.on("end", () => {
|
|
112
|
+
if (!response.complete) {
|
|
113
|
+
finish(new Error("Draftcat download ended before completion"));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
finish(null, Buffer.concat(chunks));
|
|
117
|
+
});
|
|
118
|
+
response.on("close", () => {
|
|
119
|
+
if (!response.complete) finish(new Error("Draftcat download closed before completion"));
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
activeRequest = request;
|
|
123
|
+
request.on("error", (error) => { if (!followed) finish(error); });
|
|
124
|
+
request.setTimeout(30_000, () => { if (!followed) finish(new Error("Draftcat download timed out")); });
|
|
125
|
+
} catch (error) {
|
|
126
|
+
finish(error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
requestAsset(url, 0);
|
|
80
131
|
});
|
|
81
132
|
}
|
|
82
133
|
|
|
@@ -100,10 +151,31 @@ async function install() {
|
|
|
100
151
|
const executable = process.platform === "win32" ? "draftcat.exe" : "draftcat";
|
|
101
152
|
const binaryDirectory = path.join(__dirname, "bin");
|
|
102
153
|
const destination = path.join(binaryDirectory, executable);
|
|
154
|
+
const contents = zlib.gunzipSync(archive);
|
|
155
|
+
const temporary = path.join(binaryDirectory, `.${executable}.${process.pid}.${crypto.randomBytes(8).toString("hex")}.tmp`);
|
|
103
156
|
fs.mkdirSync(binaryDirectory, { recursive: true });
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
157
|
+
let descriptor;
|
|
158
|
+
let created = false;
|
|
159
|
+
try {
|
|
160
|
+
descriptor = fs.openSync(temporary, "wx", 0o755);
|
|
161
|
+
created = true;
|
|
162
|
+
fs.writeFileSync(descriptor, contents);
|
|
163
|
+
fs.closeSync(descriptor);
|
|
164
|
+
descriptor = undefined;
|
|
165
|
+
if (process.platform !== "win32") fs.chmodSync(temporary, 0o755);
|
|
166
|
+
fs.renameSync(temporary, destination);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (descriptor !== undefined) {
|
|
169
|
+
try { fs.closeSync(descriptor); } catch { /* Preserve the original failure. */ }
|
|
170
|
+
}
|
|
171
|
+
if (created) {
|
|
172
|
+
try {
|
|
173
|
+
fs.rmSync(temporary, { force: true });
|
|
174
|
+
} catch (cleanupError) {
|
|
175
|
+
error.message += ` (temporary file cleanup failed: ${cleanupError.message})`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
107
179
|
}
|
|
108
180
|
console.log(`Installed Draftcat v${version} for ${process.platform}-${process.arch}`);
|
|
109
181
|
}
|