granttap-mcp 0.8.16 → 0.8.18

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.
Files changed (54) hide show
  1. package/LICENSE +21 -86
  2. package/README.md +36 -13
  3. package/THIRD_PARTY_NOTICES.md +3 -3
  4. package/apps/bridge/src/bin/claude-hook.ts +3 -0
  5. package/apps/bridge/src/bin/codex-policy-hook.ts +3 -0
  6. package/apps/bridge/src/capabilities/README.md +1 -1
  7. package/apps/bridge/src/capabilities/skills.ts +25 -6
  8. package/apps/bridge/src/config/README.md +1 -1
  9. package/apps/bridge/src/engine/README.md +12 -2
  10. package/apps/bridge/src/engine/engine-client.ts +2 -0
  11. package/apps/bridge/src/engine/engine-invocation-protocol.ts +117 -0
  12. package/apps/bridge/src/engine/engine-protocol.ts +13 -3
  13. package/apps/bridge/src/engine/invocation-decision-parse.ts +35 -0
  14. package/apps/bridge/src/engine/invocation-ingest.ts +167 -0
  15. package/apps/bridge/src/engine/invocation-log-reader.ts +71 -0
  16. package/apps/bridge/src/engine/invocation-parse.ts +120 -0
  17. package/apps/bridge/src/engine/invocation-query.ts +53 -0
  18. package/apps/bridge/src/engine/invocation-scope.ts +39 -0
  19. package/apps/bridge/src/machine-load/README.md +1 -1
  20. package/apps/bridge/src/mesh/README.md +1 -1
  21. package/apps/bridge/src/mesh/catalog.ts +7 -1
  22. package/apps/bridge/src/mesh/map.ts +7 -2
  23. package/apps/bridge/src/mesh/observed-claims.ts +8 -3
  24. package/apps/bridge/src/mesh/runtime-handoff.ts +3 -1
  25. package/apps/bridge/src/mesh/runtime.ts +6 -2
  26. package/apps/bridge/src/mesh/scoped-view.ts +7 -6
  27. package/apps/bridge/src/mesh/store-support.ts +25 -5
  28. package/apps/bridge/src/mesh/store.ts +9 -6
  29. package/apps/bridge/src/monitor.ts +7 -0
  30. package/apps/bridge/src/policy/README.md +1 -1
  31. package/apps/bridge/src/policy/capability-fingerprint.ts +24 -4
  32. package/apps/bridge/src/policy/decision-log.ts +23 -1
  33. package/apps/bridge/src/policy/effective-action.ts +8 -5
  34. package/apps/bridge/src/reply/README.md +1 -1
  35. package/apps/bridge/src/session-keys.ts +2 -1
  36. package/apps/bridge/src/sessions/claude.ts +5 -0
  37. package/apps/bridge/src/sessions/codex.ts +5 -0
  38. package/apps/bridge/src/sessions/cursor/README.md +1 -1
  39. package/apps/bridge/src/sessions/telemetry/README.md +1 -1
  40. package/apps/mcp/src/connection-center/README.md +1 -1
  41. package/apps/mcp/src/http-service/README.md +1 -1
  42. package/apps/mcp/src/mcp-tools/README.md +1 -1
  43. package/apps/mcp/src/mcp-tools/mesh-resource.ts +5 -2
  44. package/apps/mcp/src/oauth/README.md +1 -1
  45. package/cursor-plugin/.cursor-plugin/plugin.json +2 -2
  46. package/cursor-plugin/LICENSE +21 -86
  47. package/cursor-plugin/README.md +16 -10
  48. package/docs/images/iphone-task-route.png +0 -0
  49. package/package.json +2 -2
  50. package/packages/protocol/messages/README.md +1 -1
  51. package/packages/protocol/messages/mesh-invocations.ts +81 -0
  52. package/packages/protocol/messages/mesh.ts +3 -0
  53. package/packages/protocol/schema.ts +4 -0
  54. package/cursor-plugin/.cursor-plugin/marketplace.json +0 -18
