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.
- package/LICENSE +21 -86
- package/README.md +36 -13
- package/THIRD_PARTY_NOTICES.md +3 -3
- package/apps/bridge/src/bin/claude-hook.ts +3 -0
- package/apps/bridge/src/bin/codex-policy-hook.ts +3 -0
- package/apps/bridge/src/capabilities/README.md +1 -1
- package/apps/bridge/src/capabilities/skills.ts +25 -6
- package/apps/bridge/src/config/README.md +1 -1
- package/apps/bridge/src/engine/README.md +12 -2
- package/apps/bridge/src/engine/engine-client.ts +2 -0
- package/apps/bridge/src/engine/engine-invocation-protocol.ts +117 -0
- package/apps/bridge/src/engine/engine-protocol.ts +13 -3
- package/apps/bridge/src/engine/invocation-decision-parse.ts +35 -0
- package/apps/bridge/src/engine/invocation-ingest.ts +167 -0
- package/apps/bridge/src/engine/invocation-log-reader.ts +71 -0
- package/apps/bridge/src/engine/invocation-parse.ts +120 -0
- package/apps/bridge/src/engine/invocation-query.ts +53 -0
- package/apps/bridge/src/engine/invocation-scope.ts +39 -0
- package/apps/bridge/src/machine-load/README.md +1 -1
- package/apps/bridge/src/mesh/README.md +1 -1
- package/apps/bridge/src/mesh/catalog.ts +7 -1
- package/apps/bridge/src/mesh/map.ts +7 -2
- package/apps/bridge/src/mesh/observed-claims.ts +8 -3
- package/apps/bridge/src/mesh/runtime-handoff.ts +3 -1
- package/apps/bridge/src/mesh/runtime.ts +6 -2
- package/apps/bridge/src/mesh/scoped-view.ts +7 -6
- package/apps/bridge/src/mesh/store-support.ts +25 -5
- package/apps/bridge/src/mesh/store.ts +9 -6
- package/apps/bridge/src/monitor.ts +7 -0
- package/apps/bridge/src/policy/README.md +1 -1
- package/apps/bridge/src/policy/capability-fingerprint.ts +24 -4
- package/apps/bridge/src/policy/decision-log.ts +23 -1
- package/apps/bridge/src/policy/effective-action.ts +8 -5
- package/apps/bridge/src/reply/README.md +1 -1
- package/apps/bridge/src/session-keys.ts +2 -1
- package/apps/bridge/src/sessions/claude.ts +5 -0
- package/apps/bridge/src/sessions/codex.ts +5 -0
- package/apps/bridge/src/sessions/cursor/README.md +1 -1
- package/apps/bridge/src/sessions/telemetry/README.md +1 -1
- package/apps/mcp/src/connection-center/README.md +1 -1
- package/apps/mcp/src/http-service/README.md +1 -1
- package/apps/mcp/src/mcp-tools/README.md +1 -1
- package/apps/mcp/src/mcp-tools/mesh-resource.ts +5 -2
- package/apps/mcp/src/oauth/README.md +1 -1
- package/cursor-plugin/.cursor-plugin/plugin.json +2 -2
- package/cursor-plugin/LICENSE +21 -86
- package/cursor-plugin/README.md +16 -10
- package/docs/images/iphone-task-route.png +0 -0
- package/package.json +2 -2
- package/packages/protocol/messages/README.md +1 -1
- package/packages/protocol/messages/mesh-invocations.ts +81 -0
- package/packages/protocol/messages/mesh.ts +3 -0
- package/packages/protocol/schema.ts +4 -0
- package/cursor-plugin/.cursor-plugin/marketplace.json +0 -18
package/LICENSE
CHANGED
|
@@ -1,86 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Serhii Ziborov
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
[](https://www.npmjs.com/package/granttap-mcp)
|
|
4
4
|
[](https://github.com/sergii-ziborov/granttap-mcp/actions/workflows/ci.yml)
|
|
5
|
-
[](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
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
|
525
|
-
[
|
|
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
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Third-party notices
|
|
2
2
|
|
|
3
|
-
GrantTap MCP is
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|