@agentic-kit/run-log-gate 0.2.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/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
4
+ Copyright (c) 2025 Constructive <developers@constructive.io>
5
+ Copyright (c) 2020-present, Interweb, Inc.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # @agentic-kit/run-log-gate
2
+
3
+ Tool approvals over the run log. The request, the human's answer and every
4
+ settled gate decision are entries in the run's log — `agent_event` rows once
5
+ the store is the platform's — so a desktop run, a cloud pi Job and the agentic
6
+ lane all ask in one place and are answered from any surface that projects the
7
+ log (`RunsPanel`, `pendingApprovals` → `resolveApproval`).
8
+
9
+ ## Three pieces
10
+
11
+ - `runLogApprovals` — an `ApprovalChannel` for `@agentic-kit/harness`'s
12
+ `RunGate`. Submits a `constructive.approval.request` entry and polls the log
13
+ for the matching `constructive.approval.resolution`. The request entry's id
14
+ is derived from the tool call id, so a retried submit is the same entry and
15
+ the store's idempotency drops it instead of asking a human twice.
16
+ `resolveApproval` is the other end for a local run and the tests.
17
+ - `runLogGateDecisions` — an `onDecision` sink that appends every settled
18
+ decision as a `constructive.gate.decision` entry (audit: which calls the gate
19
+ refused, on whose authority) and a `flush()` that rethrows the first write
20
+ that failed.
21
+ - `createRunLogGateHost` — the harness's `GateHost` (what `createConfirmGate`
22
+ and `createGatedToolset` drive) built on the two above: `confirmTool` asks in
23
+ the log and returns what the log settled on, `notifyToolSkipped` files the
24
+ skipped repeat as a denial, `drain()` surfaces a lost write.
25
+
26
+ Nothing here executes on the strength of a click: the decision is the record
27
+ that was persisted and read back. A host may mirror a concise notice into a
28
+ conversation thread, but the thread is never the authority.
29
+
30
+ ## Consumers
31
+
32
+ In constructive-db: `pi-job-host` (the cloud pi Job — `RunGate` over
33
+ `runLogApprovals` + `runLogGateDecisions`), the `code_task` agentic lane and
34
+ `ui/desktop`'s capability gate (`createRunLogGateHost`).
35
+
36
+ ## Testing
37
+
38
+ `pnpm test` — the store (`MemoryRunLogStore`), the clock and the sleep are all
39
+ injectable, so every path is covered without a database or a network.
40
+
41
+ ---
42
+
43
+ ## Education and Tutorials
44
+
45
+ 1. 🚀 [Quickstart: Getting Up and Running](https://constructive.io/learn/quickstart)
46
+ Get started with modular databases in minutes. Install prerequisites and deploy your first module.
47
+
48
+ 2. 📦 [Modular PostgreSQL Development with Database Packages](https://constructive.io/learn/modular-postgres)
49
+ Learn to organize PostgreSQL projects with pgpm workspaces and reusable database modules.
50
+
51
+ 3. ✏️ [Authoring Database Changes](https://constructive.io/learn/authoring-database-changes)
52
+ Master the workflow for adding, organizing, and managing database changes with pgpm.
53
+
54
+ 4. 🧪 [End-to-End PostgreSQL Testing with TypeScript](https://constructive.io/learn/e2e-postgres-testing)
55
+ Master end-to-end PostgreSQL testing with ephemeral databases, RLS testing, and CI/CD automation.
56
+
57
+ 5. ⚡ [Supabase Testing](https://constructive.io/learn/supabase)
58
+ Use TypeScript-first tools to test Supabase projects with realistic RLS, policies, and auth contexts.
59
+
60
+ 6. 💧 [Drizzle ORM Testing](https://constructive.io/learn/drizzle-testing)
61
+ Run full-stack tests with Drizzle ORM, including database setup, teardown, and RLS enforcement.
62
+
63
+ 7. 🔧 [Troubleshooting](https://constructive.io/learn/troubleshooting)
64
+ Common issues and solutions for pgpm, PostgreSQL, and testing.
65
+
66
+ ## Related Constructive Tooling
67
+
68
+ ### 📦 Package Management
69
+
70
+ * [pgpm](https://github.com/constructive-io/constructive/tree/main/pgpm/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.
71
+
72
+ ### 🧪 Testing
73
+
74
+ * [pgsql-test](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
75
+ * [pglite-test](https://github.com/constructive-io/constructive/tree/main/postgres/pglite-test): **🪶 Drop-in pgsql-test replacement backed by PGlite** — in-process Postgres, no server required, instance-per-suite isolation.
76
+ * [pgsql-seed](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-seed): **🌱 PostgreSQL seeding utilities** for CSV, JSON, SQL data loading, and pgpm deployment.
77
+ * [supabase-test](https://github.com/constructive-io/constructive/tree/main/postgres/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
78
+ * [graphile-test](https://github.com/constructive-io/constructive/tree/main/graphile/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
79
+ * [pg-query-context](https://github.com/constructive-io/constructive/tree/main/postgres/pg-query-context): **🔒 Session context injection** to add session-local context (e.g., `SET LOCAL`) into queries—ideal for setting `role`, `jwt.claims`, and other session settings.
80
+
81
+ ### 🧠 Parsing & AST
82
+
83
+ * [pgsql-parser](https://www.npmjs.com/package/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax.
84
+ * [libpg-query-node](https://www.npmjs.com/package/libpg-query): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees.
85
+ * [pg-proto-parser](https://www.npmjs.com/package/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
86
+ * [@pgsql/enums](https://www.npmjs.com/package/@pgsql/enums): **🏷️ TypeScript enums** for PostgreSQL AST for safe and ergonomic parsing logic.
87
+ * [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **📝 Type definitions** for PostgreSQL AST nodes in TypeScript.
88
+ * [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **🛠️ AST utilities** for constructing and transforming PostgreSQL syntax trees.
89
+
90
+ ### 📚 Documentation & Skills
91
+
92
+ * [constructive-skills](https://github.com/constructive-io/constructive-skills): **📖 Platform documentation and AI agent skills** — feature catalog, blueprint reference, SDK guides (i18n, billing, limits, events, uploads, security, entities, search, AI), and deployment guides.
93
+
94
+ Install skills for AI coding agents:
95
+
96
+ ```bash
97
+ # All platform skills (security, blueprints, codegen, billing, etc.)
98
+ npx skills add constructive-io/constructive-skills
99
+
100
+ # Individual repo skills (pgpm, testing, CLI, search, etc.)
101
+ npx skills add https://github.com/constructive-io/constructive --skill pgpm
102
+ npx skills add https://github.com/constructive-io/constructive --skill constructive-testing
103
+ ```
104
+
105
+ ## Credits
106
+
107
+ **🛠 Built by the [Constructive](https://constructive.io) team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**
108
+
109
+ ## Disclaimer
110
+
111
+ AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
112
+
113
+ No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
package/approvals.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ import type { ApprovalChannel, ApprovalRequest } from '@agentic-kit/harness';
2
+ import type { ApprovalResolutionInput, RunLogStore } from '@agentic-kit/run-log';
3
+ export interface RunLogApprovalsOptions {
4
+ store: RunLogStore;
5
+ runId: string;
6
+ /** Poll delay while a request is outstanding. */
7
+ intervalMs?: number;
8
+ /** Give up after this long; omit to wait as long as a human takes. */
9
+ timeoutMs?: number;
10
+ /** Verdict on timeout. The gate defaults it to `deny`. */
11
+ onTimeout?: 'deny' | 'allow';
12
+ /** Records per read while polling. */
13
+ pageLimit?: number;
14
+ /**
15
+ * The text a human is shown for a request. Defaults to the tool's name and
16
+ * the policy's reason; a host that already composed a title and a message
17
+ * for the call passes them through here.
18
+ */
19
+ prompt?: (request: ApprovalRequest) => string;
20
+ now?: () => Date;
21
+ sleep?: (ms: number) => Promise<void>;
22
+ }
23
+ /**
24
+ * The entry id an approval request is written under.
25
+ *
26
+ * Deterministic on the tool call, so a retried submit is the same entry and the
27
+ * store's own idempotency drops the duplicate instead of asking a human twice.
28
+ */
29
+ export declare const approvalEntryId: (toolCallId: string) => string;
30
+ /**
31
+ * An `ApprovalChannel` whose request and answer are both records in the run's
32
+ * log.
33
+ */
34
+ export declare function runLogApprovals(options: RunLogApprovalsOptions): ApprovalChannel;
35
+ /**
36
+ * Answer a pending request by appending the resolution.
37
+ *
38
+ * The UI writes this through the API; a local run and the tests write it here.
39
+ * Either way it is one more entry in the same log, which is what makes "approve a
40
+ * cloud agent from a browser tab" nothing more than an insert.
41
+ */
42
+ export declare function resolveApproval(store: RunLogStore, runId: string, input: ApprovalResolutionInput, clock?: {
43
+ now?: () => Date;
44
+ }): Promise<void>;
45
+ /**
46
+ * The log's last entry id — the parent an out-of-band entry hangs from, so the
47
+ * rehydrated session file stays one tree rather than gaining a second root.
48
+ *
49
+ * Exported for the other writers of out-of-band entries (gate decisions), not
50
+ * for the package's consumers.
51
+ */
52
+ export declare function leafId(store: RunLogStore, runId: string, pageLimit?: number): Promise<string | null>;
package/approvals.js ADDED
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ // Approvals over the run log.
3
+ //
4
+ // The gate needs a human, and a Job has no socket to one: it writes the request
5
+ // into the log and reads the answer back out of it. Both directions are rows, so
6
+ // a browser hours later still sees the pending request — the log *is* the
7
+ // approval UI's state — and the run resumes the moment the resolution lands.
8
+ //
9
+ // `@agentic-kit/harness` already owns the waiting (`pollingApprovalChannel`);
10
+ // this is only its two ends bound to a `RunLogStore`.
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.approvalEntryId = void 0;
13
+ exports.runLogApprovals = runLogApprovals;
14
+ exports.resolveApproval = resolveApproval;
15
+ exports.leafId = leafId;
16
+ const harness_1 = require("@agentic-kit/harness");
17
+ const run_log_1 = require("@agentic-kit/run-log");
18
+ /**
19
+ * The entry id an approval request is written under.
20
+ *
21
+ * Deterministic on the tool call, so a retried submit is the same entry and the
22
+ * store's own idempotency drops the duplicate instead of asking a human twice.
23
+ */
24
+ const approvalEntryId = (toolCallId) => `approval-${toolCallId}`;
25
+ exports.approvalEntryId = approvalEntryId;
26
+ /**
27
+ * An `ApprovalChannel` whose request and answer are both records in the run's
28
+ * log.
29
+ */
30
+ function runLogApprovals(options) {
31
+ const { store, runId } = options;
32
+ const now = options.now ?? (() => new Date());
33
+ const prompt = options.prompt ?? defaultPrompt;
34
+ // Where reading resumed from last time: an approval may take hours, and
35
+ // re-reading the whole run per poll would grow with the transcript.
36
+ let cursor = run_log_1.START;
37
+ // Resolutions seen so far, kept across pages: an answer can land many pages
38
+ // after the request it answers.
39
+ const resolved = new Map();
40
+ const refresh = async () => {
41
+ const page = await store.read(runId, cursor, options.pageLimit);
42
+ if (page.records.length === 0)
43
+ return;
44
+ cursor = page.cursor;
45
+ for (const record of page.records) {
46
+ const outcome = resolutionOf(record);
47
+ if (outcome)
48
+ resolved.set(outcome.toolCallId, outcome.outcome);
49
+ }
50
+ };
51
+ const channelOptions = {
52
+ submit: async (request) => {
53
+ const parentId = await leafId(store, runId, options.pageLimit);
54
+ const entry = {
55
+ type: 'message',
56
+ id: (0, exports.approvalEntryId)(request.toolCallId),
57
+ parentId,
58
+ timestamp: now().toISOString(),
59
+ message: (0, run_log_1.approvalRequestMessage)({
60
+ toolCallId: request.toolCallId,
61
+ prompt: prompt(request)
62
+ })
63
+ };
64
+ await store.append(runId, [entry]);
65
+ },
66
+ poll: async (request) => {
67
+ await refresh();
68
+ return resolved.get(request.toolCallId);
69
+ },
70
+ // One clock for both the entry timestamps and the timeout, so a suite that
71
+ // fakes time fakes all of it.
72
+ now: () => now().getTime(),
73
+ ...(options.intervalMs === undefined ? {} : { intervalMs: options.intervalMs }),
74
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
75
+ ...(options.onTimeout === undefined ? {} : { onTimeout: options.onTimeout }),
76
+ ...(options.sleep === undefined ? {} : { sleep: options.sleep })
77
+ };
78
+ return (0, harness_1.pollingApprovalChannel)(channelOptions);
79
+ }
80
+ /**
81
+ * Answer a pending request by appending the resolution.
82
+ *
83
+ * The UI writes this through the API; a local run and the tests write it here.
84
+ * Either way it is one more entry in the same log, which is what makes "approve a
85
+ * cloud agent from a browser tab" nothing more than an insert.
86
+ */
87
+ async function resolveApproval(store, runId, input, clock = {}) {
88
+ const now = clock.now ?? (() => new Date());
89
+ const parentId = await leafId(store, runId);
90
+ const entry = {
91
+ type: 'message',
92
+ id: `resolution-${input.toolCallId}`,
93
+ parentId,
94
+ timestamp: now().toISOString(),
95
+ message: (0, run_log_1.approvalResolutionMessage)(input)
96
+ };
97
+ await store.append(runId, [entry]);
98
+ }
99
+ /**
100
+ * A record read as an approval answer, or `undefined` when it is anything else.
101
+ *
102
+ * The projectors surface a resolution only alongside the tool call it belongs
103
+ * to, and a Job polling for its own answer has not necessarily logged that call
104
+ * yet, so the answer is read straight off the record.
105
+ */
106
+ function resolutionOf(record) {
107
+ // This gate answers a pi session, so a record another harness wrote is not
108
+ // its answer to read.
109
+ if (record.transcriptFormat !== run_log_1.PI_TRANSCRIPT_FORMAT)
110
+ return undefined;
111
+ const entry = (0, run_log_1.assertPiSessionEntry)(record.entry);
112
+ if (!(0, run_log_1.isPiMessageEntry)(entry))
113
+ return undefined;
114
+ const message = entry.message;
115
+ if (message.role !== 'custom' || message.customType !== run_log_1.APPROVAL_RESOLUTION_TYPE) {
116
+ return undefined;
117
+ }
118
+ const details = typeof message.details === 'object' && message.details !== null
119
+ ? message.details
120
+ : {};
121
+ const toolCallId = details.toolCallId;
122
+ if (typeof toolCallId !== 'string' || toolCallId.length === 0) {
123
+ throw new Error(`approval resolution at seq ${String(record.seq)} carries no toolCallId — the ` +
124
+ 'gate cannot tell which tool call it answers');
125
+ }
126
+ const { reason, actorId } = details;
127
+ return {
128
+ toolCallId,
129
+ outcome: {
130
+ decision: details.decision === 'approved' ? 'allow' : 'deny',
131
+ ...(typeof reason === 'string' ? { reason } : {}),
132
+ ...(typeof actorId === 'string' ? { actorId } : {})
133
+ }
134
+ };
135
+ }
136
+ const defaultPrompt = ({ toolName, reason }) => reason ? `${toolName}: ${reason}` : `${toolName} needs approval`;
137
+ /**
138
+ * The log's last entry id — the parent an out-of-band entry hangs from, so the
139
+ * rehydrated session file stays one tree rather than gaining a second root.
140
+ *
141
+ * Exported for the other writers of out-of-band entries (gate decisions), not
142
+ * for the package's consumers.
143
+ */
144
+ async function leafId(store, runId, pageLimit) {
145
+ const records = await (0, run_log_1.readAll)(store, runId, run_log_1.START, pageLimit);
146
+ for (let index = records.length - 1; index >= 0; index -= 1) {
147
+ const entry = records[index].entry;
148
+ if (typeof entry.id === 'string' && entry.id.length > 0)
149
+ return entry.id;
150
+ }
151
+ return null;
152
+ }
@@ -0,0 +1,52 @@
1
+ import type { ApprovalChannel, ApprovalRequest } from '@agentic-kit/harness';
2
+ import type { ApprovalResolutionInput, RunLogStore } from '@agentic-kit/run-log';
3
+ export interface RunLogApprovalsOptions {
4
+ store: RunLogStore;
5
+ runId: string;
6
+ /** Poll delay while a request is outstanding. */
7
+ intervalMs?: number;
8
+ /** Give up after this long; omit to wait as long as a human takes. */
9
+ timeoutMs?: number;
10
+ /** Verdict on timeout. The gate defaults it to `deny`. */
11
+ onTimeout?: 'deny' | 'allow';
12
+ /** Records per read while polling. */
13
+ pageLimit?: number;
14
+ /**
15
+ * The text a human is shown for a request. Defaults to the tool's name and
16
+ * the policy's reason; a host that already composed a title and a message
17
+ * for the call passes them through here.
18
+ */
19
+ prompt?: (request: ApprovalRequest) => string;
20
+ now?: () => Date;
21
+ sleep?: (ms: number) => Promise<void>;
22
+ }
23
+ /**
24
+ * The entry id an approval request is written under.
25
+ *
26
+ * Deterministic on the tool call, so a retried submit is the same entry and the
27
+ * store's own idempotency drops the duplicate instead of asking a human twice.
28
+ */
29
+ export declare const approvalEntryId: (toolCallId: string) => string;
30
+ /**
31
+ * An `ApprovalChannel` whose request and answer are both records in the run's
32
+ * log.
33
+ */
34
+ export declare function runLogApprovals(options: RunLogApprovalsOptions): ApprovalChannel;
35
+ /**
36
+ * Answer a pending request by appending the resolution.
37
+ *
38
+ * The UI writes this through the API; a local run and the tests write it here.
39
+ * Either way it is one more entry in the same log, which is what makes "approve a
40
+ * cloud agent from a browser tab" nothing more than an insert.
41
+ */
42
+ export declare function resolveApproval(store: RunLogStore, runId: string, input: ApprovalResolutionInput, clock?: {
43
+ now?: () => Date;
44
+ }): Promise<void>;
45
+ /**
46
+ * The log's last entry id — the parent an out-of-band entry hangs from, so the
47
+ * rehydrated session file stays one tree rather than gaining a second root.
48
+ *
49
+ * Exported for the other writers of out-of-band entries (gate decisions), not
50
+ * for the package's consumers.
51
+ */
52
+ export declare function leafId(store: RunLogStore, runId: string, pageLimit?: number): Promise<string | null>;
@@ -0,0 +1,145 @@
1
+ // Approvals over the run log.
2
+ //
3
+ // The gate needs a human, and a Job has no socket to one: it writes the request
4
+ // into the log and reads the answer back out of it. Both directions are rows, so
5
+ // a browser hours later still sees the pending request — the log *is* the
6
+ // approval UI's state — and the run resumes the moment the resolution lands.
7
+ //
8
+ // `@agentic-kit/harness` already owns the waiting (`pollingApprovalChannel`);
9
+ // this is only its two ends bound to a `RunLogStore`.
10
+ import { pollingApprovalChannel } from '@agentic-kit/harness';
11
+ import { APPROVAL_RESOLUTION_TYPE, approvalRequestMessage, approvalResolutionMessage, assertPiSessionEntry, isPiMessageEntry, PI_TRANSCRIPT_FORMAT, readAll, START } from '@agentic-kit/run-log';
12
+ /**
13
+ * The entry id an approval request is written under.
14
+ *
15
+ * Deterministic on the tool call, so a retried submit is the same entry and the
16
+ * store's own idempotency drops the duplicate instead of asking a human twice.
17
+ */
18
+ export const approvalEntryId = (toolCallId) => `approval-${toolCallId}`;
19
+ /**
20
+ * An `ApprovalChannel` whose request and answer are both records in the run's
21
+ * log.
22
+ */
23
+ export function runLogApprovals(options) {
24
+ const { store, runId } = options;
25
+ const now = options.now ?? (() => new Date());
26
+ const prompt = options.prompt ?? defaultPrompt;
27
+ // Where reading resumed from last time: an approval may take hours, and
28
+ // re-reading the whole run per poll would grow with the transcript.
29
+ let cursor = START;
30
+ // Resolutions seen so far, kept across pages: an answer can land many pages
31
+ // after the request it answers.
32
+ const resolved = new Map();
33
+ const refresh = async () => {
34
+ const page = await store.read(runId, cursor, options.pageLimit);
35
+ if (page.records.length === 0)
36
+ return;
37
+ cursor = page.cursor;
38
+ for (const record of page.records) {
39
+ const outcome = resolutionOf(record);
40
+ if (outcome)
41
+ resolved.set(outcome.toolCallId, outcome.outcome);
42
+ }
43
+ };
44
+ const channelOptions = {
45
+ submit: async (request) => {
46
+ const parentId = await leafId(store, runId, options.pageLimit);
47
+ const entry = {
48
+ type: 'message',
49
+ id: approvalEntryId(request.toolCallId),
50
+ parentId,
51
+ timestamp: now().toISOString(),
52
+ message: approvalRequestMessage({
53
+ toolCallId: request.toolCallId,
54
+ prompt: prompt(request)
55
+ })
56
+ };
57
+ await store.append(runId, [entry]);
58
+ },
59
+ poll: async (request) => {
60
+ await refresh();
61
+ return resolved.get(request.toolCallId);
62
+ },
63
+ // One clock for both the entry timestamps and the timeout, so a suite that
64
+ // fakes time fakes all of it.
65
+ now: () => now().getTime(),
66
+ ...(options.intervalMs === undefined ? {} : { intervalMs: options.intervalMs }),
67
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
68
+ ...(options.onTimeout === undefined ? {} : { onTimeout: options.onTimeout }),
69
+ ...(options.sleep === undefined ? {} : { sleep: options.sleep })
70
+ };
71
+ return pollingApprovalChannel(channelOptions);
72
+ }
73
+ /**
74
+ * Answer a pending request by appending the resolution.
75
+ *
76
+ * The UI writes this through the API; a local run and the tests write it here.
77
+ * Either way it is one more entry in the same log, which is what makes "approve a
78
+ * cloud agent from a browser tab" nothing more than an insert.
79
+ */
80
+ export async function resolveApproval(store, runId, input, clock = {}) {
81
+ const now = clock.now ?? (() => new Date());
82
+ const parentId = await leafId(store, runId);
83
+ const entry = {
84
+ type: 'message',
85
+ id: `resolution-${input.toolCallId}`,
86
+ parentId,
87
+ timestamp: now().toISOString(),
88
+ message: approvalResolutionMessage(input)
89
+ };
90
+ await store.append(runId, [entry]);
91
+ }
92
+ /**
93
+ * A record read as an approval answer, or `undefined` when it is anything else.
94
+ *
95
+ * The projectors surface a resolution only alongside the tool call it belongs
96
+ * to, and a Job polling for its own answer has not necessarily logged that call
97
+ * yet, so the answer is read straight off the record.
98
+ */
99
+ function resolutionOf(record) {
100
+ // This gate answers a pi session, so a record another harness wrote is not
101
+ // its answer to read.
102
+ if (record.transcriptFormat !== PI_TRANSCRIPT_FORMAT)
103
+ return undefined;
104
+ const entry = assertPiSessionEntry(record.entry);
105
+ if (!isPiMessageEntry(entry))
106
+ return undefined;
107
+ const message = entry.message;
108
+ if (message.role !== 'custom' || message.customType !== APPROVAL_RESOLUTION_TYPE) {
109
+ return undefined;
110
+ }
111
+ const details = typeof message.details === 'object' && message.details !== null
112
+ ? message.details
113
+ : {};
114
+ const toolCallId = details.toolCallId;
115
+ if (typeof toolCallId !== 'string' || toolCallId.length === 0) {
116
+ throw new Error(`approval resolution at seq ${String(record.seq)} carries no toolCallId — the ` +
117
+ 'gate cannot tell which tool call it answers');
118
+ }
119
+ const { reason, actorId } = details;
120
+ return {
121
+ toolCallId,
122
+ outcome: {
123
+ decision: details.decision === 'approved' ? 'allow' : 'deny',
124
+ ...(typeof reason === 'string' ? { reason } : {}),
125
+ ...(typeof actorId === 'string' ? { actorId } : {})
126
+ }
127
+ };
128
+ }
129
+ const defaultPrompt = ({ toolName, reason }) => reason ? `${toolName}: ${reason}` : `${toolName} needs approval`;
130
+ /**
131
+ * The log's last entry id — the parent an out-of-band entry hangs from, so the
132
+ * rehydrated session file stays one tree rather than gaining a second root.
133
+ *
134
+ * Exported for the other writers of out-of-band entries (gate decisions), not
135
+ * for the package's consumers.
136
+ */
137
+ export async function leafId(store, runId, pageLimit) {
138
+ const records = await readAll(store, runId, START, pageLimit);
139
+ for (let index = records.length - 1; index >= 0; index -= 1) {
140
+ const entry = records[index].entry;
141
+ if (typeof entry.id === 'string' && entry.id.length > 0)
142
+ return entry.id;
143
+ }
144
+ return null;
145
+ }
@@ -0,0 +1,27 @@
1
+ import type { RunGateDecisionRecord } from '@agentic-kit/harness';
2
+ import type { RunLogStore } from '@agentic-kit/run-log';
3
+ export interface RunLogGateDecisionsOptions {
4
+ store: RunLogStore;
5
+ runId: string;
6
+ /** Records per read while looking up the parent entry. */
7
+ pageLimit?: number;
8
+ }
9
+ export interface RunLogGateDecisions {
10
+ /** Hand this to the gate as its `onDecision`. */
11
+ onDecision: (record: RunGateDecisionRecord) => void;
12
+ /**
13
+ * Wait for the queued writes and throw the first failure. A host calls this
14
+ * before it settles the run: a decision that never reached the log is a hole
15
+ * in the audit trail, not a detail.
16
+ */
17
+ flush(): Promise<void>;
18
+ }
19
+ /**
20
+ * The entry id a decision is written under.
21
+ *
22
+ * Deterministic on the tool call, so a retried execution that re-evaluates the
23
+ * same call writes the same entry and the store's idempotency drops the
24
+ * duplicate instead of logging the decision twice.
25
+ */
26
+ export declare const gateDecisionEntryId: (toolCallId: string) => string;
27
+ export declare function runLogGateDecisions(options: RunLogGateDecisionsOptions): RunLogGateDecisions;
@@ -0,0 +1,70 @@
1
+ // Gate decisions into the run log.
2
+ //
3
+ // An `ask` verdict is already durable — the request and its answer are records
4
+ // (see `approvals.ts`). A *policy* verdict is not: a rule that denies `bash`
5
+ // blocks the call, tells the model why, and leaves no trace, so "which tool
6
+ // calls did the gate block, and on whose authority" is unanswerable after the
7
+ // run. The gate already offers every settled decision through `onDecision`; this
8
+ // binds that callback to the log, which makes the transcript the audit trail for
9
+ // refusals as well as for approvals.
10
+ //
11
+ // The callback is synchronous and an append is not, so writes are queued onto one
12
+ // chain: ordering is preserved, the agent loop is never blocked on a log write,
13
+ // and the first failure is re-thrown from `flush()` rather than vanishing into an
14
+ // unhandled rejection.
15
+ import { gateDecisionMessage } from '@agentic-kit/run-log';
16
+ import { leafId } from './approvals';
17
+ /**
18
+ * The entry id a decision is written under.
19
+ *
20
+ * Deterministic on the tool call, so a retried execution that re-evaluates the
21
+ * same call writes the same entry and the store's idempotency drops the
22
+ * duplicate instead of logging the decision twice.
23
+ */
24
+ export const gateDecisionEntryId = (toolCallId) => `gate-${toolCallId}`;
25
+ export function runLogGateDecisions(options) {
26
+ const { store, runId } = options;
27
+ let queue = Promise.resolve();
28
+ let failure;
29
+ const append = async (record) => {
30
+ const parentId = await leafId(store, runId, options.pageLimit);
31
+ const entry = {
32
+ type: 'message',
33
+ id: gateDecisionEntryId(record.toolCallId),
34
+ parentId,
35
+ timestamp: record.decidedAt,
36
+ message: gateDecisionMessage({
37
+ toolCallId: record.toolCallId,
38
+ toolName: record.toolName,
39
+ verdict: record.verdict.decision,
40
+ decision: record.decision,
41
+ ...(record.reason === undefined ? {} : { reason: record.reason }),
42
+ ...(record.actorId === undefined ? {} : { actorId: record.actorId }),
43
+ decidedAt: record.decidedAt
44
+ })
45
+ };
46
+ await store.append(runId, [entry]);
47
+ };
48
+ return {
49
+ onDecision: (record) => {
50
+ queue = queue.then(async () => {
51
+ try {
52
+ await append(record);
53
+ }
54
+ catch (error) {
55
+ // Keep the first failure: it is the one with the cause, and a later
56
+ // write failing for the same reason would only bury it.
57
+ failure ??= error;
58
+ }
59
+ });
60
+ },
61
+ flush: async () => {
62
+ await queue;
63
+ if (failure !== undefined) {
64
+ const error = failure;
65
+ failure = undefined;
66
+ throw error;
67
+ }
68
+ }
69
+ };
70
+ }
@@ -0,0 +1,32 @@
1
+ import type { GateHost } from '@agentic-kit/harness';
2
+ import type { RunLogApprovalsOptions } from './approvals';
3
+ export interface RunLogGateRequest {
4
+ toolCallId: string;
5
+ title: string;
6
+ message: string;
7
+ }
8
+ export interface RunLogGateDecision {
9
+ toolCallId: string;
10
+ allowed: boolean;
11
+ reason?: string;
12
+ actorId?: string;
13
+ }
14
+ export interface RunLogGateHostOptions extends Omit<RunLogApprovalsOptions, 'prompt'> {
15
+ /** Told when a request is pending, so a host with a window can surface it. */
16
+ onRequested?: (request: RunLogGateRequest) => void;
17
+ /** Told what the log settled on, so a host can stop asking. */
18
+ onDecided?: (decision: RunLogGateDecision) => void;
19
+ }
20
+ export interface RunLogGateHost extends GateHost {
21
+ /**
22
+ * Wait for the queued writes and throw the first failure. Awaited once the
23
+ * turn ends: a decision that never reached the log is a hole in the audit
24
+ * trail, not a detail.
25
+ */
26
+ drain(): Promise<void>;
27
+ }
28
+ /** The text a human sees: the harness's title, then its message. */
29
+ export declare const gatePrompt: (title: string, message: string) => string;
30
+ /** Reason filed for a repeat of a declined call the gate skipped without asking. */
31
+ export declare const SKIPPED_REASON = "skipped: a repeat of a tool call the user already declined";
32
+ export declare function createRunLogGateHost(options: RunLogGateHostOptions): RunLogGateHost;
@@ -0,0 +1,91 @@
1
+ // The harness's `GateHost`, with the run log as its UI.
2
+ //
3
+ // `createConfirmGate` asks a host to confirm a call and tells it when a repeat
4
+ // of a declined call was skipped. Here both are records: the request goes into
5
+ // the run's log as a `constructive.approval.request` entry, the answer is read
6
+ // back as the `constructive.approval.resolution` a human wrote — from any
7
+ // surface that projects the log — and the outcome is filed as a
8
+ // `constructive.gate.decision`, the same audit entry a policy verdict leaves.
9
+ // So a desktop run, a cloud Job and the agentic lane ask and are answered in the
10
+ // one place, and nothing executes on the strength of a click: the decision is
11
+ // what was persisted and read back.
12
+ //
13
+ // `notifyToolSkipped` returns void, so its write is queued and `drain()` —
14
+ // awaited when the turn ends — rethrows the first failure rather than losing it.
15
+ import { runLogApprovals } from './approvals';
16
+ import { runLogGateDecisions } from './gate-decisions';
17
+ /** The text a human sees: the harness's title, then its message. */
18
+ export const gatePrompt = (title, message) => `${title}\n\n${message}`;
19
+ /** Reason filed for a repeat of a declined call the gate skipped without asking. */
20
+ export const SKIPPED_REASON = 'skipped: a repeat of a tool call the user already declined';
21
+ export function createRunLogGateHost(options) {
22
+ const { onRequested, onDecided, ...channelOptions } = options;
23
+ const { store, runId } = options;
24
+ const now = options.now ?? (() => new Date());
25
+ // Prompts are composed per call and the channel writes one per request, so
26
+ // the title and message travel by tool call id rather than through the
27
+ // channel's request shape.
28
+ const prompts = new Map();
29
+ const channel = runLogApprovals({
30
+ ...channelOptions,
31
+ prompt: (request) => prompts.get(request.toolCallId) ?? request.toolName
32
+ });
33
+ const decisions = runLogGateDecisions({
34
+ store,
35
+ runId,
36
+ ...(options.pageLimit === undefined ? {} : { pageLimit: options.pageLimit })
37
+ });
38
+ return {
39
+ hasUI: true,
40
+ async confirmTool(toolCallId, title, message) {
41
+ prompts.set(toolCallId, gatePrompt(title, message));
42
+ onRequested?.({ toolCallId, title, message });
43
+ try {
44
+ const outcome = await channel.request({
45
+ runId,
46
+ toolCallId,
47
+ toolName: title,
48
+ input: {},
49
+ reason: message,
50
+ requestedAt: now().toISOString()
51
+ });
52
+ decisions.onDecision({
53
+ runId,
54
+ toolCallId,
55
+ toolName: title,
56
+ input: {},
57
+ verdict: { decision: 'ask' },
58
+ decision: outcome.decision,
59
+ ...(outcome.reason === undefined ? {} : { reason: outcome.reason }),
60
+ ...(outcome.actorId === undefined ? {} : { actorId: outcome.actorId }),
61
+ decidedAt: now().toISOString()
62
+ });
63
+ onDecided?.({
64
+ toolCallId,
65
+ allowed: outcome.decision === 'allow',
66
+ ...(outcome.reason === undefined ? {} : { reason: outcome.reason }),
67
+ ...(outcome.actorId === undefined ? {} : { actorId: outcome.actorId })
68
+ });
69
+ return outcome.decision === 'allow';
70
+ }
71
+ finally {
72
+ prompts.delete(toolCallId);
73
+ }
74
+ },
75
+ notifyToolSkipped(toolCallId) {
76
+ // The harness names only the call, and the tool it repeats is in the
77
+ // transcript beside it; the projector attributes the entry by call id.
78
+ decisions.onDecision({
79
+ runId,
80
+ toolCallId,
81
+ toolName: '',
82
+ input: {},
83
+ verdict: { decision: 'deny', reason: SKIPPED_REASON },
84
+ decision: 'deny',
85
+ reason: SKIPPED_REASON,
86
+ decidedAt: now().toISOString()
87
+ });
88
+ },
89
+ drain: () => decisions.flush()
90
+ };
91
+ }
package/esm/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export type { RunLogApprovalsOptions } from './approvals';
2
+ export { approvalEntryId, resolveApproval, runLogApprovals } from './approvals';
3
+ export type { RunLogGateDecisions, RunLogGateDecisionsOptions } from './gate-decisions';
4
+ export { gateDecisionEntryId, runLogGateDecisions } from './gate-decisions';
5
+ export type { RunLogGateDecision, RunLogGateHost, RunLogGateHostOptions, RunLogGateRequest } from './gate-host';
6
+ export { createRunLogGateHost, gatePrompt, SKIPPED_REASON } from './gate-host';
package/esm/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { approvalEntryId, resolveApproval, runLogApprovals } from './approvals';
2
+ export { gateDecisionEntryId, runLogGateDecisions } from './gate-decisions';
3
+ export { createRunLogGateHost, gatePrompt, SKIPPED_REASON } from './gate-host';
@@ -0,0 +1,27 @@
1
+ import type { RunGateDecisionRecord } from '@agentic-kit/harness';
2
+ import type { RunLogStore } from '@agentic-kit/run-log';
3
+ export interface RunLogGateDecisionsOptions {
4
+ store: RunLogStore;
5
+ runId: string;
6
+ /** Records per read while looking up the parent entry. */
7
+ pageLimit?: number;
8
+ }
9
+ export interface RunLogGateDecisions {
10
+ /** Hand this to the gate as its `onDecision`. */
11
+ onDecision: (record: RunGateDecisionRecord) => void;
12
+ /**
13
+ * Wait for the queued writes and throw the first failure. A host calls this
14
+ * before it settles the run: a decision that never reached the log is a hole
15
+ * in the audit trail, not a detail.
16
+ */
17
+ flush(): Promise<void>;
18
+ }
19
+ /**
20
+ * The entry id a decision is written under.
21
+ *
22
+ * Deterministic on the tool call, so a retried execution that re-evaluates the
23
+ * same call writes the same entry and the store's idempotency drops the
24
+ * duplicate instead of logging the decision twice.
25
+ */
26
+ export declare const gateDecisionEntryId: (toolCallId: string) => string;
27
+ export declare function runLogGateDecisions(options: RunLogGateDecisionsOptions): RunLogGateDecisions;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ // Gate decisions into the run log.
3
+ //
4
+ // An `ask` verdict is already durable — the request and its answer are records
5
+ // (see `approvals.ts`). A *policy* verdict is not: a rule that denies `bash`
6
+ // blocks the call, tells the model why, and leaves no trace, so "which tool
7
+ // calls did the gate block, and on whose authority" is unanswerable after the
8
+ // run. The gate already offers every settled decision through `onDecision`; this
9
+ // binds that callback to the log, which makes the transcript the audit trail for
10
+ // refusals as well as for approvals.
11
+ //
12
+ // The callback is synchronous and an append is not, so writes are queued onto one
13
+ // chain: ordering is preserved, the agent loop is never blocked on a log write,
14
+ // and the first failure is re-thrown from `flush()` rather than vanishing into an
15
+ // unhandled rejection.
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.gateDecisionEntryId = void 0;
18
+ exports.runLogGateDecisions = runLogGateDecisions;
19
+ const run_log_1 = require("@agentic-kit/run-log");
20
+ const approvals_1 = require("./approvals");
21
+ /**
22
+ * The entry id a decision is written under.
23
+ *
24
+ * Deterministic on the tool call, so a retried execution that re-evaluates the
25
+ * same call writes the same entry and the store's idempotency drops the
26
+ * duplicate instead of logging the decision twice.
27
+ */
28
+ const gateDecisionEntryId = (toolCallId) => `gate-${toolCallId}`;
29
+ exports.gateDecisionEntryId = gateDecisionEntryId;
30
+ function runLogGateDecisions(options) {
31
+ const { store, runId } = options;
32
+ let queue = Promise.resolve();
33
+ let failure;
34
+ const append = async (record) => {
35
+ const parentId = await (0, approvals_1.leafId)(store, runId, options.pageLimit);
36
+ const entry = {
37
+ type: 'message',
38
+ id: (0, exports.gateDecisionEntryId)(record.toolCallId),
39
+ parentId,
40
+ timestamp: record.decidedAt,
41
+ message: (0, run_log_1.gateDecisionMessage)({
42
+ toolCallId: record.toolCallId,
43
+ toolName: record.toolName,
44
+ verdict: record.verdict.decision,
45
+ decision: record.decision,
46
+ ...(record.reason === undefined ? {} : { reason: record.reason }),
47
+ ...(record.actorId === undefined ? {} : { actorId: record.actorId }),
48
+ decidedAt: record.decidedAt
49
+ })
50
+ };
51
+ await store.append(runId, [entry]);
52
+ };
53
+ return {
54
+ onDecision: (record) => {
55
+ queue = queue.then(async () => {
56
+ try {
57
+ await append(record);
58
+ }
59
+ catch (error) {
60
+ // Keep the first failure: it is the one with the cause, and a later
61
+ // write failing for the same reason would only bury it.
62
+ failure ??= error;
63
+ }
64
+ });
65
+ },
66
+ flush: async () => {
67
+ await queue;
68
+ if (failure !== undefined) {
69
+ const error = failure;
70
+ failure = undefined;
71
+ throw error;
72
+ }
73
+ }
74
+ };
75
+ }
package/gate-host.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { GateHost } from '@agentic-kit/harness';
2
+ import type { RunLogApprovalsOptions } from './approvals';
3
+ export interface RunLogGateRequest {
4
+ toolCallId: string;
5
+ title: string;
6
+ message: string;
7
+ }
8
+ export interface RunLogGateDecision {
9
+ toolCallId: string;
10
+ allowed: boolean;
11
+ reason?: string;
12
+ actorId?: string;
13
+ }
14
+ export interface RunLogGateHostOptions extends Omit<RunLogApprovalsOptions, 'prompt'> {
15
+ /** Told when a request is pending, so a host with a window can surface it. */
16
+ onRequested?: (request: RunLogGateRequest) => void;
17
+ /** Told what the log settled on, so a host can stop asking. */
18
+ onDecided?: (decision: RunLogGateDecision) => void;
19
+ }
20
+ export interface RunLogGateHost extends GateHost {
21
+ /**
22
+ * Wait for the queued writes and throw the first failure. Awaited once the
23
+ * turn ends: a decision that never reached the log is a hole in the audit
24
+ * trail, not a detail.
25
+ */
26
+ drain(): Promise<void>;
27
+ }
28
+ /** The text a human sees: the harness's title, then its message. */
29
+ export declare const gatePrompt: (title: string, message: string) => string;
30
+ /** Reason filed for a repeat of a declined call the gate skipped without asking. */
31
+ export declare const SKIPPED_REASON = "skipped: a repeat of a tool call the user already declined";
32
+ export declare function createRunLogGateHost(options: RunLogGateHostOptions): RunLogGateHost;
package/gate-host.js ADDED
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ // The harness's `GateHost`, with the run log as its UI.
3
+ //
4
+ // `createConfirmGate` asks a host to confirm a call and tells it when a repeat
5
+ // of a declined call was skipped. Here both are records: the request goes into
6
+ // the run's log as a `constructive.approval.request` entry, the answer is read
7
+ // back as the `constructive.approval.resolution` a human wrote — from any
8
+ // surface that projects the log — and the outcome is filed as a
9
+ // `constructive.gate.decision`, the same audit entry a policy verdict leaves.
10
+ // So a desktop run, a cloud Job and the agentic lane ask and are answered in the
11
+ // one place, and nothing executes on the strength of a click: the decision is
12
+ // what was persisted and read back.
13
+ //
14
+ // `notifyToolSkipped` returns void, so its write is queued and `drain()` —
15
+ // awaited when the turn ends — rethrows the first failure rather than losing it.
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.SKIPPED_REASON = exports.gatePrompt = void 0;
18
+ exports.createRunLogGateHost = createRunLogGateHost;
19
+ const approvals_1 = require("./approvals");
20
+ const gate_decisions_1 = require("./gate-decisions");
21
+ /** The text a human sees: the harness's title, then its message. */
22
+ const gatePrompt = (title, message) => `${title}\n\n${message}`;
23
+ exports.gatePrompt = gatePrompt;
24
+ /** Reason filed for a repeat of a declined call the gate skipped without asking. */
25
+ exports.SKIPPED_REASON = 'skipped: a repeat of a tool call the user already declined';
26
+ function createRunLogGateHost(options) {
27
+ const { onRequested, onDecided, ...channelOptions } = options;
28
+ const { store, runId } = options;
29
+ const now = options.now ?? (() => new Date());
30
+ // Prompts are composed per call and the channel writes one per request, so
31
+ // the title and message travel by tool call id rather than through the
32
+ // channel's request shape.
33
+ const prompts = new Map();
34
+ const channel = (0, approvals_1.runLogApprovals)({
35
+ ...channelOptions,
36
+ prompt: (request) => prompts.get(request.toolCallId) ?? request.toolName
37
+ });
38
+ const decisions = (0, gate_decisions_1.runLogGateDecisions)({
39
+ store,
40
+ runId,
41
+ ...(options.pageLimit === undefined ? {} : { pageLimit: options.pageLimit })
42
+ });
43
+ return {
44
+ hasUI: true,
45
+ async confirmTool(toolCallId, title, message) {
46
+ prompts.set(toolCallId, (0, exports.gatePrompt)(title, message));
47
+ onRequested?.({ toolCallId, title, message });
48
+ try {
49
+ const outcome = await channel.request({
50
+ runId,
51
+ toolCallId,
52
+ toolName: title,
53
+ input: {},
54
+ reason: message,
55
+ requestedAt: now().toISOString()
56
+ });
57
+ decisions.onDecision({
58
+ runId,
59
+ toolCallId,
60
+ toolName: title,
61
+ input: {},
62
+ verdict: { decision: 'ask' },
63
+ decision: outcome.decision,
64
+ ...(outcome.reason === undefined ? {} : { reason: outcome.reason }),
65
+ ...(outcome.actorId === undefined ? {} : { actorId: outcome.actorId }),
66
+ decidedAt: now().toISOString()
67
+ });
68
+ onDecided?.({
69
+ toolCallId,
70
+ allowed: outcome.decision === 'allow',
71
+ ...(outcome.reason === undefined ? {} : { reason: outcome.reason }),
72
+ ...(outcome.actorId === undefined ? {} : { actorId: outcome.actorId })
73
+ });
74
+ return outcome.decision === 'allow';
75
+ }
76
+ finally {
77
+ prompts.delete(toolCallId);
78
+ }
79
+ },
80
+ notifyToolSkipped(toolCallId) {
81
+ // The harness names only the call, and the tool it repeats is in the
82
+ // transcript beside it; the projector attributes the entry by call id.
83
+ decisions.onDecision({
84
+ runId,
85
+ toolCallId,
86
+ toolName: '',
87
+ input: {},
88
+ verdict: { decision: 'deny', reason: exports.SKIPPED_REASON },
89
+ decision: 'deny',
90
+ reason: exports.SKIPPED_REASON,
91
+ decidedAt: now().toISOString()
92
+ });
93
+ },
94
+ drain: () => decisions.flush()
95
+ };
96
+ }
package/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export type { RunLogApprovalsOptions } from './approvals';
2
+ export { approvalEntryId, resolveApproval, runLogApprovals } from './approvals';
3
+ export type { RunLogGateDecisions, RunLogGateDecisionsOptions } from './gate-decisions';
4
+ export { gateDecisionEntryId, runLogGateDecisions } from './gate-decisions';
5
+ export type { RunLogGateDecision, RunLogGateHost, RunLogGateHostOptions, RunLogGateRequest } from './gate-host';
6
+ export { createRunLogGateHost, gatePrompt, SKIPPED_REASON } from './gate-host';
package/index.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SKIPPED_REASON = exports.gatePrompt = exports.createRunLogGateHost = exports.runLogGateDecisions = exports.gateDecisionEntryId = exports.runLogApprovals = exports.resolveApproval = exports.approvalEntryId = void 0;
4
+ var approvals_1 = require("./approvals");
5
+ Object.defineProperty(exports, "approvalEntryId", { enumerable: true, get: function () { return approvals_1.approvalEntryId; } });
6
+ Object.defineProperty(exports, "resolveApproval", { enumerable: true, get: function () { return approvals_1.resolveApproval; } });
7
+ Object.defineProperty(exports, "runLogApprovals", { enumerable: true, get: function () { return approvals_1.runLogApprovals; } });
8
+ var gate_decisions_1 = require("./gate-decisions");
9
+ Object.defineProperty(exports, "gateDecisionEntryId", { enumerable: true, get: function () { return gate_decisions_1.gateDecisionEntryId; } });
10
+ Object.defineProperty(exports, "runLogGateDecisions", { enumerable: true, get: function () { return gate_decisions_1.runLogGateDecisions; } });
11
+ var gate_host_1 = require("./gate-host");
12
+ Object.defineProperty(exports, "createRunLogGateHost", { enumerable: true, get: function () { return gate_host_1.createRunLogGateHost; } });
13
+ Object.defineProperty(exports, "gatePrompt", { enumerable: true, get: function () { return gate_host_1.gatePrompt; } });
14
+ Object.defineProperty(exports, "SKIPPED_REASON", { enumerable: true, get: function () { return gate_host_1.SKIPPED_REASON; } });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@agentic-kit/run-log-gate",
3
+ "version": "0.2.0",
4
+ "author": "Constructive <developers@constructive.io>",
5
+ "description": "Tool approvals over the run log — the request, the human's answer and every settled gate decision are run-log entries, so desktop, cloud pi and agentic hosts ask and are answered on one surface",
6
+ "main": "index.js",
7
+ "module": "esm/index.js",
8
+ "types": "index.d.ts",
9
+ "homepage": "https://github.com/constructive-io/constructive",
10
+ "license": "SEE LICENSE IN LICENSE",
11
+ "publishConfig": {
12
+ "access": "public",
13
+ "directory": "dist"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/constructive-io/constructive"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/constructive-io/constructive/issues"
21
+ },
22
+ "scripts": {
23
+ "clean": "makage clean",
24
+ "prepack": "npm run build",
25
+ "build": "makage build",
26
+ "build:dev": "makage build --dev",
27
+ "lint": "eslint . --fix",
28
+ "test": "jest",
29
+ "test:watch": "jest --watch"
30
+ },
31
+ "keywords": [
32
+ "agentic-kit",
33
+ "pi",
34
+ "coding-agent",
35
+ "run-log",
36
+ "approvals",
37
+ "constructive"
38
+ ],
39
+ "peerDependencies": {
40
+ "@agentic-kit/harness": "^0.15.0",
41
+ "@agentic-kit/run-log": "^0.6.0"
42
+ },
43
+ "gitHead": "2f450e5771ba241885e46cfe47bf7292d64203e7"
44
+ }