package/LICENSE CHANGED
@@ -1,86 +1,21 @@
1
- GrantTap Commercial Source License 1.0
2
-
3
- Copyright (c) 2026 Serhii Ziborov. All rights reserved.
4
-
5
- This license governs the source code and software distributed from the
6
- granttap-mcp repository and package (the "Software"), except for third-party
7
- components that identify their own license terms.
8
-
9
- 1. Definitions
10
-
11
- "GrantTap" means Serhii Ziborov doing business as GrantTap.
12
-
13
- "Authorized Access" means an active paid GrantTap subscription, an official
14
- GrantTap trial, or a separate written commercial agreement with GrantTap.
15
-
16
- "Authorized User" means a person or legal entity covered by Authorized Access.
17
-
18
- 2. Source inspection and contribution
19
-
20
- You may download, inspect, and make local modifications to the source solely to
21
- evaluate the Software, perform security review, or prepare a contribution to
22
- the official GrantTap repository. You may create a public fork solely for that
23
- contribution workflow if the fork retains this license and does not publish a
24
- release, binary, hosted service, or independently usable distribution.
25
-
26
- These rights do not permit production use, commercial use, or use to provide a
27
- service to another person.
28
-
29
- 3. Authorized product use
30
-
31
- During Authorized Access, GrantTap grants the Authorized User a limited,
32
- non-exclusive, non-transferable, non-sublicensable license to install, execute,
33
- and internally modify the Software on devices the Authorized User owns or
34
- controls, solely to use the official GrantTap product and services.
35
-
36
- When Authorized Access ends, the Authorized User must stop using the Software
37
- except where a separate written agreement says otherwise.
38
-
39
- 4. Restrictions
40
-
41
- Unless GrantTap gives prior written permission, you may not:
42
-
43
- - sell, sublicense, redistribute, or publish the Software or a derivative work;
44
- - offer the Software or a derivative work as a hosted or managed service;
45
- - use the Software to build or operate a competing product or service;
46
- - use the Software as a standalone general-purpose coding-agent runtime outside
47
- the official GrantTap product;
48
- - bypass subscription, license, access-control, or entitlement checks;
49
- - remove or alter copyright, license, attribution, or proprietary notices; or
50
- - use GrantTap names or marks to imply endorsement of a modified distribution.
51
-
52
- 5. Ownership
53
-
54
- The Software is licensed, not sold. GrantTap retains all right, title, and
55
- interest in the Software and its derivative works. No trademark, patent, or
56
- other rights are granted except the limited copyright rights stated here.
57
-
58
- 6. Third-party components
59
-
60
- Third-party components remain governed by their respective licenses. Those
61
- licenses and notices are not replaced by this license. See
62
- THIRD_PARTY_NOTICES.md where distributed.
63
-
64
- 7. Termination
65
-
66
- This license terminates automatically if you breach its terms. On termination,
67
- you must stop using and distributing the Software and delete copies that you
68
- are not legally required to retain. Sections 4 through 9 survive termination.
69
-
70
- 8. Disclaimer of warranty
71
-
72
- THE SOFTWARE IS PROVIDED "AS IS" AND "AS AVAILABLE", WITHOUT WARRANTIES OF
73
- ANY KIND, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS
74
- FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT.
75
-
76
- 9. Limitation of liability
77
-
78
- TO THE MAXIMUM EXTENT PERMITTED BY LAW, GRANTTAP AND ITS CONTRIBUTORS WILL NOT
79
- BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR
80
- PUNITIVE DAMAGES, OR FOR LOSS OF DATA, PROFITS, REVENUE, OR BUSINESS, ARISING
81
- FROM OR RELATED TO THE SOFTWARE OR THIS LICENSE.
82
-
83
- 10. Commercial terms
84
-
85
- Authorized Access, pricing, and separate commercial licenses are available
86
- through https://granttap.com.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Serhii Ziborov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/granttap-mcp)](https://www.npmjs.com/package/granttap-mcp)
4
4
  [![CI](https://github.com/sergii-ziborov/granttap-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/sergii-ziborov/granttap-mcp/actions/workflows/ci.yml)
5
- [![License](https://img.shields.io/badge/license-GrantTap%20Commercial-7c3aed.svg)](LICENSE)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
6
 
7
7
  GrantTap is a Personal live control center for local coding agents.
8
8
 
@@ -13,9 +13,9 @@ hooks, local adapters, and TypeScript wire schemas. Agents and provider
13
13
  credentials stay on your computer. Native iPhone and Apple Watch traffic is
14
14
  end-to-end encrypted.
15
15
 
16
- The source is public for inspection and contribution, but GrantTap MCP is
17
- proprietary paid software rather than open source. Production use requires an
18
- active GrantTap subscription or a separate commercial agreement.
16
+ The MCP runtime, plugins, and CLI are open source under the MIT License. The
17
+ iPhone and Apple Watch app is a separate, proprietary product; use of GrantTap's
18
+ hosted service is governed by its own terms.
19
19
 
20
20
  [Website](https://granttap.com) · [npm](https://www.npmjs.com/package/granttap-mcp) ·
21
21
  [Security model](SECURITY.md) ·
@@ -54,15 +54,38 @@ and completion events through the existing `notify` tool, and read state from a
54
54
  Mesh resource. Full transcripts and hidden reasoning are never mesh payloads.
55
55
  Claims have TTLs; a colliding claim is rejected before it is recorded so agents
56
56
  can choose different work or contact the owner before escalating to Needs You.
57
+ Observed edit-tool requests are intent claims, not confirmed writes. Resource
58
+ identity includes the repository; equal relative paths from separate repositories
59
+ do not conflict, and another checkout is a coordination warning. The path
60
+ matcher respects segment boundaries, so `src/auth.ts` and `src/auth.ts.bak`
61
+ are distinct. A live repository reading refreshes HEAD after commit or checkout.
62
+
63
+ When a separately distributed, verified GrantTap Engine is enabled on the
64
+ computer, the bridge sends content-free Invocation events before its small Usage
65
+ buffer is trimmed. Claude Code, Codex, and Cursor transcript scanners record
66
+ native call identity, request, reported outcome, resource path, and explicit
67
+ source gaps. The Engine owns the durable journal and bounded cursor history;
68
+ the phone requests the latest or older page under the Project E2EE key. A
69
+ reported successful tool call alone does not prove a filesystem change, and
70
+ unavailable Engine history is reported as unavailable.
71
+ The bridge does not yet produce verified filesystem-change events or attach
72
+ revision-bound impact/consumer links to individual Invocations. It leaves that
73
+ evidence unknown instead of treating an edit request or reported success as a
74
+ confirmed content change.
75
+ Claude Code and Codex denials with native call IDs are recorded as separate,
76
+ metadata-only hook events with the applied rule and policy revision; an
77
+ unidentified Cursor decision is not assigned to an invented call. Script and
78
+ `SKILL.md` fingerprints include a content hash when the local file is readable,
79
+ separate from their path identity. Missing content evidence remains unknown.
57
80
 
58
81
  <p align="center">
59
82
  <img src="docs/images/iphone-projects-shared.png" width="200" alt="Projects: one owned by this phone, one shared by another">
60
83
  <img src="docs/images/iphone-project-mesh.png" width="200" alt="A Project: Governance, members and computers, Mesh status, repositories, Tasks">
61
- <img src="docs/images/iphone-task-route.png" width="200" alt="A Task: its executions, its claims, and who else is in its files">
84
+ <img src="docs/images/iphone-task-route.png" width="200" alt="A Task: executions, Runtime history, and resource claims">
62
85
  <img src="docs/images/iphone-handoff.png" width="200" alt="Task handoff: destination, push, and readiness checks">
63
86
  </p>
64
87
 
65
- <p align="center"><em>Projects, one of them shared by another phone · a Project with its Governance, members, computers, repositories, and Tasks · a Task with its executions and claims · a handoff with its readiness checks</em></p>
88
+ <p align="center"><em>Projects, one of them shared by another phone · a Project with its Governance, members, computers, repositories, and Tasks · a Task with executions, Runtime history, and claims · a handoff with its readiness checks</em></p>
66
89
 
67
90
  For Claude Code, Codex, and Cursor, the provider hook runs inside the agent's
68
91
  own session and sees the exact call. GrantTap attributes every `notify` to the
@@ -204,8 +227,8 @@ history stays on the device.
204
227
 
205
228
  ## Install
206
229
 
207
- Installation and production use require Authorized Access under the
208
- [GrantTap Commercial Source License](LICENSE).
230
+ The local runtime and plugins are available under the [MIT License](LICENSE).
231
+ The iPhone and Apple Watch app and hosted service have separate terms.
209
232
 
210
233
  Install the GrantTap plugin directly from this repository's marketplace:
211
234
 
@@ -223,8 +246,9 @@ grok plugin marketplace add sergii-ziborov/granttap-mcp
223
246
  grok plugin install granttap --trust
224
247
  ```
225
248
 
226
- Import `sergii-ziborov/granttap-mcp` as a private Git plugin in Cursor. A public
227
- listing requires an open-source plugin license and Cursor review. Reload Cursor, then open
249
+ For Cursor, use the reviewed **GrantTap** Marketplace listing when it is visible
250
+ to your account, or import the public `sergii-ziborov/granttap-mcp` repository
251
+ once. Reload Cursor, then open
228
252
  **Cursor Customize → MCPs → GrantTap → Authenticate**. Complete authorization
229
253
  and any one-time QR pairing on `https://granttap.com/connect`.
230
254
 
@@ -521,9 +545,8 @@ typecheck, and release checks pass.
521
545
 
522
546
  ## License
523
547
 
524
- GrantTap MCP is distributed under the proprietary
525
- [GrantTap Commercial Source License 1.0](LICENSE). Public source access does not
526
- grant open-source, redistribution, hosted-service, or competing-product rights.
548
+ GrantTap MCP, its CLI, and bundled plugins are distributed under the
549
+ [MIT License](LICENSE). The iPhone and Apple Watch app is licensed separately.
527
550
  Third-party dependencies retain their own terms; see
528
551
  [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
529
552
 
@@ -1,8 +1,8 @@
1
1
  # Third-party notices
2
2
 
3
- GrantTap MCP is proprietary software distributed under the GrantTap Commercial
4
- Source License. Third-party components retain their own licenses; the GrantTap
5
- license does not replace or restrict those terms.
3
+ GrantTap MCP is distributed under the MIT License. Third-party components
4
+ retain their own licenses; the GrantTap license does not replace or restrict
5
+ those terms.
6
6
 
7
7
  The npm package declares, rather than vendors, these direct runtime
8
8
  dependencies. Their packages carry the applicable license texts:
@@ -88,6 +88,9 @@ async function handlePreToolUse(input: HookInput): Promise<void> {
88
88
  recordProjectDecision(input.session_id, {
89
89
  at: Date.now(), toolName: input.tool_name ?? "tool", reason: projectDecision.reason,
90
90
  ruleId: projectDecision.rule_id,
91
+ provider: "claude", nativeCallId: input.tool_use_id,
92
+ policyRevision: projectDecision.policy_revision,
93
+ artifactHash: projectDecision.artifactHash,
91
94
  });
92
95
  }
93
96
  writePermission("deny", projectDecision.reason);
@@ -43,6 +43,9 @@ async function main(): Promise<void> {
43
43
  if (input.session_id) {
44
44
  recordProjectDecision(input.session_id, {
45
45
  at: Date.now(), toolName: input.tool_name ?? "tool", reason: decision.reason, ruleId: decision.rule_id,
46
+ provider: "codex", nativeCallId: input.tool_use_id,
47
+ policyRevision: decision.policy_revision,
48
+ artifactHash: decision.artifactHash,
46
49
  });
47
50
  }
48
51
  deny(decision.reason);
@@ -2,4 +2,4 @@
2
2
 
3
3
  This module discovers provider-configured MCP servers and available skills. Metadata probing is isolated because it may start an MCP transport; local descriptors and skill discovery remain read-only.
4
4
 
5
- License: this module is distributed under the GrantTap Commercial Source License in the repository-root `LICENSE` file.
5
+ License: this module is distributed under the MIT License in the repository-root `LICENSE` file.
@@ -7,6 +7,30 @@ import { ancestors } from "./descriptors";
7
7
  /** Skills available globally and along the task's repository path. */
8
8
  export function workspaceSkills(cwd: string | undefined): SkillInfo[] {
9
9
  const found = new Map<string, SkillInfo>();
10
+ for (const root of skillRoots(cwd)) {
11
+ for (const skill of skillsIn(root)) {
12
+ if (!found.has(skill.name)) found.set(skill.name, skill);
13
+ }
14
+ }
15
+ return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
16
+ }
17
+
18
+ /** The source file behind a named skill, in the same precedence as the catalog. */
19
+ export function skillDefinitionPath(name: string, cwd: string | undefined): string | undefined {
20
+ for (const root of skillRoots(cwd)) {
21
+ let entries: string[];
22
+ try { entries = readdirSync(root); }
23
+ catch { continue; }
24
+ for (const entry of entries) {
25
+ if (entry.startsWith(".")) continue;
26
+ const path = join(root, entry, "SKILL.md");
27
+ if (frontmatter(path)?.name === name) return path;
28
+ }
29
+ }
30
+ return undefined;
31
+ }
32
+
33
+ function skillRoots(cwd: string | undefined): string[] {
10
34
  const roots = [
11
35
  join(homedir(), ".cursor", "skills-cursor"),
12
36
  join(homedir(), ".agents", "skills"),
@@ -21,12 +45,7 @@ export function workspaceSkills(cwd: string | undefined): SkillInfo[] {
21
45
  );
22
46
  }
23
47
  }
24
- for (const root of roots) {
25
- for (const skill of skillsIn(root)) {
26
- if (!found.has(skill.name)) found.set(skill.name, skill);
27
- }
28
- }
29
- return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
48
+ return roots;
30
49
  }
31
50
 
32
51
  function skillsIn(root: string): SkillInfo[] {
@@ -6,5 +6,5 @@ and device pairing serialization. `../config.ts` is its public entry point.
6
6
  Behavior is covered by `tests/config.test.ts`, `tests/capabilities.test.ts`, and
7
7
  `tests/core.test.ts`.
8
8
 
9
- License: this module is distributed under the GrantTap Commercial Source License
9
+ License: this module is distributed under the MIT License
10
10
  in the repository-root `LICENSE` file.
@@ -17,9 +17,19 @@ bypass, paused gating, and auto-accept. Codex carries ASK from `PreToolUse` to
17
17
  `PermissionRequest` with a single-use exact-call marker; Cursor evaluates shell
18
18
  and redacted MCP fingerprints directly. A disabled or unavailable engine
19
19
  preserves the established GrantTap flow, and no hook request is retried inside
20
- its 50 ms policy budget.
20
+ its 2,000 ms policy budget. Repository impact is not computed synchronously in
21
+ the hook; unavailable impact remains explicitly unavailable.
22
+
23
+ `invocation-ingest.ts` reads bounded complete transcript lines and exact-call
24
+ hook denial metadata without forwarding tool arguments, output, or reason text.
25
+ Cursor decisions without a native call ID stay unattributed. Events are replayed
26
+ with stable IDs after an IPC failure. `invocation-query.ts` returns bounded
27
+ Engine history to the phone under the Project key, while
28
+ `invocation-scope.ts` exposes one Task slice through the caller's existing
29
+ opaque Mesh capability. The Engine, not this bridge or Usage telemetry, owns
30
+ the durable journal.
21
31
 
22
32
  The proprietary engine source and Project database do not live in this package.
23
33
 
24
- License: this module is distributed under the GrantTap Commercial Source License
34
+ License: this module is distributed under the MIT License
25
35
  in the repository-root `LICENSE` file.
@@ -196,6 +196,8 @@ function expectedResultOperation(
196
196
  if (operation === "project.get") return "project.found";
197
197
  if (operation === "project.list_bindings") return "project.bindings";
198
198
  if (operation === "project.upsert_binding") return "project.binding_upserted";
199
+ if (operation === "invocation.observe") return "invocation.observed";
200
+ if (operation === "invocation.history") return "invocation.history";
199
201
  if (operation === "policy.get") return "policy.found";
200
202
  if (operation === "policy.apply") return "policy.applied";
201
203
  if (operation === "policy.coverage") return "policy.coverage";
@@ -0,0 +1,117 @@
1
+ /** The Engine's content-free, durable per-call evidence wire contract. */
2
+ export type InvocationPhase =
3
+ | "requested" | "reported_success" | "reported_failure" | "reported_unknown"
4
+ | "denied" | "change_observed" | "source_gap";
5
+ export type InvocationSource = "transcript" | "hook" | "filesystem" | "scanner";
6
+
7
+ export type InvocationEvent = {
8
+ event_id: string;
9
+ invocation_id: string;
10
+ project_id: string;
11
+ task_id: string;
12
+ execution_id: string;
13
+ provider: string;
14
+ native_call_id: string;
15
+ session_id?: string | null;
16
+ tool_name: string;
17
+ phase: InvocationPhase;
18
+ source: InvocationSource;
19
+ occurred_at: number;
20
+ repository_id?: string | null;
21
+ worktree?: string | null;
22
+ resource?: string | null;
23
+ revision?: string | null;
24
+ content_hash?: string | null;
25
+ capability_artifact_hash?: string | null;
26
+ policy_revision?: number | null;
27
+ policy_rule_id?: string | null;
28
+ };
29
+
30
+ export type InvocationHistoryQuery = {
31
+ project_id: string;
32
+ task_id?: string | null;
33
+ after_sequence?: number | null;
34
+ limit?: number | null;
35
+ tail?: boolean;
36
+ before_sequence?: number | null;
37
+ };
38
+
39
+ export type SequencedInvocationEvent = { sequence: number; event: InvocationEvent };
40
+ export type InvocationHistoryPage = {
41
+ events: SequencedInvocationEvent[];
42
+ next_sequence: number;
43
+ has_more: boolean;
44
+ previous_sequence: number;
45
+ has_older: boolean;
46
+ };
47
+
48
+ export type InvocationEngineOperation =
49
+ | { operation: "invocation.observe"; input: InvocationEvent }
50
+ | { operation: "invocation.history"; input: InvocationHistoryQuery };
51
+ export type InvocationEngineResult =
52
+ | { operation: "invocation.observed"; sequence: number }
53
+ | { operation: "invocation.history"; page: InvocationHistoryPage };
54
+
55
+ const phases = new Set<InvocationPhase>([
56
+ "requested", "reported_success", "reported_failure", "reported_unknown",
57
+ "denied", "change_observed", "source_gap",
58
+ ]);
59
+ const sources = new Set<InvocationSource>(["transcript", "hook", "filesystem", "scanner"]);
60
+
61
+ export function parseInvocationResult(result: Record<string, unknown>, invalid: () => never): boolean {
62
+ if (result.operation === "invocation.observed") {
63
+ if (!positiveInteger(result.sequence)) invalid();
64
+ return true;
65
+ }
66
+ if (result.operation !== "invocation.history") return false;
67
+ const page = record(result.page, invalid);
68
+ if (!Array.isArray(page.events) || page.events.length > 32
69
+ || !safeInteger(page.next_sequence) || typeof page.has_more !== "boolean"
70
+ || !safeInteger(page.previous_sequence) || typeof page.has_older !== "boolean") invalid();
71
+ let prior = 0;
72
+ for (const raw of page.events as unknown[]) {
73
+ const row = record(raw, invalid);
74
+ if (!positiveInteger(row.sequence) || row.sequence <= prior || row.sequence > page.next_sequence) invalid();
75
+ prior = row.sequence;
76
+ parseInvocationEvent(row.event, invalid);
77
+ }
78
+ return true;
79
+ }
80
+
81
+ function parseInvocationEvent(value: unknown, invalid: () => never): void {
82
+ const event = record(value, invalid);
83
+ for (const [key, max] of [
84
+ ["event_id", 128], ["invocation_id", 128], ["project_id", 128], ["task_id", 128],
85
+ ["execution_id", 128], ["provider", 32], ["native_call_id", 128], ["tool_name", 160],
86
+ ] as const) boundedString(event[key], max, invalid);
87
+ if (!phases.has(event.phase as InvocationPhase) || !sources.has(event.source as InvocationSource)
88
+ || !safeInteger(event.occurred_at)) invalid();
89
+ for (const key of ["repository_id", "worktree", "resource", "revision"] as const) {
90
+ if (event[key] != null) boundedString(event[key], 512, invalid);
91
+ }
92
+ if (event.session_id != null) boundedString(event.session_id, 128, invalid);
93
+ for (const key of ["content_hash", "capability_artifact_hash"] as const) {
94
+ if (event[key] != null && (typeof event[key] !== "string" || !/^[a-f0-9]{64}$/i.test(event[key]))) invalid();
95
+ }
96
+ if (event.policy_revision != null && !safeInteger(event.policy_revision)) invalid();
97
+ if (event.policy_rule_id != null) boundedString(event.policy_rule_id, 128, invalid);
98
+ if (event.phase === "change_observed" && (event.source !== "filesystem"
99
+ || !event.repository_id || !event.resource || !event.revision || !event.content_hash)) invalid();
100
+ }
101
+
102
+ function record(value: unknown, invalid: () => never): Record<string, unknown> {
103
+ if (!value || typeof value !== "object" || Array.isArray(value)) invalid();
104
+ return value as Record<string, unknown>;
105
+ }
106
+
107
+ function safeInteger(value: unknown): value is number {
108
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
109
+ }
110
+
111
+ function positiveInteger(value: unknown): value is number {
112
+ return safeInteger(value) && value > 0;
113
+ }
114
+
115
+ function boundedString(value: unknown, max: number, invalid: () => never): void {
116
+ if (typeof value !== "string" || value.trim() === "" || value.length > max || /[\u0000-\u001f]/.test(value)) invalid();
117
+ }
@@ -3,6 +3,15 @@ import {
3
3
  type EnginePolicyOperation,
4
4
  type EnginePolicyResult,
5
5
  } from "./engine-policy-protocol";
6
+ import {
7
+ parseInvocationResult,
8
+ type InvocationEngineOperation,
9
+ type InvocationEngineResult,
10
+ } from "./engine-invocation-protocol";
11
+
12
+ export type {
13
+ InvocationEvent, InvocationHistoryPage, InvocationHistoryQuery, InvocationPhase, InvocationSource,
14
+ } from "./engine-invocation-protocol";
6
15
 
7
16
  export const ENGINE_PROTOCOL_VERSION = 1 as const;
8
17
  export const MAX_ENGINE_FRAME_BYTES = 64 * 1024;
@@ -53,7 +62,7 @@ export type EngineProjectBinding = {
53
62
  last_seen_at: number;
54
63
  };
55
64
 
56
- export type EngineOperation = EnginePolicyOperation
65
+ export type EngineOperation = EnginePolicyOperation | InvocationEngineOperation
57
66
  | { operation: "engine.ping" }
58
67
  | { operation: "engine.version" }
59
68
  | {
@@ -77,7 +86,7 @@ export type EngineRequest = {
77
86
  request_id: string;
78
87
  } & EngineOperation;
79
88
 
80
- export type EngineResult = EnginePolicyResult
89
+ export type EngineResult = EnginePolicyResult | InvocationEngineResult
81
90
  | { operation: "engine.pong"; engine_version: string }
82
91
  | {
83
92
  operation: "engine.version";
@@ -202,7 +211,8 @@ function parseResult(value: unknown): EngineResult {
202
211
  result.bindings.forEach(parseBinding);
203
212
  } else if (operation === "project.binding_upserted") {
204
213
  parseBinding(result.binding);
205
- } else if (!parsePolicyResult(result, invalidResult)) {
214
+ } else if (!parsePolicyResult(result, invalidResult)
215
+ && !parseInvocationResult(result, invalidResult)) {
206
216
  throw new EngineProtocolError("engine result operation is unsupported");
207
217
  }
208
218
  return result as EngineResult;
@@ -0,0 +1,35 @@
1
+ import { sourceGapFact, type InvocationFact } from "./invocation-parse";
2
+
3
+ /** Metadata-only hook refusal. No arguments, outputs, or explanation text. */
4
+ export function parseInvocationDecision(
5
+ line: string, provider: "claude" | "codex" | "cursor", offset: number,
6
+ ): InvocationFact[] {
7
+ let value: unknown;
8
+ try { value = JSON.parse(line) as unknown; }
9
+ catch { return [sourceGapFact(offset)]; }
10
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
11
+ return [sourceGapFact(offset)];
12
+ }
13
+ const record = value as Record<string, unknown>;
14
+ if (record.provider !== provider) return [];
15
+ if (typeof record.nativeCallId !== "string" || !record.nativeCallId.trim()
16
+ || record.nativeCallId.length > 128 || /[\u0000-\u001f]/.test(record.nativeCallId)
17
+ || typeof record.toolName !== "string" || !record.toolName.trim()
18
+ || record.toolName.length > 160 || /[\u0000-\u001f]/.test(record.toolName)
19
+ || typeof record.at !== "number" || !Number.isSafeInteger(record.at) || record.at < 0) {
20
+ return [sourceGapFact(offset)];
21
+ }
22
+ const revision = record.policyRevision;
23
+ const rule = record.ruleId;
24
+ const artifact = record.artifactHash;
25
+ return [{
26
+ callId: record.nativeCallId, toolName: record.toolName, phase: "denied",
27
+ source: "hook", occurredAt: record.at,
28
+ ...(typeof revision === "number" && Number.isSafeInteger(revision) && revision >= 0
29
+ ? { policyRevision: revision } : {}),
30
+ ...(typeof rule === "string" && rule.trim() && rule.length <= 128
31
+ && !/[\u0000-\u001f]/.test(rule) ? { policyRuleId: rule } : {}),
32
+ ...(typeof artifact === "string" && /^[a-f0-9]{64}$/i.test(artifact)
33
+ ? { capabilityArtifactHash: artifact.toLowerCase() } : {}),
34
+ }];
35
+ }