@decidio/sdk 0.10.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 +113 -3
- 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.d.ts +2 -0
- package/dist/cli.js +623 -96
- 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/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,117 @@ allowlist. Each entry names what changed, what a reader must do on upgrade, and
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
-
## 0.
|
|
10
|
+
## 0.12.0 — request keys by default; an approval is collected once; the owner's guard controls
|
|
11
|
+
|
|
12
|
+
- **A request key by default** (founder 11f). Every authorize that carries a request commitment now carries an
|
|
13
|
+
`idempotencyKey` too: a keyed digest of a fresh id for the call and the commitment. The SDK retries the authorize twice
|
|
14
|
+
(after 0.3 s and 1 s) with the same request when the network fails, on a 5xx, or on a 429 that is not the workspace's
|
|
15
|
+
hourly limit - so a retry never runs an action twice, and a retry of a request still waiting is answered with the one
|
|
16
|
+
already waiting. A 4xx is never retried. A retry signs a fresh
|
|
17
|
+
identity proof (its nonce is single-use); nothing else changes. No key material: no commitment and no key, as before.
|
|
18
|
+
- `withApproval(..., { requestKey: (...args) => string })` / `protect(..., { requestKey })` uses your own key (an order
|
|
19
|
+
id) instead, keyed before it is sent. The same key for a changed request is refused (409 IDEMPOTENCY_CONFLICT).
|
|
20
|
+
- The result names the key it sent: `ProtectedExecution.requestKey`. `ExecutionContext.idempotencyKey` stays the
|
|
21
|
+
decision id - keep using it downstream.
|
|
22
|
+
- The engines key on their own ids: OpenAI on the tool call id; Temporal on the activity's workflow, run and activity
|
|
23
|
+
ids (`authorizeActivity`, new); Inngest on `runId` (new argument of `decidioGateStep`).
|
|
24
|
+
- **An approval is collected once** (founder call 24 "(a)"). Before the SDK runs an action on a person's approval it
|
|
25
|
+
claims it (`POST /agent/claim/<decisionId>`, `claimApproval`) with a key random to that run, and runs only if Decidio
|
|
26
|
+
says this run collected it. Otherwise it raises `DecidioBlockedError` in plain words, e.g. "This approval was already
|
|
27
|
+
used by another run. Ask again for a new approval." This covers the blocking wait, the durable resume, Temporal and the
|
|
28
|
+
OpenAI verified resume. A durable action keeps its claim key with it (`PendingAction.claimKey`), so a restarted worker
|
|
29
|
+
claims with the same key and is answered as the same run; a claim refused as used or expired is recorded like a block
|
|
30
|
+
(`status: "blocked"` with the reason). Auto-approvals are never claimed, and reading the status never claims.
|
|
31
|
+
- **A shared request is left open.** The blocking timeout no longer withdraws a request the authorize answered as
|
|
32
|
+
`idempotentReplay` or `openTwin`, and a 409 `SHARED_REQUEST` from the withdraw is reported, not thrown:
|
|
33
|
+
`DecidioTimeoutError.shared` is true and `withdrawn` false.
|
|
34
|
+
- The messages that said "a retry creates a NEW request" now say the SDK already retried with the same request.
|
|
35
|
+
|
|
36
|
+
On upgrade: the gate must have `POST /agent/claim` (the server's claim-once release). Against an older gate the claim
|
|
37
|
+
answers 404 and nothing runs on a person's approval - durable entries stay parked until the gate is upgraded. Temporal:
|
|
38
|
+
pass `claim: (decisionId, claimKey) => executeActivity(claimActivity, decisionId, claimKey)` (the activity returns
|
|
39
|
+
`claimApprovalActivity(config, decisionId, claimKey)`) beside `verify` - an injected `authorize` without it is refused
|
|
40
|
+
before anything is routed - and return `authorizeActivity(config, ctx)` from the authorize activity. Entries parked by an
|
|
41
|
+
older SDK (or put into a store directly) still resume, but carry no claim key: they claim with a fresh random key on
|
|
42
|
+
every resolve, so an answer lost on the way back costs a new approval - never a second run. Proved by: smoke.ts
|
|
43
|
+
(256 -> 292), adapters-smoke.ts (74 -> 83), cli-smoke.ts (416 -> 418), the conformance runners (a refusal-advice vector)
|
|
44
|
+
and the three-door gate (the authorize bodies stay byte-identical with a pinned invocation id).
|
|
45
|
+
|
|
46
|
+
### The owner's guard controls (connect-once slice 4)
|
|
47
|
+
|
|
48
|
+
Cycle connect-once's slice 4 (founder 2026-10-30: "the owner would have controls to make the guard mandatory, disable pause").
|
|
49
|
+
Upgrade: nothing to do for a person; a team that wants its repositories held runs `connect --team` again and commits the file.
|
|
50
|
+
|
|
51
|
+
- **A team's committed hook line carries its guard policy key.** `connect --team` writes `--policy=<key>` into the tell line
|
|
52
|
+
(Team and Enterprise). A computer that isn't connected sends the key when it starts its link, and the start answers one
|
|
53
|
+
yes/no: whether the workspace owner holds computers that aren't connected. Held, a destructive command is refused until the
|
|
54
|
+
computer connects, instead of being told once and let through. If Decidio can't answer (unreachable, a server error, rate
|
|
55
|
+
limited), a keyed line holds; Decidio's connect switched off (404) falls back to the line's own `--unconnected`. An older
|
|
56
|
+
server that refuses the key is asked again without it. Editing the key out of the line is switching the check off, so it
|
|
57
|
+
is refused like any other such edit. A keyed line tells once ONLY when Decidio answered for its key that the team doesn't hold
|
|
58
|
+
(or connect is switched off): a project `.env` naming an agent or another server, or a link another check is starting, holds.
|
|
59
|
+
- **`connect --team` says when the line carries no team key** - a connection made before 0.12.0, or credentials from the shell
|
|
60
|
+
or `.env` - so the owner's hold won't reach the repository, and how to fix it (`connect --team --force`).
|
|
61
|
+
- **Claude Code cloud sessions**: when the hook can't reach Decidio, it says the environment's network access must allow
|
|
62
|
+
Decidio's host, under Network access → Allowed domains (the default level doesn't list it), with the docs link.
|
|
63
|
+
- **`hook managed-settings`** prints the Decidio hooks as JSON for Claude Code's managed settings (an organisation-wide
|
|
64
|
+
install), from the same set `hook install` writes - a test holds it equal to the plugin's `hooks.json`. Where the file goes,
|
|
65
|
+
and what it does and doesn't guarantee, go to stderr.
|
|
66
|
+
- **The session line follows the owner's keep-on switch** from the next pause answer (the computer remembers it).
|
|
67
|
+
- Proved by: cli-smoke 418 -> 427 (the keyed line held, told, unanswerable, a `.env` or a lock in the way, connect off, the key edited
|
|
68
|
+
out, the cloud network words, the keyless `--team` line, managed-settings equal to hooks.json, the remembered answer); connect.smoke and the integration suite on the server side.
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## 0.11.0 — connect once, on in every project; no surprises; the connector connects a computer
|
|
72
|
+
|
|
73
|
+
0.10.0's tag was held for this release, so 0.11.0 is the first release meant to carry `connect` (0.10.0 was then published by
|
|
74
|
+
mistake on 2026-10-30, from a tag on its own merge; 0.11.0 supersedes it). It carries
|
|
75
|
+
cycle connect-once's slice 1, 2a and 2b (founder 2026-10-29: one tag, after they merge).
|
|
76
|
+
|
|
77
|
+
- **One connect per computer.** A connection now covers every repository on the computer, and `connect` switches the hook
|
|
78
|
+
on in your own `~/.claude/settings.json`, which every project reads. A repository connected the 0.10.0 way keeps its own
|
|
79
|
+
connection (read first). A cloud session and `--team` still use the repository's `.claude/settings.json`.
|
|
80
|
+
- **The hook connects the computer itself.** A destructive command on a computer with no credential is refused with a link
|
|
81
|
+
and a short code (to Claude, and in a line to the person); once the workspace owner allows it, the next try finishes
|
|
82
|
+
connecting and asks for the approval in the same run. It never writes Claude Code's settings - in auto mode the assistant
|
|
83
|
+
may not, and a person put the hook there. Two hooks at once share one link. It connects only the Decidio your shell
|
|
84
|
+
names (or the default), never one a repository's `.env` names, and only over HTTPS; a project whose `.env` names an agent
|
|
85
|
+
with no token is refused by name rather than given the computer's connection. The run that connects tells the person which
|
|
86
|
+
workspace and who allowed it, even when the command then runs at once.
|
|
87
|
+
- **The computer-wide hook is the pinned `npx -y @decidio/sdk@0.11.0` command**, so it works in every project; a project
|
|
88
|
+
install still writes `$CLAUDE_PROJECT_DIR` unescaped, so Claude Code expands it. The settings file is written whole, and
|
|
89
|
+
through a symlink.
|
|
90
|
+
- **No surprises** (founder 2026-10-29: "the user is fully aware and not surprised"):
|
|
91
|
+
- `hook session`, a SessionStart hook: every session opens with one line - approvals are on here, what waits and for whom,
|
|
92
|
+
or that this computer isn't connected yet. Local only, never blocks;
|
|
93
|
+
- `connect` says what waits for approval and that everything else runs as before;
|
|
94
|
+
- a held command names who approves and where they see it;
|
|
95
|
+
- the plugin skill says what connecting does before doing it, and has the person run `! npx @decidio/sdk connect` when
|
|
96
|
+
auto mode refuses it.
|
|
97
|
+
- **The plugin** (0.5.0) carries the SessionStart hook, and a marketplace file ready to publish - not published (founder:
|
|
98
|
+
nothing public until the listing is ready).
|
|
99
|
+
- **The connector connects a computer** (slice 2a):
|
|
100
|
+
- `connect --ticket <ticket>` finishes the link the Decidio connector allowed (`connect_device` returns the ticket): the
|
|
101
|
+
poll carries this computer's own secret and the ticket, and only the credential is written;
|
|
102
|
+
- the hook names the connector's way first when a computer isn't connected (call `connect_device` with the code, run what
|
|
103
|
+
it returns), then the link and code; a link the connector allowed is kept while it waits for its ticket.
|
|
104
|
+
- **Pause and resume** (slice 2b): `hook prompt`, a UserPromptSubmit hook that `hook install`, `connect` and the plugin add
|
|
105
|
+
behind a shell pre-filter, acts on the person's own typed "decidio pause" / "decidio start" (exact words, `source: user`
|
|
106
|
+
only) and keeps the words from the model. While paused a gated command runs with a line and no decision - asked of Decidio only
|
|
107
|
+
in a session paused on this computer; the settings guard (now run over the whole line, inside `sh -c` too, for the pause and
|
|
108
|
+
the shared file's notice alike) and the self-decide refusal stay on; an agent running `hook prompt` itself is refused. The
|
|
109
|
+
session line says how to pause, or that the organisation keeps the guard on.
|
|
110
|
+
- **A shared file tells, then lets through** (slice 2a): `hook pretool --unconnected=tell`, which `connect` writes into a
|
|
111
|
+
shared file, refuses a computer with no credential once per session and command, then lets the same call pass with a line
|
|
112
|
+
to the person - never an `allow`, so Claude Code's own permission check still decides. No flag, a value it doesn't know,
|
|
113
|
+
or no session id holds, as before. `connect --team --hold-everyone` writes the holding hook instead.
|
|
114
|
+
|
|
115
|
+
On upgrade: nothing to do; 0.10.0 repository connections keep working. Proved by: cli-smoke.ts (378 -> 400; slice 1: one connect for
|
|
116
|
+
two repositories, the 0.10.0 file read first, the hook's own link - wait - collect-then-ask without touching settings, two
|
|
117
|
+
hooks sharing a link, a refused link, the approver in holds, the session line, SessionStart installed; slice 2a: the
|
|
118
|
+
connector's way, the ticket's usage, secret and ticket on the poll, tell once then a line, hold by default, --hold-everyone).
|
|
119
|
+
|
|
120
|
+
## 0.10.0 — connect a coding assistant in one tap (not published; folded into 0.11.0)
|
|
11
121
|
|
|
12
122
|
- **`npx @decidio/sdk connect`** (cycle connect). A coding assistant sets Decidio up for you: it runs `connect`, which prints a
|
|
13
123
|
link and a short code (on a real terminal also a QR, and it opens the browser); the workspace owner opens the link signed in,
|
|
@@ -16,8 +126,8 @@ allowlist. Each entry names what changed, what a reader must do on upgrade, and
|
|
|
16
126
|
ask-only credential - never a session - kept OUTSIDE the repository (the config dir, owner-only, keyed by the repository root
|
|
17
127
|
and the Decidio), and the hooks switch on in `.claude/settings.local.json` (`--team`: the shared `.claude/settings.json`).
|
|
18
128
|
No `.env`, nothing to paste, the word "agent" nowhere in the setup.
|
|
19
|
-
- **In a cloud session the hooks go in `.claude/settings.json`.** A cloud session reads
|
|
20
|
-
|
|
129
|
+
- **In a cloud session the hooks go in `.claude/settings.json`.** A cloud session reads the repository's files, not your
|
|
130
|
+
computer's, and only the committed shared file reaches the next cloud machine, so that is where they go. connect leaves it uncommitted and says to ask
|
|
21
131
|
the person before committing it (committed, every cloud session of the repository and every teammate's clone ask too). The
|
|
22
132
|
credential is that cloud machine's: a fresh one refuses destructive commands until it connects again.
|
|
23
133
|
- **The machine proves its key.** The start is signed with the machine's own key (the request-bound proof every signed
|
package/README.md
CHANGED
|
@@ -528,6 +528,9 @@ wake-up.
|
|
|
528
528
|
- **Temporal**: whenever you pass `authorize`, also pass `verify: (id) => executeActivity(verifyActivity, id)`,
|
|
529
529
|
where the activity returns `verifyApprovalActivity(config, id, ctx)` (from `@decidio/sdk/temporal`)
|
|
530
530
|
for the same `ctx`. A signal only wakes the workflow; still pending → it waits for the next one.
|
|
531
|
+
Since 0.12.0 also pass `claim: (id, key) => executeActivity(claimActivity, id, key)`, where the
|
|
532
|
+
activity returns `claimApprovalActivity(config, id, key)`, and return `authorizeActivity(config, ctx)`
|
|
533
|
+
from the authorize activity: it keys the request on the activity's workflow, run and activity ids.
|
|
531
534
|
- **Inngest**: the check runs as a memoized step after the event; a premature or forged event waits again.
|
|
532
535
|
- **OpenAI Agents**: `applyResume(state, item, verdict)` is removed. Use
|
|
533
536
|
`await applyVerifiedResume(config, state, item, decisionId, describe)`, which approves the
|
|
@@ -541,11 +544,16 @@ wake-up.
|
|
|
541
544
|
whole node on resume, so an earlier gate in the same node runs again: if its request auto-approved and
|
|
542
545
|
its action already ran, the replay answers `proceed` to your verified agent and the action runs twice.
|
|
543
546
|
Put each gated action in its own node, or make the actions idempotent.
|
|
544
|
-
- **
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
547
|
+
- **An approval is collected once (0.12.0).** Before acting on a person's approval, the blocking wait,
|
|
548
|
+
the durable resume, Temporal and `applyVerifiedResume` claim it (`POST /agent/claim/<id>`) with a key
|
|
549
|
+
random to that run, and run only if Decidio says this run collected it - otherwise
|
|
550
|
+
`DecidioBlockedError`: "This approval was already used by another run. Ask again for a new
|
|
551
|
+
approval." Auto-approvals are never claimed; reading the status never claims. Every authorize with a
|
|
552
|
+
request commitment also sends a request key (a fresh id per call, your own `requestKey`, or the
|
|
553
|
+
engine's id for the call), and a retried authorize carries the same key.
|
|
554
|
+
- **Known limit — LangGraph and Inngest do not claim yet.** Two concurrent resumes of the same LangGraph
|
|
555
|
+
thread (the checkpoint is your storage) can both pass the check and both run the action; resume each
|
|
556
|
+
one from one place (one worker per thread id, or a lock). Inngest relies on its own step semantics.
|
|
549
557
|
- **Upgrade the gate first.** A keyed SDK refuses a routed approval from a Decidio gate that does not
|
|
550
558
|
return commitments (`commitment_missing`) — it fails closed, never silently passes.
|
|
551
559
|
|
|
@@ -759,14 +767,15 @@ manager for production.
|
|
|
759
767
|
- `DecidioRequestChangedError` (0.7.0, a `DecidioBlockedError`) — Decidio approved the decision for a *different* request than the one about to run ("the request changed after approval"); nothing executed. Ask again for the request you mean to run.
|
|
760
768
|
- `DecidioApprovalUnverifiedError` (0.7.0, a `DecidioBlockedError`) — the approval could not be tied to this request: `code: "commitment_missing"` (Decidio returned no request commitment although this SDK sent one — typically a gate older than 0.7.0) or `"unverifiable"`. Nothing executed.
|
|
761
769
|
- `DecidioSuspendedError` — (durable mode) the action was routed and is parked for async approval; the process may exit and resume later. Not a failure.
|
|
762
|
-
- `DecidioTimeoutError` — (blocking mode only) no human decided within `pollTimeoutMs` (default 10 min); nothing executed locally. Since 0.6.4 the SDK's last act before throwing is to tell Decidio it gave up, so the request is closed as **withdrawn** (it leaves the queue; a later approval does nothing) — `error.withdrawn` is `true` when that landed and `false` when the call failed and the request may still be open. Withdrawing needs the bound agent token and the signing key `init` writes (an unbound or unsigned caller cannot withdraw anything — it just gets `withdrawn:false`). If a human decided in the last poll window, the SDK honours that decision instead: the action runs as approved, or `DecidioRejectedError` is thrown. The durable path never withdraws: its parked action is what a later approval resumes.
|
|
770
|
+
- `DecidioTimeoutError` — (blocking mode only) no human decided within `pollTimeoutMs` (default 10 min); nothing executed locally. Since 0.6.4 the SDK's last act before throwing is to tell Decidio it gave up, so the request is closed as **withdrawn** (it leaves the queue; a later approval does nothing) — `error.withdrawn` is `true` when that landed and `false` when the call failed and the request may still be open. Withdrawing needs the bound agent token and the signing key `init` writes (an unbound or unsigned caller cannot withdraw anything — it just gets `withdrawn:false`). If a human decided in the last poll window, the SDK honours that decision instead: the action runs as approved, or `DecidioRejectedError` is thrown. The durable path never withdraws: its parked action is what a later approval resumes. Since 0.12.0 a request that may be shared is never withdrawn: after an `idempotentReplay` or `openTwin` answer, or a 409 `SHARED_REQUEST`, `error.shared` is `true` and a person still decides it.
|
|
763
771
|
- `DecidioRateLimitError` — the workspace's governed-action limit was hit; carries the server's `limit` / `remaining` / `resetAt`.
|
|
764
772
|
- `DecidioHttpError` — the gate refused the call. Carries `status`, `endpoint`, the parsed `body`,
|
|
765
773
|
and **`retryable`** — the field to branch on. A 401/403 is terminal (the agent token expired, or
|
|
766
774
|
someone re-ran `init` for that agent and revoked it): retrying cannot help, so the message names
|
|
767
|
-
the fix. Anything else is transient
|
|
768
|
-
and
|
|
769
|
-
|
|
775
|
+
the fix. Anything else is transient: the SDK already retried the authorize with the same request key
|
|
776
|
+
(0.12.0) - a request still waiting is answered with the one already waiting - and retrying is safe: it
|
|
777
|
+
can never double-execute.
|
|
778
|
+
- `DecidioNetworkError` — the gate was unreachable (after the SDK's own retries, for the authorize). Nothing executed locally.
|
|
770
779
|
- `DecidioUnsafeArgumentsError` — your call arguments cannot survive JSON with their meaning intact, so nothing was routed and nothing ran. The message names the field and the fix. Most often: `NaN`/`Infinity`, `-0`, a `Date`, `Map` or class instance, a `BigInt`, or a sparse array — anything whose serialized form would differ from the value the approval and signature cover. Pass the already-serialized form.
|
|
771
780
|
|
|
772
781
|
```js
|
package/dist/adapters/_gate.d.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { type GuardConfig, type ApprovalContext, type EvidenceOutcome, type ApprovalCheck, type CommittedRequest } from "../index.js";
|
|
1
|
+
import { type GuardConfig, type ApprovalContext, type EvidenceOutcome, type ApprovalCheck, type ClaimOutcome, type CommittedRequest, type JsonValue } from "../index.js";
|
|
2
2
|
/** The ONLY authorization verdicts this SDK understands. A gate answer outside this set means
|
|
3
3
|
* the SDK does not understand the decision — and an answer you do not understand means NO. */
|
|
4
4
|
export declare const AUTHORIZE_VERDICTS: readonly ["proceed", "route", "block"];
|
|
5
5
|
export type AuthorizeVerdict = (typeof AUTHORIZE_VERDICTS)[number];
|
|
6
|
+
/** `requestKey` (0.12.0): the request key this authorize sent, when it sent one. `idempotentReplay` / `openTwin`: the gate
|
|
7
|
+
* answered with a request that already existed (the same key asked again, or the same keyless request another caller is
|
|
8
|
+
* waiting on). */
|
|
6
9
|
export interface GateAuth {
|
|
7
10
|
decision: AuthorizeVerdict;
|
|
8
11
|
decisionId: string;
|
|
9
12
|
receiptId?: string;
|
|
10
13
|
reason?: string;
|
|
14
|
+
requestKey?: string;
|
|
15
|
+
idempotentReplay?: boolean;
|
|
16
|
+
openTwin?: boolean;
|
|
11
17
|
}
|
|
12
18
|
/** Fail-closed verdict allow-list. Called INSIDE gateAuthorize, so every adapter that authorizes
|
|
13
19
|
* through the shared gate is protected by construction rather than by remembering — an adapter
|
|
@@ -57,10 +63,21 @@ export declare function gateAuthorizeTwinAware(config: GuardConfig, ctx: Approva
|
|
|
57
63
|
request?: CommittedRequest;
|
|
58
64
|
justification?: string;
|
|
59
65
|
}): Promise<GateAuth | GateDuplicate>;
|
|
66
|
+
/** `naturalId` (0.12.0, 11f): the engine's own name for THIS call - an OpenAI tool call id, a Temporal workflow run and
|
|
67
|
+
* activity, an Inngest run - so the engine retrying the same call is the same request. The request key is keyed over it
|
|
68
|
+
* and the commitment. Without one (and without `idempotencyKey`) the key is over a fresh id for this call, as on the core
|
|
69
|
+
* path. No key material: no commitment, so no key - as before. */
|
|
60
70
|
export declare function gateAuthorize(config: GuardConfig, ctx: ApprovalContext, opts?: {
|
|
61
71
|
idempotencyKey?: string;
|
|
62
72
|
request?: CommittedRequest;
|
|
73
|
+
naturalId?: Record<string, JsonValue>;
|
|
63
74
|
}): Promise<GateAuth>;
|
|
75
|
+
/** COLLECT A PERSON'S APPROVAL ONCE on an engine door (0.12.0, call 24) - claimApproval, as a plain-JSON value an engine
|
|
76
|
+
* that must run HTTP in an activity (Temporal) returns unchanged. */
|
|
77
|
+
export declare function gateClaim(config: GuardConfig, decisionId: string, claimKey: string): Promise<ClaimOutcome>;
|
|
78
|
+
/** Validate a claim outcome that crossed a serialization boundary (a Temporal activity result). Anything that is not a
|
|
79
|
+
* well-formed outcome for THIS decision is refused - "an answer you do not understand is never permission". */
|
|
80
|
+
export declare function assertClaimShape(raw: unknown, decisionId: string): ClaimOutcome;
|
|
64
81
|
/** Report an adapter-path execution and RETURN what Decidio recorded.
|
|
65
82
|
*
|
|
66
83
|
* Two 0.2.0 changes, both closing the same gap. It applies the caller's `evidence` mapper — the
|
package/dist/adapters/_gate.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { DecidioBlockedError, DecidioRejectedError, signBoundProof, signConfirm, assertSafeTransport, carriesCredential, canonicalize, assertDescribed, readConfirmResponse, buildConfirmBody, decisionIdProblem, bindingMaterial, requestCommitment, idempotencyKey, invocationRequestKey, sendAuthorize, checkApproval, claimApproval } from "../index.js";
|
|
3
4
|
export const AUTHORIZE_VERDICTS = ["proceed", "route", "block"];
|
|
4
5
|
export function assertAuthorizeVerdict(auth, fallbackDecisionId = "unknown") {
|
|
5
6
|
const a = auth;
|
|
@@ -45,48 +46,54 @@ export const isGateDuplicate = (a) => {
|
|
|
45
46
|
return a?.decision === "duplicate" && !!d && typeof d === "object" && typeof d.decisionId === "string" && decisionIdProblem(d.decisionId) === null;
|
|
46
47
|
};
|
|
47
48
|
export async function gateAuthorizeTwinAware(config, ctx, opts = {}) {
|
|
48
|
-
const a = await postAuthorize(config, ctx, { ...opts, twinCheck: true });
|
|
49
|
+
const { answer: a } = await postAuthorize(config, ctx, { ...opts, twinCheck: true });
|
|
49
50
|
if (isGateDuplicate(a))
|
|
50
51
|
return a;
|
|
51
52
|
return assertAuthorizeVerdict(a, "unknown");
|
|
52
53
|
}
|
|
53
54
|
export async function gateAuthorize(config, ctx, opts = {}) {
|
|
54
|
-
|
|
55
|
+
const { answer, requestKey } = await postAuthorize(config, ctx, opts);
|
|
56
|
+
const auth = assertAuthorizeVerdict(answer, "unknown");
|
|
57
|
+
return requestKey ? { ...auth, requestKey } : auth;
|
|
55
58
|
}
|
|
56
59
|
async function postAuthorize(config, ctx, opts = {}) {
|
|
57
60
|
ctx = canonicalize(ctx, "describe(...)");
|
|
58
61
|
assertDescribed(ctx);
|
|
59
62
|
assertSafeTransport(config.apiUrl, carriesCredential(config));
|
|
60
|
-
const f = config.fetchImpl ?? fetch;
|
|
61
63
|
const resumeUrl = config.resumeUrl ?? config.resume?.resumeUrl;
|
|
62
|
-
const
|
|
64
|
+
const sign = () => (config.agentKey && config.agentDid)
|
|
63
65
|
? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
|
|
64
66
|
: undefined;
|
|
67
|
+
const requesterIdentity = sign();
|
|
65
68
|
const material = bindingMaterial(config);
|
|
66
69
|
const commitment = material ? requestCommitment(material, opts.request ?? adapterRequest(ctx)) : undefined;
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}),
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
70
|
+
const requestKey = opts.idempotencyKey
|
|
71
|
+
?? (material && commitment
|
|
72
|
+
? (opts.naturalId ? idempotencyKey(material, { ...opts.naturalId, commitment }) : invocationRequestKey(material, config.invocationId ? String(config.invocationId()) : randomUUID(), commitment))
|
|
73
|
+
: undefined);
|
|
74
|
+
const answer = await sendAuthorize(config, {
|
|
75
|
+
requester: config.agentId, action: ctx.action, amount: ctx.amount, scope: ctx.scope,
|
|
76
|
+
sourceSystem: config.sourceSystem, ...(ctx.context ? { context: ctx.context } : {}),
|
|
77
|
+
...(requesterIdentity ? { requesterIdentity } : {}), ...(resumeUrl ? { resumeUrl } : {}),
|
|
78
|
+
...(commitment ? { requestCommitment: commitment } : {}),
|
|
79
|
+
...(requestKey ? { idempotencyKey: requestKey } : {}),
|
|
80
|
+
...(opts.twinCheck ? { twinCheck: true } : {}),
|
|
81
|
+
...(opts.justification ? { justification: opts.justification } : {}),
|
|
82
|
+
}, { rateLimit: "http", resign: sign });
|
|
83
|
+
return { answer, ...(requestKey ? { requestKey } : {}) };
|
|
84
|
+
}
|
|
85
|
+
export function gateClaim(config, decisionId, claimKey) {
|
|
86
|
+
return claimApproval(config, decisionId, claimKey);
|
|
87
|
+
}
|
|
88
|
+
export function assertClaimShape(raw, decisionId) {
|
|
89
|
+
const c = raw;
|
|
90
|
+
if (c && typeof c === "object" && c.decisionId === decisionId) {
|
|
91
|
+
if (c.ok === true && typeof c.again === "boolean")
|
|
92
|
+
return { ok: true, decisionId, again: c.again };
|
|
93
|
+
if (c.ok === false && typeof c.code === "string" && typeof c.reason === "string" && typeof c.final === "boolean")
|
|
94
|
+
return c;
|
|
88
95
|
}
|
|
89
|
-
return
|
|
96
|
+
return { ok: false, decisionId, code: "unrecognized", final: false, reason: `the claim for decision ${decisionId} came back malformed (${JSON.stringify(raw ?? null).slice(0, 120)}) - return claimApprovalActivity(...) from your activity unchanged. Nothing ran.` };
|
|
90
97
|
}
|
|
91
98
|
export async function gateConfirm(config, decisionId, result, evidence) {
|
|
92
99
|
const f = config.fetchImpl ?? fetch;
|
|
@@ -58,7 +58,7 @@ export async function decidioGate(config, ctx, run, opts) {
|
|
|
58
58
|
}
|
|
59
59
|
const result = await run();
|
|
60
60
|
const confirmation = await gateConfirm(config, auth.decisionId, result, opts?.evidence);
|
|
61
|
-
return { value: result, decisionId: auth.decisionId, confirmation };
|
|
61
|
+
return { value: result, decisionId: auth.decisionId, confirmation, ...(typeof auth.requestKey === "string" ? { requestKey: auth.requestKey } : {}) };
|
|
62
62
|
}
|
|
63
63
|
export function decidioResumeCommand(body) {
|
|
64
64
|
const decisionId = body?.decisionId;
|
|
@@ -34,9 +34,10 @@ interface RunStateLike {
|
|
|
34
34
|
* the call itself - its name, call id and exact argument string (toolCallRequest). An item edited after
|
|
35
35
|
* approval, or a second identical call (another call id), never rides the approval.
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* the tool
|
|
37
|
+
* ONE APPROVAL, ONE RUN (0.12.0, founder call 24): applyVerifiedResume COLLECTS a person's approval before it
|
|
38
|
+
* approves the item (POST /agent/claim, a key random per call), so two concurrent resumes of the same parked state
|
|
39
|
+
* cannot both run the tool - the second is told the approval was already used and the RunState is not modified.
|
|
40
|
+
* A request key rides with every authorize, keyed on the tool call id: the same call asked again is the same request.
|
|
40
41
|
*
|
|
41
42
|
* Duck-typed on the state/item (no hard @openai/agents import) so it stays thin.
|
|
42
43
|
*
|
|
@@ -82,10 +83,16 @@ export declare const gateInterruptions: typeof authorizeInterruptions;
|
|
|
82
83
|
* state.reject(item), returns that verdict. Still pending, or approved for a DIFFERENT request, or not
|
|
83
84
|
* tied to one → throws the typed refusal and the RunState is NOT modified.
|
|
84
85
|
*
|
|
86
|
+
* A PERSON'S approval is COLLECTED ONCE before the item is approved (0.12.0, call 24): with `opts.claimKey` (yours to
|
|
87
|
+
* keep for a retry of THIS resume) or a fresh random one. Another run already used it, or it expired, or the claim could
|
|
88
|
+
* not be made → DecidioBlockedError, saying so, and the RunState is NOT modified. An auto-approval is never claimed.
|
|
89
|
+
*
|
|
85
90
|
* REPLACES `applyResume(state, item, verdict)`, REMOVED in 0.7.0. It applied a bare verdict string
|
|
86
91
|
* with no decision attached, so correctness depended entirely on the host routing the right verdict
|
|
87
92
|
* to the right item. It was removed rather than deprecated: a deprecated function that still approves
|
|
88
93
|
* keeps the hole open for every integration that never reads the warning, and one that refused
|
|
89
94
|
* approvals at runtime would fail AFTER a human approved - the removal fails at build time instead. */
|
|
90
|
-
export declare function applyVerifiedResume(config: GuardConfig, state: RunStateLike, item: ToolApprovalItem, decisionId: string, describe: (item: ToolApprovalItem) => ApprovalContext
|
|
95
|
+
export declare function applyVerifiedResume(config: GuardConfig, state: RunStateLike, item: ToolApprovalItem, decisionId: string, describe: (item: ToolApprovalItem) => ApprovalContext, opts?: {
|
|
96
|
+
claimKey?: string;
|
|
97
|
+
}): Promise<"approved" | "rejected" | "blocked">;
|
|
91
98
|
export {};
|
package/dist/adapters/openai.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { gateAuthorize, gateCheckApproval, toolCallRequest, checkEngineVersion } from "./_gate.js";
|
|
2
|
-
import { DecidioBlockedError, approvalRefusal, decisionIdProblem } from "../index.js";
|
|
1
|
+
import { gateAuthorize, gateCheckApproval, gateClaim, toolCallRequest, checkEngineVersion } from "./_gate.js";
|
|
2
|
+
import { DecidioBlockedError, approvalRefusal, claimRefusal, decisionIdProblem, newClaimKey } from "../index.js";
|
|
3
3
|
export async function authorizeInterruptions(config, state, describe) {
|
|
4
4
|
const resolved = [];
|
|
5
5
|
const pending = [];
|
|
6
6
|
const batch = state.getInterruptions().map((item) => { const ctx = describe(item); return { item, ctx, request: toolCallRequest(item, ctx) }; });
|
|
7
7
|
for (const { item, ctx, request } of batch) {
|
|
8
|
-
const auth = await gateAuthorize(config, ctx, { request });
|
|
8
|
+
const auth = await gateAuthorize(config, ctx, { request, naturalId: { engine: "openai-agents", callId: request.args[0].callId } });
|
|
9
9
|
if (auth.decision === "proceed") {
|
|
10
10
|
state.approve(item);
|
|
11
11
|
resolved.push({ item, verdict: "approved", decisionId: auth.decisionId });
|
|
@@ -22,13 +22,21 @@ export async function authorizeInterruptions(config, state, describe) {
|
|
|
22
22
|
return { resolved, pending, confirmationSupported: false };
|
|
23
23
|
}
|
|
24
24
|
export const gateInterruptions = authorizeInterruptions;
|
|
25
|
-
export async function applyVerifiedResume(config, state, item, decisionId, describe) {
|
|
25
|
+
export async function applyVerifiedResume(config, state, item, decisionId, describe, opts = {}) {
|
|
26
26
|
const idProblem = decisionIdProblem(decisionId);
|
|
27
27
|
if (idProblem)
|
|
28
28
|
throw new DecidioBlockedError(`applyVerifiedResume needs the decision this item was routed as (${idProblem}) - pass the decisionId authorizeInterruptions returned for it. The RunState was NOT modified.`, typeof decisionId === "string" ? decisionId : "");
|
|
29
29
|
const ctx = describe(item);
|
|
30
30
|
const check = await gateCheckApproval(config, decisionId, ctx, toolCallRequest(item, ctx));
|
|
31
31
|
if (check.ok) {
|
|
32
|
+
if (check.state === "approved") {
|
|
33
|
+
const claimed = await gateClaim(config, decisionId, opts.claimKey ?? newClaimKey());
|
|
34
|
+
if (!claimed.ok) {
|
|
35
|
+
const refusal = claimRefusal(claimed);
|
|
36
|
+
refusal.message += " The RunState was NOT modified.";
|
|
37
|
+
throw refusal;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
32
40
|
state.approve(item);
|
|
33
41
|
return "approved";
|
|
34
42
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type GuardConfig, type ApprovalContext, type ProtectedExecution, type EvidenceOutcome, type ApprovalCheck } from "../index.js";
|
|
1
|
+
import { type GuardConfig, type ApprovalContext, type ProtectedExecution, type EvidenceOutcome, type ApprovalCheck, type ClaimOutcome } from "../index.js";
|
|
2
2
|
import { type GateAuth } from "./_gate.js";
|
|
3
3
|
/**
|
|
4
4
|
* @decidio/sdk/temporal — durable suspend via Temporal's native `condition()` + a
|
|
@@ -8,8 +8,9 @@ import { type GateAuth } from "./_gate.js";
|
|
|
8
8
|
* Inside your workflow:
|
|
9
9
|
* const verdicts = installDecidioSignal(); // wires the resume signal
|
|
10
10
|
* const out = await decidioGate(config, ctx, () => createOpp(o), verdicts, {
|
|
11
|
-
* authorize: () => executeActivity(authorizeActivity, ...), // HTTP belongs in activities
|
|
11
|
+
* authorize: () => executeActivity(authorizeActivity, ...), // HTTP belongs in activities; body: return authorizeActivity(config, ctx)
|
|
12
12
|
* verify: (id) => executeActivity(verifyActivity, id), // body: return verifyApprovalActivity(config, id, ctx)
|
|
13
|
+
* claim: (id, key) => executeActivity(claimActivity, id, key), // body: return claimApprovalActivity(config, id, key)
|
|
13
14
|
* confirm: (id, r) => executeActivity(confirmActivity, ...),
|
|
14
15
|
* });
|
|
15
16
|
* Decidio's webhook bridges to: handle.signal(decidioResumeSignal, decisionId, verdict, reason)
|
|
@@ -21,6 +22,13 @@ import { type GateAuth } from "./_gate.js";
|
|
|
21
22
|
* → the typed refusal. `verify` is HTTP, so it must be an ACTIVITY, never workflow code (determinism) -
|
|
22
23
|
* and it is required whenever `authorize` is.
|
|
23
24
|
*
|
|
25
|
+
* ONE APPROVAL, ONE RUN (0.12.0, founder call 24): once Decidio confirms a PERSON'S approval, the workflow collects
|
|
26
|
+
* it (`claim`, an activity) with a key made from the workflow's own deterministic uuid4 - the same key on a replay or an
|
|
27
|
+
* activity retry, a different one in any other run - and runs only if Decidio says this run collected it. Another run
|
|
28
|
+
* already used it, or it expired → DecidioBlockedError, and the action never runs. `claim` is required whenever
|
|
29
|
+
* `authorize` is. A request key rides with every authorize: `authorizeActivity` keys it on the activity's own workflow,
|
|
30
|
+
* run and activity ids, so Temporal retrying the activity is the same request.
|
|
31
|
+
*
|
|
24
32
|
* (This wiring doc ships in the .d.ts — the README points here.)
|
|
25
33
|
*/
|
|
26
34
|
export declare const decidioResumeSignal: import("@temporalio/workflow").SignalDefinition<[string, string, (string | undefined)?]>;
|
|
@@ -35,6 +43,20 @@ export declare function installDecidioSignal(): Map<string, {
|
|
|
35
43
|
* and route `verify` through it. GET /agent/status + the request commitment recomputed over `ctx`, which
|
|
36
44
|
* must be the same context the gate authorized. Never call it from workflow code (it is HTTP). */
|
|
37
45
|
export declare function verifyApprovalActivity(config: GuardConfig, decisionId: string, ctx: ApprovalContext): Promise<ApprovalCheck>;
|
|
46
|
+
/** The ids that name ONE activity execution across its retries: the workflow, its run, and the activity. */
|
|
47
|
+
export interface TemporalActivityIds {
|
|
48
|
+
workflowId: string;
|
|
49
|
+
runId: string;
|
|
50
|
+
activityId: string;
|
|
51
|
+
}
|
|
52
|
+
/** The authorize ACTIVITY body (0.12.0) - register an activity that returns this, unchanged, and route `authorize`
|
|
53
|
+
* through it. The request key is keyed on Temporal's own ids for this activity (11f): passed as `ids`, or read from the
|
|
54
|
+
* running activity's context - so Temporal retrying the activity after a timeout is the SAME request while it waits.
|
|
55
|
+
* Outside an activity, with no `ids`, the key is over a fresh id for the call. */
|
|
56
|
+
export declare function authorizeActivity(config: GuardConfig, ctx: ApprovalContext, ids?: TemporalActivityIds): Promise<GateAuth>;
|
|
57
|
+
/** The claim as an ACTIVITY body (0.12.0, call 24) - register an activity that returns this, unchanged, and route `claim`
|
|
58
|
+
* through it: POST /agent/claim with the key the workflow made. Never call it from workflow code (it is HTTP). */
|
|
59
|
+
export declare function claimApprovalActivity(config: GuardConfig, decisionId: string, claimKey: string): Promise<ClaimOutcome>;
|
|
38
60
|
export interface DecidioGateOpts<R = unknown> {
|
|
39
61
|
authorize?: () => Promise<GateAuth>;
|
|
40
62
|
/** THE APPROVAL CHECK, run after the resume signal and before `run()` (0.7.0). It is HTTP, so route it
|
|
@@ -42,6 +64,11 @@ export interface DecidioGateOpts<R = unknown> {
|
|
|
42
64
|
* `ctx` this gate authorized - and return that value unchanged. Required whenever `authorize` is
|
|
43
65
|
* supplied; without either it defaults to a direct call (tests only - a workflow must not do I/O). */
|
|
44
66
|
verify?: (decisionId: string) => Promise<ApprovalCheck>;
|
|
67
|
+
/** THE CLAIM (0.12.0, call 24), run after the approval check and before `run()` when a PERSON approved: collect the
|
|
68
|
+
* approval for this run, once. HTTP, so route it through an ACTIVITY whose body is
|
|
69
|
+
* `return claimApprovalActivity(config, decisionId, claimKey)` - return that value unchanged. Required whenever
|
|
70
|
+
* `authorize` is supplied; without either it defaults to a direct call (tests only). */
|
|
71
|
+
claim?: (decisionId: string, claimKey: string) => Promise<ClaimOutcome>;
|
|
45
72
|
/** Reports through YOUR activity. Return the gate's confirm response body (validated with
|
|
46
73
|
* `readConfirmResponse`) and it becomes the outcome; return nothing and the outcome says the SDK could
|
|
47
74
|
* not read back what Decidio recorded (prodsec V035-03: a returned outcome used to be discarded). */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { condition, defineSignal, setHandler } from "@temporalio/workflow";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { condition, defineSignal, setHandler, uuid4 } from "@temporalio/workflow";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { DecidioBlockedError, approvalRefusal, claimRefusal, newClaimKey } from "../index.js";
|
|
4
|
+
import { gateAuthorize, gateConfirm, gateCheckApproval, gateClaim, assertApprovalCheckShape, assertClaimShape, checkEngineVersion, assertAuthorizeVerdict } from "./_gate.js";
|
|
4
5
|
export const decidioResumeSignal = defineSignal("decidioResume");
|
|
5
6
|
export function installDecidioSignal() {
|
|
6
7
|
const verdicts = new Map();
|
|
@@ -10,12 +11,40 @@ export function installDecidioSignal() {
|
|
|
10
11
|
export function verifyApprovalActivity(config, decisionId, ctx) {
|
|
11
12
|
return gateCheckApproval(config, decisionId, ctx);
|
|
12
13
|
}
|
|
14
|
+
function currentActivityIds() {
|
|
15
|
+
try {
|
|
16
|
+
const info = createRequire(import.meta.url)("@temporalio/activity").Context.current().info;
|
|
17
|
+
const ids = { workflowId: info?.workflowExecution?.workflowId, runId: info?.workflowExecution?.runId, activityId: info?.activityId };
|
|
18
|
+
return typeof ids.workflowId === "string" && typeof ids.runId === "string" && typeof ids.activityId === "string" ? ids : undefined;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function authorizeActivity(config, ctx, ids) {
|
|
25
|
+
const natural = ids ?? currentActivityIds();
|
|
26
|
+
return gateAuthorize(config, ctx, natural ? { naturalId: { engine: "temporal", workflowId: natural.workflowId, runId: natural.runId, activityId: natural.activityId } } : {});
|
|
27
|
+
}
|
|
28
|
+
export function claimApprovalActivity(config, decisionId, claimKey) {
|
|
29
|
+
return gateClaim(config, decisionId, claimKey);
|
|
30
|
+
}
|
|
31
|
+
function workflowClaimKey() {
|
|
32
|
+
try {
|
|
33
|
+
return `run_${uuid4()}`;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return newClaimKey();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
13
39
|
let confirmWarned = false;
|
|
14
40
|
export async function decidioGate(config, ctx, run, verdicts, opts = {}) {
|
|
15
41
|
if (opts.authorize && !opts.verify) {
|
|
16
42
|
throw new Error("[decidio] temporal: `authorize` runs in your activity, so the approval check must too - pass `verify: (decisionId) => executeActivity(verifyActivity, decisionId)`, where the activity returns verifyApprovalActivity(config, decisionId, ctx) (from @decidio/sdk/temporal) unchanged. Since 0.7.0 a resume signal only wakes the workflow; the approval is confirmed with Decidio before your action runs. Nothing was routed or executed.");
|
|
17
43
|
}
|
|
18
|
-
|
|
44
|
+
if (opts.authorize && !opts.claim) {
|
|
45
|
+
throw new Error("[decidio] temporal: `authorize` runs in your activity, so the claim must too - pass `claim: (decisionId, claimKey) => executeActivity(claimActivity, decisionId, claimKey)`, where the activity returns claimApprovalActivity(config, decisionId, claimKey) (from @decidio/sdk/temporal) unchanged. Since 0.12.0 a person's approval is collected once, by the run that acts on it, before your action runs. Nothing was routed or executed.");
|
|
46
|
+
}
|
|
47
|
+
const auth = assertAuthorizeVerdict(await (opts.authorize ?? (() => authorizeActivity(config, ctx)))(), "unknown");
|
|
19
48
|
if (auth.decision === "block")
|
|
20
49
|
throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
|
|
21
50
|
if (auth.decision === "route") {
|
|
@@ -24,8 +53,15 @@ export async function decidioGate(config, ctx, run, verdicts, opts = {}) {
|
|
|
24
53
|
await condition(() => verdicts.has(auth.decisionId));
|
|
25
54
|
verdicts.delete(auth.decisionId);
|
|
26
55
|
const check = assertApprovalCheckShape(await verify(auth.decisionId), auth.decisionId);
|
|
27
|
-
if (check.ok)
|
|
56
|
+
if (check.ok) {
|
|
57
|
+
if (check.state === "approved") {
|
|
58
|
+
const claim = opts.claim ?? ((decisionId, claimKey) => gateClaim(config, decisionId, claimKey));
|
|
59
|
+
const claimed = assertClaimShape(await claim(auth.decisionId, workflowClaimKey()), auth.decisionId);
|
|
60
|
+
if (!claimed.ok)
|
|
61
|
+
throw claimRefusal(claimed);
|
|
62
|
+
}
|
|
28
63
|
break;
|
|
64
|
+
}
|
|
29
65
|
if (check.code !== "pending")
|
|
30
66
|
throw approvalRefusal(check);
|
|
31
67
|
}
|
|
@@ -52,6 +88,6 @@ export async function decidioGate(config, ctx, run, verdicts, opts = {}) {
|
|
|
52
88
|
else {
|
|
53
89
|
confirmation = await gateConfirm(config, auth.decisionId, result, opts?.evidence);
|
|
54
90
|
}
|
|
55
|
-
return { value: result, decisionId: auth.decisionId, confirmation };
|
|
91
|
+
return { value: result, decisionId: auth.decisionId, confirmation, ...(typeof auth.requestKey === "string" ? { requestKey: auth.requestKey } : {}) };
|
|
56
92
|
}
|
|
57
93
|
checkEngineVersion("temporal");
|
package/dist/cli.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export declare function defaultAgentId(dir?: string): string;
|
|
|
18
18
|
* distinct decision ids can never share a file (first-run review of 0.4.1: dots were STRIPPED, `a.b` and `ab`
|
|
19
19
|
* collided). `~` is outside the allowed set, so the escape is unambiguous. Exported for the smokes. */
|
|
20
20
|
export declare function receiptFileName(id: string): string;
|
|
21
|
+
/** Exactly what the person types to pause or resume the guard - nothing else is read. PURE. */
|
|
22
|
+
export declare function guardWords(prompt: unknown): "pause" | "start" | null;
|
|
21
23
|
/** Scoped packs (GitHub App design §2): a pack's scope in words - the organisation's, a unit's (its type, and its name when
|
|
22
24
|
* the server sent one: names that fail its person check are withheld there), or the reader's own. Null for an older server. */
|
|
23
25
|
export declare function packScopeLabel(scope: any): string | null;
|