@decidio/sdk 0.11.0 → 0.12.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/CHANGELOG.md +63 -1
- package/README.md +18 -9
- package/dist/adapters/_gate.d.ts +18 -1
- package/dist/adapters/_gate.js +34 -27
- package/dist/adapters/langgraph.js +1 -1
- package/dist/adapters/openai.d.ts +11 -4
- package/dist/adapters/openai.js +12 -4
- package/dist/adapters/temporal.d.ts +29 -2
- package/dist/adapters/temporal.js +42 -6
- package/dist/cli.js +131 -32
- package/dist/demo-gate.js +14 -0
- package/dist/errors.js +3 -1
- package/dist/guard.d.ts +5 -0
- package/dist/guard.js +3 -2
- package/dist/index.d.ts +39 -7
- package/dist/index.js +103 -20
- package/dist/inngest.d.ts +5 -0
- package/dist/inngest.js +2 -2
- package/dist/resume.d.ts +52 -2
- package/dist/resume.js +74 -5
- package/dist/signing.d.ts +8 -0
- package/dist/signing.js +6 -0
- package/openapi.yaml +158 -2
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -78,6 +78,7 @@ const DEFAULT_API = "https://decidio-api.onrender.com";
|
|
|
78
78
|
let API = (process.env.DECIDIO_API_URL ?? DEFAULT_API).replace(/\/$/, "");
|
|
79
79
|
const COMPUTER = "*";
|
|
80
80
|
const EVERY_PROJECT = "every project";
|
|
81
|
+
const GUARD_POLICY_RE = /^[A-Za-z0-9_-]{20,64}$/;
|
|
81
82
|
let CONNECTED_APPROVER = null;
|
|
82
83
|
let CONNECTED_UNANSWERED = false;
|
|
83
84
|
let CONNECTED_APP_URL = null;
|
|
@@ -3450,10 +3451,12 @@ async function cmdHookPrompt() {
|
|
|
3450
3451
|
const body = await r.json().catch(() => null);
|
|
3451
3452
|
if (r.status === 403 && body?.code === "ORGANISATION_KEEPS_ON") {
|
|
3452
3453
|
notePause(session, false);
|
|
3454
|
+
rememberPausable(false);
|
|
3453
3455
|
return block(ORGANISATION_KEEPS_ON);
|
|
3454
3456
|
}
|
|
3455
3457
|
if (r.ok && body?.state === "paused") {
|
|
3456
3458
|
notePause(session, true);
|
|
3459
|
+
rememberPausable(true);
|
|
3457
3460
|
return block(`Decidio guard paused for this session: destructive commands run without a person's approval - each one is recorded in your workspace's audit log - until you type "decidio start" (or for ${GUARD_PAUSE_HOURS} hours). Decidio still asks before a command edits Claude Code's settings to switch its check off.`);
|
|
3458
3461
|
}
|
|
3459
3462
|
if (r.ok && body?.state === "on") {
|
|
@@ -3468,6 +3471,17 @@ async function cmdHookPrompt() {
|
|
|
3468
3471
|
return block(`Decidio couldn't ${action} the guard just now (${String(e?.message ?? e).replace(/^\[decidio\] /, "").slice(0, 160)}) - ${action === "pause" ? "it stays on" : "if it was paused, type \"decidio start\" again"}.`);
|
|
3469
3472
|
}
|
|
3470
3473
|
}
|
|
3474
|
+
function rememberPausable(pausable) {
|
|
3475
|
+
if (CREDS_FROM !== "connection")
|
|
3476
|
+
return;
|
|
3477
|
+
const c = activeConnection(projectRoot(), API);
|
|
3478
|
+
if (!c || (c.guardPausable !== false) === pausable)
|
|
3479
|
+
return;
|
|
3480
|
+
try {
|
|
3481
|
+
writeSecretFile(connectionFile(c.root), JSON.stringify({ ...c, guardPausable: pausable }) + "\n");
|
|
3482
|
+
}
|
|
3483
|
+
catch { }
|
|
3484
|
+
}
|
|
3471
3485
|
function hookForgesPrompt(command, depth = 0) {
|
|
3472
3486
|
const hit = hookSegments(command).some((s) => {
|
|
3473
3487
|
const a = s.args.map((x) => x.replace(/^["']|["']$/g, ""));
|
|
@@ -3555,8 +3569,11 @@ async function cmdHookPretool(opts = { unconnected: "hold" }) {
|
|
|
3555
3569
|
const short = hookShort(hookRedact(g.text.replace(/\s+/g, " ").trim()));
|
|
3556
3570
|
let cfg = hookConfig();
|
|
3557
3571
|
if ("missing" in cfg) {
|
|
3558
|
-
const got = await hookProvision();
|
|
3572
|
+
const got = await hookProvision(opts.policy);
|
|
3559
3573
|
if (!got.ok) {
|
|
3574
|
+
const held = got.hold === true || (opts.policy !== undefined && got.hold !== false);
|
|
3575
|
+
if (opts.unconnected === "tell" && held && !g.offSwitch)
|
|
3576
|
+
return deny(`Decidio: "${short}" is a ${g.category} ${w.noun} that needs a person's approval - ${got.reason} ${w.ran} ${got.holdWhy ?? (got.hold === true ? "This team's Decidio holds a computer that isn't connected until it connects." : "This repository's Decidio line holds a computer that isn't connected unless Decidio confirms this team lets it through, and it couldn't confirm that here.")}`, got.user);
|
|
3560
3577
|
if (opts.unconnected === "tell" && !g.offSwitch) {
|
|
3561
3578
|
const told = toldOnce(typeof input.session_id === "string" ? input.session_id : "", g.binding);
|
|
3562
3579
|
if (told === "passed")
|
|
@@ -3819,6 +3836,53 @@ function hookRunner(portable = false) {
|
|
|
3819
3836
|
const hookPromptLine = (runner) => `cd "$CLAUDE_PROJECT_DIR" && IN=$(cat) && case "$IN" in *[Dd][Ee][Cc][Ii][Dd][Ii][Oo]*) printf '%s' "$IN" | ${runner} hook prompt;; esac`;
|
|
3820
3837
|
const HOOK_FILE_TOOLS = ["Write", "Edit", "MultiEdit"];
|
|
3821
3838
|
const HOOK_GUARD_PATHS = ["**/.claude/settings*.json", "~/.claude/settings*.json", "~/.claude/plugins/**"];
|
|
3839
|
+
function decidioHookSet(runner, preLine, postLine) {
|
|
3840
|
+
const set = [
|
|
3841
|
+
{ event: "PreToolUse", matcher: "Bash|PowerShell", command: preLine, extra: { timeout: 150, onFailure: "block" } },
|
|
3842
|
+
{ event: "PostToolUse", matcher: "Bash|PowerShell", command: postLine, extra: { timeout: 30 } },
|
|
3843
|
+
{ event: "SessionStart", matcher: "startup|resume", command: `cd "$CLAUDE_PROJECT_DIR" && ${runner} hook session`, extra: { timeout: 15 } },
|
|
3844
|
+
{ event: "UserPromptSubmit", matcher: "", command: hookPromptLine(runner), extra: { timeout: 15 } },
|
|
3845
|
+
];
|
|
3846
|
+
for (const t of HOOK_FILE_TOOLS)
|
|
3847
|
+
for (const p of HOOK_GUARD_PATHS) {
|
|
3848
|
+
set.push({ event: "PreToolUse", matcher: t, iff: `${t}(${p})`, command: preLine, extra: { timeout: 150, onFailure: "block" } });
|
|
3849
|
+
set.push({ event: "PostToolUse", matcher: t, iff: `${t}(${p})`, command: postLine, extra: { timeout: 30 } });
|
|
3850
|
+
}
|
|
3851
|
+
return set;
|
|
3852
|
+
}
|
|
3853
|
+
function decidioHooksJson(runner) {
|
|
3854
|
+
const hooks = {};
|
|
3855
|
+
for (const h of decidioHookSet(runner, `cd "$CLAUDE_PROJECT_DIR" && ${runner} hook pretool`, `cd "$CLAUDE_PROJECT_DIR" && ${runner} hook posttool`)) {
|
|
3856
|
+
const handler = { type: "command", ...(h.iff ? { if: h.iff } : {}), command: h.command, ...h.extra };
|
|
3857
|
+
(hooks[h.event] ??= []).push(h.matcher ? { matcher: h.matcher, hooks: [handler] } : { hooks: [handler] });
|
|
3858
|
+
}
|
|
3859
|
+
return hooks;
|
|
3860
|
+
}
|
|
3861
|
+
function cmdHookManagedSettings() {
|
|
3862
|
+
process.stdout.write(JSON.stringify({ hooks: decidioHooksJson(`npx -y @decidio/sdk@${VERSION}`) }, null, 2) + "\n");
|
|
3863
|
+
for (const line of [
|
|
3864
|
+
"Decidio hooks for Claude Code's managed settings (stdout above). Merge the \"hooks\" block into your organisation's managed settings:",
|
|
3865
|
+
" macOS /Library/Application Support/ClaudeCode/managed-settings.json (or the MDM profile domain com.anthropic.claudecode)",
|
|
3866
|
+
" Linux /etc/claude-code/managed-settings.json (WSL too)",
|
|
3867
|
+
" Windows C:\\Program Files\\ClaudeCode\\managed-settings.json (or HKLM\\SOFTWARE\\Policies\\ClaudeCode, value \"Settings\")",
|
|
3868
|
+
" or a drop-in file in managed-settings.d/ beside it.",
|
|
3869
|
+
"What it does: managed hooks run beside people's own hooks and can't be removed from their settings; a user's or project's",
|
|
3870
|
+
" disableAllHooks doesn't reach them. Never set disableAllHooks in managed settings - it would turn these off too.",
|
|
3871
|
+
"Watch out:",
|
|
3872
|
+
" - Server-managed settings (the Claude admin console) and MDM/registry policies outrank this file - by default the first source",
|
|
3873
|
+
" found wins and the rest are skipped. Put these hooks in the highest-ranked source you deploy (server-managed hooks show each",
|
|
3874
|
+
" person an approval dialog first), or set \"managedSourcesBehavior\": \"merge\" there (Claude Code 2.1.242+).",
|
|
3875
|
+
" - Claude Code cloud sessions (claude.ai/code) read neither this file nor an MDM profile - only server-managed settings reach them.",
|
|
3876
|
+
" - Every computer needs Node 18+ and the npm registry: the pretool hook blocks the call if it can't run (fail closed).",
|
|
3877
|
+
" - Each computer still connects once - the first destructive command shows a link the workspace owner allows; until then it waits.",
|
|
3878
|
+
" - A repository whose committed settings already carry Decidio's hook runs both (one request; test it once).",
|
|
3879
|
+
" - A local administrator can edit this file.",
|
|
3880
|
+
" - allowManagedHooksOnly: true would also stop people's own hooks - your choice; Decidio doesn't need it.",
|
|
3881
|
+
` - To update, redeploy with the new version (pinned here: @decidio/sdk@${VERSION}).`,
|
|
3882
|
+
])
|
|
3883
|
+
err(line);
|
|
3884
|
+
return 0;
|
|
3885
|
+
}
|
|
3822
3886
|
async function cmdHookInstall(opts) {
|
|
3823
3887
|
const settingsPath = joinPath(opts.dir ?? ".", ".claude", opts.project ? "settings.json" : "settings.local.json");
|
|
3824
3888
|
let settings = {};
|
|
@@ -3865,17 +3929,16 @@ async function cmdHookInstall(opts) {
|
|
|
3865
3929
|
hooks[event].push(matcher ? { matcher, hooks: [want] } : { hooks: [want] });
|
|
3866
3930
|
return state;
|
|
3867
3931
|
};
|
|
3868
|
-
const preLine = `cd "$CLAUDE_PROJECT_DIR" && ${runner} hook pretool${opts.unconnected === "tell" ?
|
|
3932
|
+
const preLine = `cd "$CLAUDE_PROJECT_DIR" && ${runner} hook pretool${opts.unconnected === "tell" ? ` --unconnected=tell${opts.policy && GUARD_POLICY_RE.test(opts.policy) ? ` --policy=${opts.policy}` : ""}` : ""}`, postLine = `cd "$CLAUDE_PROJECT_DIR" && ${runner} hook posttool`;
|
|
3933
|
+
const set = decidioHookSet(runner, preLine, postLine);
|
|
3869
3934
|
const pre = place("PreToolUse", "Bash|PowerShell", undefined, preLine, { timeout: 150, onFailure: "block" });
|
|
3870
3935
|
const post = place("PostToolUse", "Bash|PowerShell", undefined, postLine, { timeout: 30 });
|
|
3871
|
-
place("SessionStart", "startup|resume", undefined, `cd "$CLAUDE_PROJECT_DIR" && ${runner} hook session`, { timeout: 15 });
|
|
3872
|
-
place("UserPromptSubmit", "", undefined, hookPromptLine(runner), { timeout: 15 });
|
|
3873
3936
|
const guard = { added: 0, updated: 0, kept: 0 };
|
|
3874
|
-
for (const
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
guard[
|
|
3878
|
-
|
|
3937
|
+
for (const h of set.slice(2)) {
|
|
3938
|
+
const st = place(h.event, h.matcher, h.iff, h.command, h.extra);
|
|
3939
|
+
if (h.iff)
|
|
3940
|
+
guard[st]++;
|
|
3941
|
+
}
|
|
3879
3942
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
3880
3943
|
const target = realOrSame(settingsPath), tmpSettings = `${target}.decidio-${process.pid}.tmp`;
|
|
3881
3944
|
writeFileSync(tmpSettings, JSON.stringify(settings, null, 2) + "\n", { mode: existsSync(target) ? statSync(target).mode & 0o777 : 0o644 });
|
|
@@ -4028,25 +4091,45 @@ function openInBrowser(url) {
|
|
|
4028
4091
|
const CONNECT_WAIT_S = () => { const n = Number(process.env.DECIDIO_CONNECT_WAIT_S ?? 90); return Number.isFinite(n) && n >= 1 && n <= 110 ? n : 90; };
|
|
4029
4092
|
let lastHookInstall = null;
|
|
4030
4093
|
let connectError = "";
|
|
4031
|
-
|
|
4032
|
-
|
|
4094
|
+
function hostOf(url) {
|
|
4095
|
+
try {
|
|
4096
|
+
return new URL(url).host || "Decidio's API host";
|
|
4097
|
+
}
|
|
4098
|
+
catch {
|
|
4099
|
+
return "Decidio's API host";
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
function cloudNetworkWords() {
|
|
4103
|
+
const host = hostOf(API);
|
|
4104
|
+
return `this is a Claude Code cloud session: its environment's network access must allow ${host}. Edit the environment and add ${host} under Network access → Allowed domains, keeping the package-manager defaults (on Team and Enterprise an Owner can set it once on a shared environment): https://code.claude.com/docs/en/cloud-environments#network-access`;
|
|
4105
|
+
}
|
|
4106
|
+
let connectFailure = "other";
|
|
4107
|
+
async function startConnect(root, host, quiet = false, policy) {
|
|
4108
|
+
const fail = (m, kind = "other") => { connectError = m; connectFailure = kind; if (!quiet)
|
|
4033
4109
|
err(`✘ ${m}`); return null; };
|
|
4034
4110
|
const prior = connectionRecord(root, API);
|
|
4035
4111
|
const kp = prior && !inCloudSession() ? { did: prior.did, privateKeyBase64: prior.key } : generateKeypair();
|
|
4036
4112
|
const cloud = inCloudSession();
|
|
4037
4113
|
const project = (root === COMPUTER ? EVERY_PROJECT : projectLabel(root)).trim() || "project", machine = machineLabel(cloud).trim() || "machine";
|
|
4038
4114
|
const proof = signBoundProof(kp.privateKeyBase64, { agentId: "connect", did: kp.did, action: "connect.start", amount: 0, scope: [host, project, machine, cloud ? "cloud" : "local"].join("\n") });
|
|
4039
|
-
const
|
|
4115
|
+
const startBody = { host, project, machine, did: kp.did, cloud, proof };
|
|
4116
|
+
let r = await api("/connect/start", { method: "POST", body: JSON.stringify(policy ? { ...startBody, policy } : startBody) }, "", true);
|
|
4117
|
+
let asked = !!policy;
|
|
4118
|
+
if (policy && r.status === 400 && r.body?.code === "VALIDATION") {
|
|
4119
|
+
asked = false;
|
|
4120
|
+
r = await api("/connect/start", { method: "POST", body: JSON.stringify(startBody) }, "", true);
|
|
4121
|
+
}
|
|
4040
4122
|
if (r.status === 0)
|
|
4041
|
-
return fail(`couldn't reach Decidio at ${API} - check your connection and run npx @decidio/sdk connect again
|
|
4123
|
+
return fail(`couldn't reach Decidio at ${API} - ${inCloudSession() ? cloudNetworkWords() : "check your connection and run npx @decidio/sdk connect again."}`, "unreachable");
|
|
4042
4124
|
if (r.status === 404 || r.status === 401)
|
|
4043
|
-
return fail(`${r.body?.message ?? "Connecting a coding assistant isn't switched on for this Decidio yet - it is on in the Decidio lab."} (${API})
|
|
4125
|
+
return fail(`${r.body?.message ?? "Connecting a coding assistant isn't switched on for this Decidio yet - it is on in the Decidio lab."} (${API})`, "off");
|
|
4044
4126
|
if (r.status === 429)
|
|
4045
|
-
return fail(`${r.body?.message ?? "Too many tries - wait a minute and try again."}
|
|
4127
|
+
return fail(`${r.body?.message ?? "Too many tries - wait a minute and try again."}`, "unreachable");
|
|
4046
4128
|
if (r.status !== 201 || typeof r.body?.pollSecret !== "string" || typeof r.body?.link !== "string")
|
|
4047
|
-
return fail(`couldn't start a connection (${r.status}${r.body?.message ? `: ${r.body.message}` : ""}). Nothing was saved
|
|
4129
|
+
return fail(`couldn't start a connection (${r.status}${r.body?.message ? `: ${r.body.message}` : ""}). Nothing was saved.`, r.status >= 500 ? "unreachable" : "other");
|
|
4048
4130
|
const p = { v: 1, api: API, root, project, host, machine, cloud, did: kp.did, key: kp.privateKeyBase64, code: String(r.body.code), link: String(r.body.link),
|
|
4049
|
-
pollSecret: r.body.pollSecret, expiresAt: String(r.body.expiresAt), interval: Number(r.body.interval) || 2, startedAt: new Date().toISOString()
|
|
4131
|
+
pollSecret: r.body.pollSecret, expiresAt: String(r.body.expiresAt), interval: Number(r.body.interval) || 2, startedAt: new Date().toISOString(),
|
|
4132
|
+
...(asked && policy && typeof r.body.hold === "boolean" ? { policy, hold: r.body.hold } : {}) };
|
|
4050
4133
|
mkdirSync(connectDir(), { recursive: true });
|
|
4051
4134
|
writeSecretFile(pendingConnectFile(root), JSON.stringify(p) + "\n");
|
|
4052
4135
|
if (typeof r.body.qr === "string")
|
|
@@ -4058,8 +4141,10 @@ function readPendingConnect(root) {
|
|
|
4058
4141
|
const p = readJsonQuiet(pendingConnectFile(root));
|
|
4059
4142
|
return p && p.v === 1 && p.api === API && p.root === root && typeof p.pollSecret === "string" ? p : null;
|
|
4060
4143
|
}
|
|
4061
|
-
async function hookProvision() {
|
|
4144
|
+
async function hookProvision(policy) {
|
|
4062
4145
|
const scope = COMPUTER;
|
|
4146
|
+
const UNCONFIRMED = "Decidio couldn't confirm whether this team lets a computer that isn't connected through, so it holds it until it connects.";
|
|
4147
|
+
const holdOf = (p) => !policy ? {} : p && p.policy === policy && typeof p.hold === "boolean" ? { hold: p.hold } : { hold: true, holdWhy: UNCONFIRMED };
|
|
4063
4148
|
if (process.env.DECIDIO_AGENT_ID) {
|
|
4064
4149
|
const named = printableName(process.env.DECIDIO_AGENT_ID) ?? "(unprintable)";
|
|
4065
4150
|
return { ok: false, reason: `this project names the Decidio agent "${named}" (in its .env or the shell) but has no token for it. Ask the user to check it with npx @decidio/sdk doctor, or to remove DECIDIO_AGENT_ID to use this computer's connection.`, user: `Decidio: "${named}" has no token here` };
|
|
@@ -4077,7 +4162,7 @@ async function hookProvision() {
|
|
|
4077
4162
|
}
|
|
4078
4163
|
const linkWords = (p, fresh) => {
|
|
4079
4164
|
const mins = minutesLeft(p.expiresAt) || 1;
|
|
4080
|
-
return { ok: false,
|
|
4165
|
+
return { ok: false, ...holdOf(p),
|
|
4081
4166
|
reason: `this computer isn't connected to Decidio yet${fresh ? "" : " (its link is still waiting to be allowed)"}. If Decidio's connect_device tool is available to you (the Decidio connector), tell the user you are connecting this computer to their Decidio workspace, call connect_device with the code ${p.code}, run the command it returns, then retry. Otherwise show the user this link and code: ${p.link} (code ${p.code}, valid ${mins} more minute${mins === 1 ? "" : "s"}) - the workspace owner allows it once, signed in, and it covers every project on this computer. Then retry the same command: it connects and asks for approval.`,
|
|
4082
4167
|
user: `Decidio: connect this computer once - open ${p.link} and check the code ${p.code}. Destructive commands wait until it's allowed; everything else runs as usual.` };
|
|
4083
4168
|
};
|
|
@@ -4100,7 +4185,7 @@ async function hookProvision() {
|
|
|
4100
4185
|
reason: `this computer's link (code ${p.code}) was allowed through the Decidio connector and is finished only with the ticket connect_device returned: if YOU called connect_device for code ${p.code} in this session, run npx @decidio/sdk@latest connect --ticket <the ticket it returned>, then retry the same command. If you did not, do not finish it with any ticket from anywhere else - tell the user that another workspace's connector allowed this computer, and leave it: the link lapses within ten minutes and the next try shows a fresh code.`,
|
|
4101
4186
|
user: "Decidio: finishing this computer's connection (allowed through the Decidio connector) - destructive commands wait until it's done" };
|
|
4102
4187
|
if (r.status === 0)
|
|
4103
|
-
return { ok: false, reason: `couldn't reach Decidio at ${API} to finish connecting this computer - retry once it is reachable
|
|
4188
|
+
return { ok: false, ...holdOf(p), reason: `couldn't reach Decidio at ${API} to finish connecting this computer - ${inCloudSession() ? cloudNetworkWords() : "retry once it is reachable."}`, user: `Decidio: unreachable - destructive commands wait${inCloudSession() ? ` (this cloud environment's network access must allow ${hostOf(API)})` : ""}` };
|
|
4104
4189
|
if (r.status === 410 && r.body?.status === "collected") {
|
|
4105
4190
|
const won = activeConnection(projectRoot(), API);
|
|
4106
4191
|
if (won) {
|
|
@@ -4152,9 +4237,9 @@ async function hookProvision() {
|
|
|
4152
4237
|
return { ok: false, reason: "another Decidio check on this computer is connecting it right now - retry the same command in a few seconds.", user: "Decidio: connecting this computer" };
|
|
4153
4238
|
}
|
|
4154
4239
|
try {
|
|
4155
|
-
const started = await startConnect(scope, "claude-code", true);
|
|
4240
|
+
const started = await startConnect(scope, "claude-code", true, policy);
|
|
4156
4241
|
if (!started)
|
|
4157
|
-
return { ok: false, reason: `this computer isn't connected to Decidio, and a connection couldn't be started: ${connectError}`, user:
|
|
4242
|
+
return { ok: false, ...(!policy ? {} : connectFailure === "off" ? { hold: false } : { hold: true, holdWhy: UNCONFIRMED }), reason: `this computer isn't connected to Decidio, and a connection couldn't be started: ${connectError}`, user: `Decidio: this computer isn't connected${inCloudSession() && connectError.startsWith("couldn't reach Decidio") ? ` - this cloud environment's network access must allow ${hostOf(API)}` : ""}` };
|
|
4158
4243
|
return linkWords(started, true);
|
|
4159
4244
|
}
|
|
4160
4245
|
finally {
|
|
@@ -4364,7 +4449,8 @@ function storeCollected(scope, p, body) {
|
|
|
4364
4449
|
const c = { v: 1, api: API, root: scope, project: p.project, host: p.host, machine: p.machine, cloud: p.cloud, agentId: String(body.agentId), did: p.did, key: p.key,
|
|
4365
4450
|
token: body.token, jti: String(body.jti), expiresAt: String(body.expiresAt), workspaceId: String(body.workspaceId), workspaceName: body.workspace?.name ?? null,
|
|
4366
4451
|
approver: body.approver?.name ?? null, approvedBy: body.approvedBy?.name ?? null, appUrl: typeof body.appUrl === "string" ? body.appUrl : null, connectedAt: new Date().toISOString(),
|
|
4367
|
-
guardPausable: body.guardPausable !== false, ...(body.answered === false ? { answered: false } : {})
|
|
4452
|
+
guardPausable: body.guardPausable !== false, ...(body.answered === false ? { answered: false } : {}),
|
|
4453
|
+
...(typeof body.guardPolicyKey === "string" && GUARD_POLICY_RE.test(body.guardPolicyKey) ? { guardPolicyKey: body.guardPolicyKey } : {}) };
|
|
4368
4454
|
mkdirSync(connectDir(), { recursive: true });
|
|
4369
4455
|
try {
|
|
4370
4456
|
writeSecretFile(connectionFile(scope), JSON.stringify(c) + "\n");
|
|
@@ -4458,7 +4544,7 @@ async function finishConnect(root, c, opts, fresh) {
|
|
|
4458
4544
|
const home = process.platform === "win32" ? (SHELL_ENV.USERPROFILE ?? homedir()) : (SHELL_ENV.HOME ?? homedir());
|
|
4459
4545
|
const plugin = !cloud && decidioPluginEnabled(root);
|
|
4460
4546
|
if (!plugin) {
|
|
4461
|
-
const rc = await cmdHookInstall({ project: shared || everywhere, dir: everywhere ? home : root, quiet: true, portable: everywhere, ...(shared && !opts.holdEveryone ? { unconnected: "tell" } : {}) });
|
|
4547
|
+
const rc = await cmdHookInstall({ project: shared || everywhere, dir: everywhere ? home : root, quiet: true, portable: everywhere, ...(shared && !opts.holdEveryone ? { unconnected: "tell", ...(c?.guardPolicyKey ? { policy: c.guardPolicyKey } : {}) } : {}) });
|
|
4462
4548
|
if (rc !== 0)
|
|
4463
4549
|
return rc;
|
|
4464
4550
|
}
|
|
@@ -4497,10 +4583,15 @@ async function finishConnect(root, c, opts, fresh) {
|
|
|
4497
4583
|
log(`• A cloud session reads the repository's settings, not a computer's, so the hooks are in ${settings} - not committed. Ask the person before committing it: committed, every cloud session of this repository asks too, and so does every teammate's clone (a teammate connects with npx @decidio/sdk connect).`);
|
|
4498
4584
|
else if (shared)
|
|
4499
4585
|
log(`• Commit ${settings}: every clone then asks before destructive commands (a teammate connects with npx @decidio/sdk connect).`);
|
|
4586
|
+
const keyed = !!c?.guardPolicyKey && GUARD_POLICY_RE.test(c.guardPolicyKey);
|
|
4500
4587
|
if (shared && !plugin)
|
|
4501
4588
|
log(dim(opts.holdEveryone
|
|
4502
4589
|
? " A computer that isn't connected is refused every destructive command until it connects (--hold-everyone)."
|
|
4503
|
-
:
|
|
4590
|
+
: ` A computer that isn't connected is told once (with how to connect), then the same command runs without Decidio's approval${keyed ? " - unless the workspace owner holds computers that aren't connected (the Agents page)" : ""}. To keep refusing it until it connects: npx @decidio/sdk connect --team --hold-everyone`));
|
|
4591
|
+
if (shared && !plugin && !opts.holdEveryone && !keyed)
|
|
4592
|
+
log(`⚠ This file's Decidio line carries no team key, so the workspace owner's "Hold computers that aren't connected" won't reach this repository. ${c
|
|
4593
|
+
? "This computer's connection was made before that switch existed: run npx @decidio/sdk connect --team --force (the workspace owner allows it once more), then commit the file."
|
|
4594
|
+
: "These credentials come from the shell or .env: run npx @decidio/sdk connect --team from a computer connected with npx @decidio/sdk connect, then commit the file."}`);
|
|
4504
4595
|
if (fresh && c?.answered !== false)
|
|
4505
4596
|
log(dim(` Try it: ask ${hostName} to delete a scratch folder with rm -rf - the approval reaches ${approverName ?? "the approver"} on their phone.`));
|
|
4506
4597
|
return 0;
|
|
@@ -5849,6 +5940,9 @@ CODING AGENTS (Claude Code)
|
|
|
5849
5940
|
refused: store the user's one-line reason it is different, then retry the same
|
|
5850
5941
|
command - the request reaches the approver with that reason beside the refusal
|
|
5851
5942
|
npx @decidio/sdk hook rules the command patterns the hook gates (DECIDIO_HOOK_EXTRA_RULES adds yours)
|
|
5943
|
+
npx @decidio/sdk hook managed-settings the Decidio hooks for Claude Code's managed settings (an Enterprise admin deploys
|
|
5944
|
+
them to every developer's computer) - the JSON on stdout, where it goes and what it
|
|
5945
|
+
does and doesn't guarantee on stderr; writes nothing
|
|
5852
5946
|
npx @decidio/sdk hook pretool|posttool the hooks themselves: run by Claude Code with the tool call as JSON on
|
|
5853
5947
|
stdin, never by hand. A gated command is held up to DECIDIO_HOOK_HOLD_MS
|
|
5854
5948
|
(90 s; at most 120 s) for the approver, then refused with its decision id; the SAME command
|
|
@@ -5857,8 +5951,9 @@ CODING AGENTS (Claude Code)
|
|
|
5857
5951
|
--unconnected=tell (written into a shared file by connect): a computer with no
|
|
5858
5952
|
credential is refused once per session and command, then let through with a line.
|
|
5859
5953
|
npx @decidio/sdk hook prompt the UserPromptSubmit hook: the person's own typed "decidio pause" / "decidio start"
|
|
5860
|
-
pauses or resumes the guard for that session (
|
|
5861
|
-
pause and every command run under it is
|
|
5954
|
+
pauses or resumes the guard for that session (not where the workspace keeps the
|
|
5955
|
+
guard on - Enterprise by default; every pause and every command run under it is
|
|
5956
|
+
recorded); other prompts pass untouched.
|
|
5862
5957
|
|
|
5863
5958
|
CONTEXT PACKS (Claude Code, Claude.ai, ChatGPT)
|
|
5864
5959
|
npx @decidio/sdk context create <slug> --title "ACME application" --trigger "whenever we discuss the ACME application"
|
|
@@ -6094,7 +6189,7 @@ async function main(argv) {
|
|
|
6094
6189
|
verify: { flags: ["--allow-unpinned"], valued: ["--issuer"] },
|
|
6095
6190
|
tokens: { flags: ["--mine"] },
|
|
6096
6191
|
dev: { flags: [], valued: ["--target", "--port"] },
|
|
6097
|
-
hook: { flags: ["--project"], valued: ["--timeout", "--approver", "--cli", "--unconnected"] },
|
|
6192
|
+
hook: { flags: ["--project"], valued: ["--timeout", "--approver", "--cli", "--unconnected", "--policy"] },
|
|
6098
6193
|
github: { flags: ["--agents-md"], valued: ["--environment", "--justification", "--decision", "--workflow", "--api"] },
|
|
6099
6194
|
deploys: { flags: ["--watch", "--json"], valued: ["--environment", "--repo", "--limit"] },
|
|
6100
6195
|
controls: { flags: ["--json"] },
|
|
@@ -6299,10 +6394,10 @@ async function main(argv) {
|
|
|
6299
6394
|
ticket: valuedOpt(argsIn, "--ticket"), holdEveryone: argsIn.includes("--hold-everyone") });
|
|
6300
6395
|
case "disconnect": return cmdDisconnect();
|
|
6301
6396
|
case "hook": {
|
|
6302
|
-
const VALUED = ["--timeout", "--approver", "--cli", "--unconnected"];
|
|
6397
|
+
const VALUED = ["--timeout", "--approver", "--cli", "--unconnected", "--policy"];
|
|
6303
6398
|
const pos = argsIn.filter((x, i) => !x.startsWith("-") && !VALUED.includes(argsIn[i - 1] ?? ""));
|
|
6304
6399
|
const sub = pos[0];
|
|
6305
|
-
const allowed = { pretool: ["--unconnected"], posttool: [], session: [], prompt: [], wait: ["--timeout"], install: ["--project", "--approver", "--cli"], rules: [], justify: [] };
|
|
6400
|
+
const allowed = { pretool: ["--unconnected", "--policy"], posttool: [], session: [], prompt: [], wait: ["--timeout"], install: ["--project", "--approver", "--cli"], rules: [], justify: [], "managed-settings": [] };
|
|
6306
6401
|
const given = argsIn.filter((x) => x.startsWith("--")).map((x) => x.split("=")[0]);
|
|
6307
6402
|
if (sub && allowed[sub]) {
|
|
6308
6403
|
const stray = given.find((f) => !allowed[sub].includes(f));
|
|
@@ -6312,7 +6407,10 @@ async function main(argv) {
|
|
|
6312
6407
|
}
|
|
6313
6408
|
}
|
|
6314
6409
|
switch (sub) {
|
|
6315
|
-
case "pretool":
|
|
6410
|
+
case "pretool": {
|
|
6411
|
+
const pol = valuedOpt(argsIn, "--policy");
|
|
6412
|
+
return cmdHookPretool({ unconnected: valuedOpt(argsIn, "--unconnected") === "tell" ? "tell" : "hold", ...(pol && GUARD_POLICY_RE.test(pol) ? { policy: pol } : {}) });
|
|
6413
|
+
}
|
|
6316
6414
|
case "posttool": return cmdHookPosttool();
|
|
6317
6415
|
case "session": return cmdHookSession();
|
|
6318
6416
|
case "prompt": return cmdHookPrompt();
|
|
@@ -6327,8 +6425,9 @@ async function main(argv) {
|
|
|
6327
6425
|
case "install": return cmdHookInstall({ project: argsIn.includes("--project"), approver: valuedOpt(argsIn, "--approver"), cli: valuedOpt(argsIn, "--cli") });
|
|
6328
6426
|
case "rules": return cmdHookRules();
|
|
6329
6427
|
case "justify": return cmdHookJustify(pos[1], pos.slice(2).join(" "));
|
|
6428
|
+
case "managed-settings": return cmdHookManagedSettings();
|
|
6330
6429
|
default:
|
|
6331
|
-
err(`✘ hook needs a subcommand: install | wait <decision-id> | justify <decision-id> "<what changed>" | rules (pretool, posttool and session are run by Claude Code).\n\n${USAGE}`);
|
|
6430
|
+
err(`✘ hook needs a subcommand: install | wait <decision-id> | justify <decision-id> "<what changed>" | rules | managed-settings (pretool, posttool and session are run by Claude Code).\n\n${USAGE}`);
|
|
6332
6431
|
return 2;
|
|
6333
6432
|
}
|
|
6334
6433
|
}
|
package/dist/demo-gate.js
CHANGED
|
@@ -41,6 +41,7 @@ export async function startDemoGate(opts = {}) {
|
|
|
41
41
|
let relayCode = null;
|
|
42
42
|
let relayOk = null;
|
|
43
43
|
let committed = null;
|
|
44
|
+
let claimedBy = null;
|
|
44
45
|
const relayFetch = async (path, init = {}) => {
|
|
45
46
|
if (!relayBase)
|
|
46
47
|
return null;
|
|
@@ -112,6 +113,7 @@ export async function startDemoGate(opts = {}) {
|
|
|
112
113
|
return send({ ok: true, changed: true });
|
|
113
114
|
if (path === "/agent/authorize") {
|
|
114
115
|
verdict = null;
|
|
116
|
+
claimedBy = null;
|
|
115
117
|
committed = typeof body?.requestCommitment === "string" && /^[0-9a-f]{64}$/.test(body.requestCommitment) ? body.requestCommitment : null;
|
|
116
118
|
return send({ decision: "route", decisionId: DECISION_ID, receiptId: "R-DEMO-1", requestCommitment: committed });
|
|
117
119
|
}
|
|
@@ -128,6 +130,18 @@ export async function startDemoGate(opts = {}) {
|
|
|
128
130
|
return send({ state: "rejected", reason: "quickstart: withdrawn — the tour stopped waiting", requestCommitment: committed });
|
|
129
131
|
return send(verdict === "reject" ? { state: "rejected", reason: "quickstart: rejected by the approver", requestCommitment: committed } : { state: verdict === "approve" ? "approved" : "pending", requestCommitment: committed });
|
|
130
132
|
}
|
|
133
|
+
if (path.startsWith("/agent/claim/")) {
|
|
134
|
+
const key = typeof body?.claimKey === "string" && /^[A-Za-z0-9._:-]{16,128}$/.test(body.claimKey) ? body.claimKey : null;
|
|
135
|
+
if (!key)
|
|
136
|
+
return send({ code: "VALIDATION", message: "claimKey must be 16-128 characters of letters, digits, dot, colon, underscore or dash" }, 400);
|
|
137
|
+
if (verdict !== "approve")
|
|
138
|
+
return send({ code: "NOT_CLAIMABLE", decisionId: DECISION_ID, state: verdict === "reject" ? "rejected" : "pending", message: "only a person's approval is collected here" }, 409);
|
|
139
|
+
if (claimedBy && claimedBy !== key)
|
|
140
|
+
return send({ code: "ALREADY_USED", decisionId: DECISION_ID, state: "approved", message: "another caller already collected this approval - ask again for a new one" }, 409);
|
|
141
|
+
const again = claimedBy === key;
|
|
142
|
+
claimedBy = key;
|
|
143
|
+
return send({ ok: true, claimed: true, decisionId: DECISION_ID, again });
|
|
144
|
+
}
|
|
131
145
|
if (path.startsWith("/agent/withdraw/")) {
|
|
132
146
|
if (verdict)
|
|
133
147
|
return send({ code: "NOT_OPEN", state: verdict === "approve" ? "approved" : "rejected", decisionId: DECISION_ID }, 409);
|
package/dist/errors.js
CHANGED
|
@@ -30,7 +30,9 @@ export class DecidioHttpError extends Error {
|
|
|
30
30
|
? ` — this looks transient. Your action ALREADY RAN; do NOT re-run it — only its report failed to land, and the door's own note says whether a report was kept to re-send.`
|
|
31
31
|
: /\/agent\/status/.test(endpoint)
|
|
32
32
|
? ` — this looks transient; polling again is safe (a status read changes nothing).`
|
|
33
|
-
:
|
|
33
|
+
: /\/agent\/authorize/.test(endpoint)
|
|
34
|
+
? ` — this looks transient. This SDK already retried it with the same request key (a request still waiting is answered with the one already waiting); retrying is safe and can never double-execute.`
|
|
35
|
+
: ` — this looks transient; retrying is safe and can never double-execute.`)
|
|
34
36
|
: ` — the server refused the request itself, so retrying it unchanged will fail the same way. Fix the request (or the decision it names) first.`) +
|
|
35
37
|
sideEffectNote(endpoint, typeof body?.code === "string" ? body.code : undefined));
|
|
36
38
|
this.status = status;
|
package/dist/guard.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface GuardFacadeConfig {
|
|
|
17
17
|
workspaceId?: string;
|
|
18
18
|
/** Base URL of the Decidio app (DECIDIO_APP_URL) — used to deep-link a routed decision. */
|
|
19
19
|
appUrl?: string;
|
|
20
|
+
/** The per-call id behind the default request key (0.12.0) - see GuardConfig.invocationId. Leave it unset. */
|
|
21
|
+
invocationId?: () => string;
|
|
20
22
|
fetchImpl?: typeof fetch;
|
|
21
23
|
}
|
|
22
24
|
export interface ProtectOptions {
|
|
@@ -53,6 +55,9 @@ export interface ProtectOptions {
|
|
|
53
55
|
* success indication, so instead of forcing your API into magic field names:
|
|
54
56
|
* `evidence: (r) => ({ recordId: r.transactionId, success: r.status === "committed" })`. */
|
|
55
57
|
evidence?: (result: any) => unknown;
|
|
58
|
+
/** YOUR OWN REQUEST KEY for a call, from its arguments (0.12.0) - e.g. `(inv) => inv.id` - so the same call asked again,
|
|
59
|
+
* even from a new process, is the same request. Without it each call sends a fresh random key. See withApproval. */
|
|
60
|
+
requestKey?: (...args: any[]) => string;
|
|
56
61
|
}
|
|
57
62
|
export declare class Guard {
|
|
58
63
|
config: GuardFacadeConfig;
|
package/dist/guard.js
CHANGED
|
@@ -60,6 +60,7 @@ export class Guard {
|
|
|
60
60
|
const base = {
|
|
61
61
|
apiUrl: c.apiUrl, agentId: c.agentId, apiToken: c.apiToken,
|
|
62
62
|
sourceSystem: c.sourceSystem, agentKey: c.agentKey, agentDid: c.agentDid, workspaceId: c.workspaceId, appUrl: c.appUrl, fetchImpl: c.fetchImpl,
|
|
63
|
+
...(c.invocationId ? { invocationId: c.invocationId } : {}),
|
|
63
64
|
pollTimeoutMs: opts?.pollTimeoutMs, pollIntervalMs: opts?.pollIntervalMs,
|
|
64
65
|
};
|
|
65
66
|
if ((opts?.mode ?? "durable") === "blocking") {
|
|
@@ -68,12 +69,12 @@ export class Guard {
|
|
|
68
69
|
`(pollTimeoutMs), and when it does, this process stops waiting and the request is withdrawn in Decidio: ` +
|
|
69
70
|
`a later approval does nothing. Only safe for short, watched approvals — omit mode:"blocking" for ` +
|
|
70
71
|
`durable async resume on real (minutes-to-days) approvals.`);
|
|
71
|
-
return withApproval(fn, describe, base, { ...(opts?.evidence ? { evidence: opts.evidence } : {}), ...(opts?.withContext ? { withContext: true } : {}) });
|
|
72
|
+
return withApproval(fn, describe, base, { ...(opts?.evidence ? { evidence: opts.evidence } : {}), ...(opts?.withContext ? { withContext: true } : {}), ...(opts?.requestKey ? { requestKey: opts.requestKey } : {}) });
|
|
72
73
|
}
|
|
73
74
|
const resume = this.ensureResume();
|
|
74
75
|
if (opts?.action)
|
|
75
76
|
resume.register(opts.action, fn, opts?.evidence, opts?.withContext);
|
|
76
|
-
return withApproval(fn, describe, { ...base, resumeUrl: c.resumeUrl, resume }, { ...(opts?.evidence ? { evidence: opts.evidence } : {}), ...(opts?.withContext ? { withContext: true } : {}) });
|
|
77
|
+
return withApproval(fn, describe, { ...base, resumeUrl: c.resumeUrl, resume }, { ...(opts?.evidence ? { evidence: opts.evidence } : {}), ...(opts?.withContext ? { withContext: true } : {}), ...(opts?.requestKey ? { requestKey: opts.requestKey } : {}) });
|
|
77
78
|
}
|
|
78
79
|
protectBestEffort(fn, describe, opts) {
|
|
79
80
|
const wrapped = this.protect(fn, describe, opts);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { ResumeController, ApprovalCheck } from "./resume.js";
|
|
1
|
+
import type { ResumeController, ApprovalCheck, ClaimOutcome } from "./resume.js";
|
|
2
2
|
import { type CommittedRequest } from "./signing.js";
|
|
3
3
|
import { type EvidenceOutcome } from "./evidence.js";
|
|
4
|
-
export { DecidioSuspendedError, DecidioDeniedError, DecidioRecoveryUnavailableError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded, checkApproval } from "./resume.js";
|
|
5
|
-
export type { PendingAction, PendingStore, ResumeController, ResumeOutcome, ApprovalCheck, ApprovalCheckCode, DecisionStatus } from "./resume.js";
|
|
4
|
+
export { DecidioSuspendedError, DecidioDeniedError, DecidioRecoveryUnavailableError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded, checkApproval, claimApproval, newClaimKey } from "./resume.js";
|
|
5
|
+
export type { PendingAction, PendingStore, ResumeController, ResumeOutcome, ApprovalCheck, ApprovalCheckCode, DecisionStatus, ClaimOutcome, ClaimCode } from "./resume.js";
|
|
6
6
|
export { guard, Guard } from "./guard.js";
|
|
7
7
|
export type { GuardFacadeConfig, ProtectOptions } from "./guard.js";
|
|
8
8
|
export { DecidioHttpError, decisionIdProblem } from "./errors.js";
|
|
9
|
-
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential, bindingMaterial, boundScope, requestCommitment, idempotencyKey, committedRequestJson } from "./signing.js";
|
|
9
|
+
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential, bindingMaterial, boundScope, requestCommitment, idempotencyKey, invocationRequestKey, ownRequestKey, committedRequestJson } from "./signing.js";
|
|
10
10
|
export type { BindingMaterial, CommittedRequest } from "./signing.js";
|
|
11
11
|
export { canonicalize, canonicalizeArgs, canonicalJson, DecidioUnsafeArgumentsError } from "./canonical.js";
|
|
12
12
|
export type { JsonValue, JsonPrimitive } from "./canonical.js";
|
|
@@ -61,6 +61,11 @@ export interface GuardConfig {
|
|
|
61
61
|
* Sent on /agent/authorize so the decision carries it. Falls back to
|
|
62
62
|
* resume.resumeUrl. Omit when using the poll worker. */
|
|
63
63
|
resumeUrl?: string;
|
|
64
|
+
/** Where the per-call id behind the DEFAULT REQUEST KEY comes from (0.12.0, 11f). Every call that sends a request
|
|
65
|
+
* commitment also sends a request key derived from a fresh random id and that commitment, so this SDK's own retry of
|
|
66
|
+
* the authorize is the same request. Leave it unset: a fixed id makes every call the SAME request. It exists so a
|
|
67
|
+
* test or a conformance harness can pin the bytes it compares. */
|
|
68
|
+
invocationId?: () => string;
|
|
64
69
|
fetchImpl?: typeof fetch;
|
|
65
70
|
}
|
|
66
71
|
export interface ApprovalContext {
|
|
@@ -115,11 +120,15 @@ export declare function coreRequest(ctx: {
|
|
|
115
120
|
export declare class DecidioTimeoutError extends Error {
|
|
116
121
|
decisionId: string;
|
|
117
122
|
withdrawn: boolean;
|
|
123
|
+
shared: boolean;
|
|
118
124
|
/** `withdrawn` (0.6.4+): true when the SDK told Decidio it gave up and the request was closed as
|
|
119
125
|
* WITHDRAWN (the queue no longer carries it; a later approval does nothing); false when that
|
|
120
126
|
* best-effort call failed, so the request may still be open in Decidio. Nothing was executed
|
|
121
|
-
* locally either way.
|
|
122
|
-
|
|
127
|
+
* locally either way.
|
|
128
|
+
* `shared` (0.12.0): true when the request was left open on purpose because it may be shared - the authorize
|
|
129
|
+
* answered an earlier request (`idempotentReplay`) or joined one another caller is waiting on (`openTwin`), or the
|
|
130
|
+
* withdraw was refused because another caller joined it (409 SHARED_REQUEST). A person still decides it. */
|
|
131
|
+
constructor(decisionId: string, withdrawn?: boolean, shared?: boolean);
|
|
123
132
|
}
|
|
124
133
|
/** A transport failure with GUIDANCE instead of a raw fetch stack. Carries the endpoint, the
|
|
125
134
|
* operation, the underlying code, and the safety state a caller cares about most: whether
|
|
@@ -132,7 +141,10 @@ export declare class DecidioRateLimitError extends Error {
|
|
|
132
141
|
limit: number | undefined;
|
|
133
142
|
remaining: number | undefined;
|
|
134
143
|
resetAt: string | undefined;
|
|
135
|
-
|
|
144
|
+
code?: string | undefined;
|
|
145
|
+
/** `code` (0.12.0): the server's own code - `AGENT_RATE` for the workspace's hourly allowance, which is never retried;
|
|
146
|
+
* a 429 without it is a transport throttle the SDK retries before it gives up. */
|
|
147
|
+
constructor(limit: number | undefined, remaining: number | undefined, resetAt: string | undefined, serverMessage?: string, code?: string | undefined);
|
|
136
148
|
}
|
|
137
149
|
export declare class DecidioNetworkError extends Error {
|
|
138
150
|
endpoint: string;
|
|
@@ -145,6 +157,18 @@ export declare class DecidioNetworkError extends Error {
|
|
|
145
157
|
* every engine door reached the mock authorization path with a request the core would have refused.
|
|
146
158
|
* Exported so `adapters/_gate.ts` calls the same function rather than a second copy that can drift. */
|
|
147
159
|
export declare function assertDescribed(ctx: ApprovalContext): void;
|
|
160
|
+
/** THE ONE AUTHORIZE POST (0.12.0, 11f) - the core wrapper and every engine adapter send through here. A network failure
|
|
161
|
+
* or a transient answer is retried a bounded number of times with the SAME body: under a request key that is the same
|
|
162
|
+
* request (the gate answers the decision it already made, `idempotentReplay`), and a keyless request asked again while
|
|
163
|
+
* it waits joins the one waiting (`openTwin`). A body that carries a commitment WITHOUT a key is never retried - nothing
|
|
164
|
+
* would make the retry the same request. `rateLimit: "http"` keeps an adapter's 429 a DecidioHttpError, as it always was. */
|
|
165
|
+
export declare function sendAuthorize(config: GuardConfig, first: Record<string, unknown>, opts?: {
|
|
166
|
+
rateLimit?: "typed" | "http";
|
|
167
|
+
resign?: () => unknown;
|
|
168
|
+
}): Promise<any>;
|
|
169
|
+
/** The typed refusal for a claim that did not let this run go ahead (0.12.0) - DecidioBlockedError, so code that treats
|
|
170
|
+
* a block as "did not run" keeps doing so; the words say what happened and what to do ("ask again"). */
|
|
171
|
+
export declare function claimRefusal(claim: ClaimOutcome): DecidioBlockedError;
|
|
148
172
|
/**
|
|
149
173
|
* Wrap an async action behind the Decidio gate. `describe` extracts the approval
|
|
150
174
|
* context (action, amount, scope) from the call arguments. Returns a function with
|
|
@@ -172,6 +196,9 @@ export interface ProtectedExecution<R> {
|
|
|
172
196
|
/** What Decidio recorded. `reportRecorded` false means the execution happened and Decidio does
|
|
173
197
|
* NOT know; `executionConfirmed` false means it knows but could not attest a commit. */
|
|
174
198
|
confirmation: EvidenceOutcome;
|
|
199
|
+
/** The request key this call sent with its authorize (0.12.0, 11f) - absent when none was sent (no key material, so no
|
|
200
|
+
* commitment). It names the REQUEST to Decidio; your downstream idempotency key stays `decisionId`. */
|
|
201
|
+
requestKey?: string;
|
|
175
202
|
}
|
|
176
203
|
export declare function withApproval<A extends any[], R>(fn: (...args: A) => Promise<R>, describe: (...args: A) => ApprovalContext, config: GuardConfig, opts?: {
|
|
177
204
|
/** Map YOUR function's natural return value to the evidence Decidio records. The gate awards
|
|
@@ -183,4 +210,9 @@ export declare function withApproval<A extends any[], R>(fn: (...args: A) => Pro
|
|
|
183
210
|
* idempotencyKey }`. Opt-in, never inferred from the function's arity — guessing wrong means
|
|
184
211
|
* passing a context object as someone's first business argument. */
|
|
185
212
|
withContext?: boolean;
|
|
213
|
+
/** YOUR OWN REQUEST KEY for a call (0.12.0, 11f), from its arguments - an order or invoice id - so the same call asked
|
|
214
|
+
* again, even from a new process, is the same request. Keyed with this agent's material before it is sent (your id
|
|
215
|
+
* never reaches Decidio in the clear). The same key for a CHANGED request is refused by the gate (409
|
|
216
|
+
* IDEMPOTENCY_CONFLICT), never a silent second request. Without it each call sends a fresh random key. */
|
|
217
|
+
requestKey?: (...args: A) => string;
|
|
186
218
|
}): (...args: A) => Promise<ProtectedExecution<R>>;
|