@pinta-ai/pinta-copilot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/LICENSE +136 -0
- package/README.md +116 -0
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.js +19 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/env-bridge.d.ts +25 -0
- package/dist/core/env-bridge.js +43 -0
- package/dist/core/env-bridge.js.map +1 -0
- package/dist/core/guard.d.ts +14 -0
- package/dist/core/guard.js +48 -0
- package/dist/core/guard.js.map +1 -0
- package/dist/core/otlp.d.ts +55 -0
- package/dist/core/otlp.js +184 -0
- package/dist/core/otlp.js.map +1 -0
- package/dist/core/redact.d.ts +57 -0
- package/dist/core/redact.js +149 -0
- package/dist/core/redact.js.map +1 -0
- package/dist/core/retry-queue.d.ts +26 -0
- package/dist/core/retry-queue.js +127 -0
- package/dist/core/retry-queue.js.map +1 -0
- package/dist/core/surface.d.ts +21 -0
- package/dist/core/surface.js +13 -0
- package/dist/core/surface.js.map +1 -0
- package/dist/core/trace.d.ts +20 -0
- package/dist/core/trace.js +80 -0
- package/dist/core/trace.js.map +1 -0
- package/dist/core/transport.d.ts +18 -0
- package/dist/core/transport.js +127 -0
- package/dist/core/transport.js.map +1 -0
- package/dist/core/types.d.ts +49 -0
- package/dist/core/types.js +124 -0
- package/dist/core/types.js.map +1 -0
- package/dist/env-file.d.ts +4 -0
- package/dist/env-file.js +69 -0
- package/dist/env-file.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +71 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/doctor.d.ts +1 -0
- package/dist/tools/doctor.js +81 -0
- package/dist/tools/doctor.js.map +1 -0
- package/dist/tools/install-hooks.d.ts +1 -0
- package/dist/tools/install-hooks.js +101 -0
- package/dist/tools/install-hooks.js.map +1 -0
- package/package.json +38 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to pinta-copilot are documented here.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-06-09
|
|
6
|
+
|
|
7
|
+
Initial release. Forked from `pinta-cc` (Claude Code adapter); core layer
|
|
8
|
+
(otlp/transport/retry-queue/redact/guard) reused. Verified against real
|
|
9
|
+
GitHub Copilot CLI 1.0.49 + VS Code extension and a live Pinta Manager guard.
|
|
10
|
+
See [`DESIGNDOC.md`](./DESIGNDOC.md) for the full design.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **GitHub Copilot adapter** covering two surfaces from one hook file —
|
|
15
|
+
Copilot CLI and the VS Code extension (in-editor Copilot Chat). A single
|
|
16
|
+
`~/.copilot/hooks/pinta-copilot.json` fires on both; **no VS Code setting
|
|
17
|
+
required** (`chat.useClaudeHooks` default `false` works).
|
|
18
|
+
- **3-way event discriminator + env fallback** — resolves the hook name from
|
|
19
|
+
`hook_event_name` / `hookEventName` / `hookName`, then `PINTA_COPILOT_EVENT`
|
|
20
|
+
(CLI `permissionRequest` uses a camelCase `hookName` schema; CLI
|
|
21
|
+
`subagentStart` ships no event-name field at all, so `install-hooks` stamps
|
|
22
|
+
the event name into each hook entry's `env`).
|
|
23
|
+
- **Internal tools are telemetry-only** — `report_intent` / `ask_user` are
|
|
24
|
+
never guarded (denying them would brick the turn); `PINTA_GUARD_TIMEOUT_MS`
|
|
25
|
+
makes the guard client timeout configurable (default 50ms).
|
|
26
|
+
- **`doctor`** — read-only health check (hook file, env, endpoint, surface).
|
|
27
|
+
- **`DESIGNDOC.md`** — the as-built design document.
|
|
28
|
+
- **Surface detection** (`copilot.surface` = `cli` | `ext` | `cloud`) via
|
|
29
|
+
`ELECTRON_RUN_AS_NODE` / `VSCODE_*` / `COPILOT_AGENT_*` — deliberately not
|
|
30
|
+
`TERM_PROGRAM` (integrated-terminal CLI would misclassify).
|
|
31
|
+
- **Dual guard path** — `preToolUse` (all surfaces) + `permissionRequest`
|
|
32
|
+
(CLI only); deny is emitted in each event's expected format with reason.
|
|
33
|
+
- **Always exit 0** — Copilot CLI `preToolUse` is fail-closed, so adapter
|
|
34
|
+
crashes must never block tools / brick the agent.
|
|
35
|
+
- **Bronze flattening** into `copilot.*`, `ingest.type="copilot"`; both CLI and
|
|
36
|
+
ext payload shapes pass through losslessly.
|
|
37
|
+
- **Config via env file** `~/.copilot/pinta-copilot.env` (unset-only load).
|
|
38
|
+
- **Per-turn ULID trace keyed by `session_id`** (concurrent CLI + ext safe).
|
|
39
|
+
- `tools/install-hooks` — writes/removes the user-level hook file with absolute
|
|
40
|
+
paths.
|
|
41
|
+
|
|
42
|
+
### Changed from pinta-cc
|
|
43
|
+
|
|
44
|
+
- Span prefix `cc.*` → `copilot.*`; `ingest.type` `cc` → `copilot`;
|
|
45
|
+
`service.name` `claude-code` → `copilot`.
|
|
46
|
+
- Plugin/marketplace channel removed (direct hook-file install only).
|
|
47
|
+
- Handlers consolidated into `index.ts`; config data dir anchored at
|
|
48
|
+
`~/.copilot/pinta-copilot-data` (cwd-independent).
|
|
49
|
+
|
|
50
|
+
### Removed (pinta-cc / Claude residue)
|
|
51
|
+
|
|
52
|
+
- `env-bridge.ts` (`CLAUDE_PLUGIN_OPTION_*` → OTel bridge) — plugin-channel only,
|
|
53
|
+
unused for direct install. Config now comes from the env file via
|
|
54
|
+
**namespaced vars** `COPILOT_PLUGIN_OPTION_ENDPOINT` / `COPILOT_PLUGIN_OPTION_HEADERS`
|
|
55
|
+
(so they don't collide with Copilot's native OTel `OTEL_EXPORTER_OTLP_*`, which
|
|
56
|
+
remain a lower-priority fallback); the guard relay token is `PINTA_RELAY_TOKEN`.
|
|
57
|
+
- Dead `hasOtlpEndpoint()`, the `CLAUDE_PLUGIN_DATA` fallback, the unused
|
|
58
|
+
`identity.ts` stub, and stale `[pinta-cc]` log branding / Claude-referencing
|
|
59
|
+
comments.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# PolyForm Noncommercial License 1.0.0
|
|
2
|
+
|
|
3
|
+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
4
|
+
|
|
5
|
+
## Acceptance
|
|
6
|
+
|
|
7
|
+
In order to get any license under these terms, you must agree
|
|
8
|
+
to them as both strict obligations and conditions to all
|
|
9
|
+
your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the
|
|
14
|
+
software to do everything you might do with the software
|
|
15
|
+
that would otherwise infringe the licensor's copyright
|
|
16
|
+
in it for any permitted purpose. However, you may
|
|
17
|
+
only distribute the software according to [Distribution
|
|
18
|
+
License](#distribution-license) and make changes or new works
|
|
19
|
+
based on the software according to [Changes and New Works
|
|
20
|
+
License](#changes-and-new-works-license).
|
|
21
|
+
|
|
22
|
+
## Distribution License
|
|
23
|
+
|
|
24
|
+
The licensor grants you an additional copyright license to
|
|
25
|
+
distribute copies of the software. Your license to distribute
|
|
26
|
+
covers distributing the software with changes and new works
|
|
27
|
+
permitted by [Changes and New Works
|
|
28
|
+
License](#changes-and-new-works-license).
|
|
29
|
+
|
|
30
|
+
## Notices
|
|
31
|
+
|
|
32
|
+
You must ensure that anyone who gets a copy of any part of
|
|
33
|
+
the software from you also gets a copy of these terms or the
|
|
34
|
+
URL for them above, as well as copies of any plain-text lines
|
|
35
|
+
beginning with `Required Notice:` that the licensor provided
|
|
36
|
+
with the software. For example:
|
|
37
|
+
|
|
38
|
+
> Required Notice: Copyright Pinta AI (https://pinta.sh)
|
|
39
|
+
|
|
40
|
+
## Changes and New Works License
|
|
41
|
+
|
|
42
|
+
The licensor grants you an additional copyright license to make
|
|
43
|
+
changes and new works based on the software for any permitted
|
|
44
|
+
purpose.
|
|
45
|
+
|
|
46
|
+
## Patent License
|
|
47
|
+
|
|
48
|
+
The licensor grants you a patent license for the software that
|
|
49
|
+
covers patent claims the licensor can license, or becomes able
|
|
50
|
+
to license, that you would infringe by using the software.
|
|
51
|
+
|
|
52
|
+
## Noncommercial Purposes
|
|
53
|
+
|
|
54
|
+
Any noncommercial purpose is a permitted purpose.
|
|
55
|
+
|
|
56
|
+
## Personal Uses
|
|
57
|
+
|
|
58
|
+
Personal use for research, experiment, and testing for
|
|
59
|
+
the benefit of public knowledge, personal study, private
|
|
60
|
+
entertainment, hobby projects, amateur pursuits, or religious
|
|
61
|
+
observance, without any anticipated commercial application,
|
|
62
|
+
is use for a permitted purpose.
|
|
63
|
+
|
|
64
|
+
## Noncommercial Organizations
|
|
65
|
+
|
|
66
|
+
Use by any charitable organization, educational institution,
|
|
67
|
+
public research organization, public safety or health
|
|
68
|
+
organization, environmental protection organization, or
|
|
69
|
+
government institution is use for a permitted purpose
|
|
70
|
+
regardless of the source of funding or obligations resulting
|
|
71
|
+
from the funding.
|
|
72
|
+
|
|
73
|
+
## Fair Use
|
|
74
|
+
|
|
75
|
+
You may have "fair use" rights for the software under the
|
|
76
|
+
law. These terms do not limit them.
|
|
77
|
+
|
|
78
|
+
## No Other Rights
|
|
79
|
+
|
|
80
|
+
These terms do not allow you to sublicense or transfer any of
|
|
81
|
+
your licenses to anyone else, or prevent the licensor from
|
|
82
|
+
granting licenses to anyone else. These terms do not imply
|
|
83
|
+
any other licenses.
|
|
84
|
+
|
|
85
|
+
## Patent Defense
|
|
86
|
+
|
|
87
|
+
If you make any written claim that the software infringes or
|
|
88
|
+
contributes to infringement of any patent, your patent license
|
|
89
|
+
for the software granted under these terms ends immediately. If
|
|
90
|
+
your company makes such a claim, your patent license ends
|
|
91
|
+
immediately for work on behalf of your company.
|
|
92
|
+
|
|
93
|
+
## Violations
|
|
94
|
+
|
|
95
|
+
The first time you are notified in writing that you have
|
|
96
|
+
violated any of these terms, or done anything with the software
|
|
97
|
+
not covered by your licenses, your licenses can nonetheless
|
|
98
|
+
continue if you come into full compliance with these terms,
|
|
99
|
+
and take practical steps to correct past violations, within
|
|
100
|
+
32 days of receiving notice. Otherwise, all your licenses
|
|
101
|
+
end immediately.
|
|
102
|
+
|
|
103
|
+
## No Liability
|
|
104
|
+
|
|
105
|
+
***As far as the law allows, the software comes as is, without
|
|
106
|
+
any warranty or condition, and the licensor will not be liable
|
|
107
|
+
to you for any damages arising out of these terms or the use
|
|
108
|
+
or nature of the software, under any kind of legal claim.***
|
|
109
|
+
|
|
110
|
+
## Definitions
|
|
111
|
+
|
|
112
|
+
The **licensor** is the individual or entity offering these
|
|
113
|
+
terms, and the **software** is the software the licensor makes
|
|
114
|
+
available under these terms.
|
|
115
|
+
|
|
116
|
+
**You** refers to the individual or entity agreeing to these
|
|
117
|
+
terms.
|
|
118
|
+
|
|
119
|
+
**Your company** is any legal entity, sole proprietorship,
|
|
120
|
+
or other kind of organization that you work for, plus all
|
|
121
|
+
organizations that have control over, are under the control
|
|
122
|
+
of, or are under common control with that organization.
|
|
123
|
+
**Control** means ownership of substantially all the assets of
|
|
124
|
+
an entity, or the power to direct its management and policies
|
|
125
|
+
by vote, contract, or otherwise. Control can be direct or
|
|
126
|
+
indirect.
|
|
127
|
+
|
|
128
|
+
**Your licenses** are all the licenses granted to you for the
|
|
129
|
+
software under these terms.
|
|
130
|
+
|
|
131
|
+
**Use** means anything you do with the software requiring one
|
|
132
|
+
of your licenses.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
Required Notice: Copyright (c) 2026 Pinta AI
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# pinta-copilot — OTLP forwarder + guard for GitHub Copilot hooks
|
|
2
|
+
|
|
3
|
+
Converts **GitHub Copilot** hook events into OTLP/HTTP spans and forwards them to any OpenTelemetry-compatible collector, with an optional external **guard** that can allow/deny tool calls. Vendor-neutral. No Pinta CLI dependency. Identity is attached at the relay layer.
|
|
4
|
+
|
|
5
|
+
A **single adapter + a single hook file** covers two surfaces:
|
|
6
|
+
|
|
7
|
+
| Surface | Hook source | Guard | Notes |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| **Copilot CLI** | `~/.copilot/hooks/pinta-copilot.json` | `preToolUse` **+** `permissionRequest` | `preToolUse` is **fail-closed** |
|
|
10
|
+
| **VS Code extension** (in-editor Copilot Chat) | same `~/.copilot/hooks/` file | `preToolUse` | `preToolUse` is fail-open |
|
|
11
|
+
|
|
12
|
+
> Cloud agent (`.github/hooks/`) is out of scope for now.
|
|
13
|
+
|
|
14
|
+
## Why it works with no VS Code setup
|
|
15
|
+
|
|
16
|
+
The VS Code Copilot extension reads the **same** `~/.copilot/hooks/` file the CLI does (VS Code core `DEFAULT_HOOK_FILE_PATHS`). **No VS Code setting is required** — in particular `chat.useClaudeHooks` works at its default `false`. Install the one file and both the CLI and in-editor Copilot Chat fire it. (Verified against Copilot CLI 1.0.49 + VS Code, 2026-06.)
|
|
17
|
+
|
|
18
|
+
## ⚠️ Fail-closed safety
|
|
19
|
+
|
|
20
|
+
Copilot's **CLI `preToolUse` hook is fail-closed**: a non-zero exit, crash, or timeout *denies* the tool — and a crashing hook blocks `report_intent`/`ask_user` too, bricking the whole agent turn. This adapter therefore **always exits 0** on every path; transport and guard failures are absorbed (telemetry fail-open). Do not patch in code paths that can throw past the top-level handler.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/pinta-ai/pinta-copilot.git
|
|
26
|
+
cd pinta-copilot
|
|
27
|
+
npm install && npm run build
|
|
28
|
+
npm run install-hooks # writes ~/.copilot/hooks/pinta-copilot.json (absolute paths)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Restart the Copilot CLI / reload the VS Code window to load hooks. Remove with `npm run uninstall-hooks`.
|
|
32
|
+
|
|
33
|
+
> Managed installs (Pinta Manager) write the same file via the sidecar enroll module — no manual step.
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
Config is read from an **env file** the adapter loads at startup — `~/.copilot/pinta-copilot.env` (or `$COPILOT_HOME/pinta-copilot.env`), `KEY=VALUE` per line. Explicit `process.env` (incl. a hook `env` block) overrides the file; the file overrides legacy keys.
|
|
38
|
+
|
|
39
|
+
```env
|
|
40
|
+
# ~/.copilot/pinta-copilot.env
|
|
41
|
+
COPILOT_PLUGIN_OPTION_ENDPOINT=https://your-collector.example.com/v1/traces
|
|
42
|
+
COPILOT_PLUGIN_OPTION_HEADERS=x-pinta-relay-token=YOUR-TOKEN
|
|
43
|
+
# optional: external guard (allow/deny tool calls)
|
|
44
|
+
PINTA_GUARD_ENDPOINT=https://your-relay.example.com/guard
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Var | Purpose |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `COPILOT_PLUGIN_OPTION_ENDPOINT` | Full OTLP/HTTP traces URL. **Namespaced to avoid colliding with Copilot's native OTel** (`OTEL_EXPORTER_OTLP_*`). The standard `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` / `OTEL_EXPORTER_OTLP_ENDPOINT` are honored as a lower-priority fallback. |
|
|
50
|
+
| `COPILOT_PLUGIN_OPTION_HEADERS` | `key=val,key=val` request headers (auth). Falls back to `OTEL_EXPORTER_OTLP_HEADERS`. |
|
|
51
|
+
| `PINTA_GUARD_ENDPOINT` | Optional. POST'd on `preToolUse`/`permissionRequest`; a `DENY` response blocks the tool. |
|
|
52
|
+
| `COPILOT_HOME` | Overrides `~/.copilot` for hook + env-file paths. |
|
|
53
|
+
|
|
54
|
+
## Guard (allow / deny + reason)
|
|
55
|
+
|
|
56
|
+
On `preToolUse` (all surfaces) and `permissionRequest` (CLI only) the adapter queries `PINTA_GUARD_ENDPOINT`. A `DENY` is emitted in the surface-appropriate format and the reason is shown to the model/user:
|
|
57
|
+
|
|
58
|
+
- `preToolUse` → `{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "<reason>" } }`
|
|
59
|
+
- `permissionRequest` → `{ "behavior": "deny", "message": "<reason>" }`
|
|
60
|
+
|
|
61
|
+
Guard is **fail-open** (no endpoint / timeout / error → allow), so it never breaks a session.
|
|
62
|
+
|
|
63
|
+
## Span conventions
|
|
64
|
+
|
|
65
|
+
| Attribute | Value |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `ingest.type` | `"copilot"` (aware-backend discriminator) |
|
|
68
|
+
| `copilot.hook` | Hook event name (resolved from `hook_event_name` / `hookEventName` / `hookName`) |
|
|
69
|
+
| `copilot.surface` | `cli` \| `ext` \| `cloud` (runtime-detected) |
|
|
70
|
+
| `copilot.<key>` | Every other top-level field (Bronze flattening, raw key preserved) |
|
|
71
|
+
| `service.name` | `"copilot"` · `telemetry.sdk.name` `"pinta-copilot"` |
|
|
72
|
+
|
|
73
|
+
### CLI ↔ ext payload differences (absorbed by the adapter)
|
|
74
|
+
|
|
75
|
+
| | CLI | ext |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| discriminator | `hook_event_name` (snake); `permissionRequest` uses `hookName` (camel) | `hook_event_name` (snake) |
|
|
78
|
+
| tool result | `tool_result` (structured) | `tool_response` (Claude-style) |
|
|
79
|
+
| `tool_use_id` | absent | present |
|
|
80
|
+
| `transcript_path` | Stop only | every event |
|
|
81
|
+
| subagent id | `agent_name`/`agent_display_name` | `agent_id`/`agent_type` |
|
|
82
|
+
| `permissionRequest` | fires | not fired |
|
|
83
|
+
|
|
84
|
+
Bronze flattening passes both shapes through losslessly; the backend's `CopilotIngestData` normalizes (`tool_response ?? tool_result`, `agent_id ?? agent_name`, …).
|
|
85
|
+
|
|
86
|
+
## Architecture
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
src/
|
|
90
|
+
├── index.ts # stdin → classify → trace → guard → span → exit 0 (always)
|
|
91
|
+
├── env-file.ts # ~/.copilot/pinta-copilot.env loader (unset-only)
|
|
92
|
+
├── core/
|
|
93
|
+
│ ├── types.ts # 3-way discriminator + snake/camel field absorption + classify
|
|
94
|
+
│ ├── surface.ts # cli | cloud | ext detection (ELECTRON_RUN_AS_NODE, …; NOT TERM_PROGRAM)
|
|
95
|
+
│ ├── otlp.ts # Bronze flattening (copilot.*) + ingest.type + surface + guard attrs
|
|
96
|
+
│ ├── trace.ts # per-turn ULID trace, keyed by session_id
|
|
97
|
+
│ ├── transport.ts # POST OTLP/HTTP traces (reads OTel env at call time)
|
|
98
|
+
│ ├── retry-queue.ts # file-backed JSONL queue, flushed next invocation
|
|
99
|
+
│ ├── guard.ts # POST PINTA_GUARD_ENDPOINT (50ms), fail-open
|
|
100
|
+
│ ├── redact.ts # Tier-1 redaction + Tier-3 truncation
|
|
101
|
+
│ ├── config.ts / env-bridge.ts
|
|
102
|
+
└── tools/install-hooks.ts # write/remove ~/.copilot/hooks/pinta-copilot.json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm install
|
|
109
|
+
npm run build # tsc → dist/
|
|
110
|
+
npm test # vitest
|
|
111
|
+
npm run mock-server # local OTLP collector
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
[PolyForm Noncommercial 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0) — see [LICENSE](LICENSE). Commercial use is not permitted; contact Pinta AI for a commercial license.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime config. Endpoint/headers come from OTEL_EXPORTER_OTLP_* env vars
|
|
3
|
+
* (loaded from ~/.copilot/pinta-copilot.env at startup) — not in this struct.
|
|
4
|
+
*
|
|
5
|
+
* pinta-copilot is installed as a direct hook file (D4), so the data dir
|
|
6
|
+
* (trace + retry-queue) must be a STABLE location independent of cwd —
|
|
7
|
+
* otherwise the per-turn trace written by UserPromptSubmit can't be read back
|
|
8
|
+
* by the following PreToolUse if cwd differs. We anchor it under the Copilot
|
|
9
|
+
* home (`$COPILOT_HOME` or `~/.copilot`).
|
|
10
|
+
*/
|
|
11
|
+
export interface PintaConfig {
|
|
12
|
+
pluginData: string;
|
|
13
|
+
tracePath: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function loadConfig(): PintaConfig;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadConfig = loadConfig;
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function copilotHome() {
|
|
10
|
+
return process.env.COPILOT_HOME || path_1.default.join(os_1.default.homedir(), ".copilot");
|
|
11
|
+
}
|
|
12
|
+
function loadConfig() {
|
|
13
|
+
const pluginData = process.env.COPILOT_PLUGIN_DATA || path_1.default.join(copilotHome(), "pinta-copilot-data");
|
|
14
|
+
return {
|
|
15
|
+
pluginData,
|
|
16
|
+
tracePath: path_1.default.join(pluginData, "trace.json"),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":";;;;;AAsBA,gCAOC;AA7BD,4CAAoB;AACpB,gDAAwB;AAiBxB,SAAS,WAAW;IAClB,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,cAAI,CAAC,IAAI,CAAC,YAAE,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;AACzE,CAAC;AAED,SAAgB,UAAU;IACxB,MAAM,UAAU,GACd,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,oBAAoB,CAAC,CAAC;IACpF,OAAO;QACL,UAAU;QACV,SAAS,EAAE,cAAI,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC;KAC/C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge Claude Code's userConfig env vars (CLAUDE_PLUGIN_OPTION_*) to the
|
|
3
|
+
* OTel SDK standard env vars (OTEL_EXPORTER_OTLP_*).
|
|
4
|
+
*
|
|
5
|
+
* Claude Code maps each plugin.json `userConfig.<key>` to a corresponding
|
|
6
|
+
* `CLAUDE_PLUGIN_OPTION_<KEY>` env var on hook spawn. We keep the user-
|
|
7
|
+
* facing names friendly (`endpoint`, `api_key`) and translate them into the
|
|
8
|
+
* canonical OTel env names so transport.ts (and any future OTel SDK adoption)
|
|
9
|
+
* can read them via the OTel-spec names.
|
|
10
|
+
*
|
|
11
|
+
* The user-facing `endpoint` is treated as a *full* OTLP/HTTP traces URL
|
|
12
|
+
* (e.g., `http://127.0.0.1:5147/v1/traces`), so we map it to
|
|
13
|
+
* `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` (signal-specific full URL per OTel
|
|
14
|
+
* spec) rather than `OTEL_EXPORTER_OTLP_ENDPOINT` (which is a base URL the
|
|
15
|
+
* SDK appends `/v1/traces` to). This way both the OTel SDK (when used) and
|
|
16
|
+
* pinta-cc's hand-written transport agree on the URL without any path
|
|
17
|
+
* manipulation downstream.
|
|
18
|
+
*
|
|
19
|
+
* Pinta Manager auto-injects `CLAUDE_PLUGIN_OPTION_*` via Claude Code's
|
|
20
|
+
* settings.json. OSS users fill them in via the `/plugin install` UI.
|
|
21
|
+
*
|
|
22
|
+
* Existing OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / HEADERS take precedence
|
|
23
|
+
* (explicit override).
|
|
24
|
+
*/
|
|
25
|
+
export declare function bridgeUserConfigToOtelEnv(): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.bridgeUserConfigToOtelEnv = bridgeUserConfigToOtelEnv;
|
|
4
|
+
/**
|
|
5
|
+
* Bridge Claude Code's userConfig env vars (CLAUDE_PLUGIN_OPTION_*) to the
|
|
6
|
+
* OTel SDK standard env vars (OTEL_EXPORTER_OTLP_*).
|
|
7
|
+
*
|
|
8
|
+
* Claude Code maps each plugin.json `userConfig.<key>` to a corresponding
|
|
9
|
+
* `CLAUDE_PLUGIN_OPTION_<KEY>` env var on hook spawn. We keep the user-
|
|
10
|
+
* facing names friendly (`endpoint`, `api_key`) and translate them into the
|
|
11
|
+
* canonical OTel env names so transport.ts (and any future OTel SDK adoption)
|
|
12
|
+
* can read them via the OTel-spec names.
|
|
13
|
+
*
|
|
14
|
+
* The user-facing `endpoint` is treated as a *full* OTLP/HTTP traces URL
|
|
15
|
+
* (e.g., `http://127.0.0.1:5147/v1/traces`), so we map it to
|
|
16
|
+
* `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` (signal-specific full URL per OTel
|
|
17
|
+
* spec) rather than `OTEL_EXPORTER_OTLP_ENDPOINT` (which is a base URL the
|
|
18
|
+
* SDK appends `/v1/traces` to). This way both the OTel SDK (when used) and
|
|
19
|
+
* pinta-cc's hand-written transport agree on the URL without any path
|
|
20
|
+
* manipulation downstream.
|
|
21
|
+
*
|
|
22
|
+
* Pinta Manager auto-injects `CLAUDE_PLUGIN_OPTION_*` via Claude Code's
|
|
23
|
+
* settings.json. OSS users fill them in via the `/plugin install` UI.
|
|
24
|
+
*
|
|
25
|
+
* Existing OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / HEADERS take precedence
|
|
26
|
+
* (explicit override).
|
|
27
|
+
*/
|
|
28
|
+
function bridgeUserConfigToOtelEnv() {
|
|
29
|
+
if (process.env.CLAUDE_PLUGIN_OPTION_ENDPOINT &&
|
|
30
|
+
!process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) {
|
|
31
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT =
|
|
32
|
+
process.env.CLAUDE_PLUGIN_OPTION_ENDPOINT;
|
|
33
|
+
}
|
|
34
|
+
if (process.env.CLAUDE_PLUGIN_OPTION_API_KEY && !process.env.OTEL_EXPORTER_OTLP_HEADERS) {
|
|
35
|
+
process.env.OTEL_EXPORTER_OTLP_HEADERS =
|
|
36
|
+
`x-pinta-relay-token=${process.env.CLAUDE_PLUGIN_OPTION_API_KEY}`;
|
|
37
|
+
}
|
|
38
|
+
// Expose for guard.ts (~/guard/evaluate auth header)
|
|
39
|
+
if (!process.env.PINTA_RELAY_TOKEN && process.env.CLAUDE_PLUGIN_OPTION_API_KEY) {
|
|
40
|
+
process.env.PINTA_RELAY_TOKEN = process.env.CLAUDE_PLUGIN_OPTION_API_KEY;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=env-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-bridge.js","sourceRoot":"","sources":["../../src/core/env-bridge.ts"],"names":[],"mappings":";;AAwBA,8DAgBC;AAxCD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,SAAgB,yBAAyB;IACvC,IACE,OAAO,CAAC,GAAG,CAAC,6BAA6B;QACzC,CAAC,OAAO,CAAC,GAAG,CAAC,kCAAkC,EAC/C,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,kCAAkC;YAC5C,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAC;IAC9C,CAAC;IACD,IAAI,OAAO,CAAC,GAAG,CAAC,4BAA4B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,CAAC;QACxF,OAAO,CAAC,GAAG,CAAC,0BAA0B;YACpC,uBAAuB,OAAO,CAAC,GAAG,CAAC,4BAA4B,EAAE,CAAC;IACtE,CAAC;IACD,qDAAqD;IACrD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,OAAO,CAAC,GAAG,CAAC,4BAA4B,EAAE,CAAC;QAC/E,OAAO,CAAC,GAAG,CAAC,iBAAiB,GAAG,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC;IAC3E,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface GuardInput {
|
|
2
|
+
spanId: string;
|
|
3
|
+
toolName?: string;
|
|
4
|
+
toolInput?: unknown;
|
|
5
|
+
rawTextFields?: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export interface GuardResult {
|
|
8
|
+
decision: 'ALLOW' | 'DENY' | 'REVIEW';
|
|
9
|
+
reason: string | null;
|
|
10
|
+
userMessage: string | null;
|
|
11
|
+
durationMs: number;
|
|
12
|
+
failOpenReason?: 'timeout' | 'refused' | 'error';
|
|
13
|
+
}
|
|
14
|
+
export declare function evaluateGuard(input: GuardInput, endpoint: string | undefined): Promise<GuardResult | null>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.evaluateGuard = evaluateGuard;
|
|
4
|
+
// Guard must be fast or fail-open. 50ms default keeps the hook snappy;
|
|
5
|
+
// override for slower relays (or test harnesses) via PINTA_GUARD_TIMEOUT_MS.
|
|
6
|
+
const TIMEOUT_MS = Number(process.env.PINTA_GUARD_TIMEOUT_MS) || 50;
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise((_, reject) => setTimeout(() => {
|
|
9
|
+
const err = new Error('Guard request timed out');
|
|
10
|
+
err.name = 'TimeoutError';
|
|
11
|
+
reject(err);
|
|
12
|
+
}, ms));
|
|
13
|
+
}
|
|
14
|
+
async function evaluateGuard(input, endpoint) {
|
|
15
|
+
if (!endpoint)
|
|
16
|
+
return null;
|
|
17
|
+
if (process.env.PINTA_GUARD_DISABLED === '1')
|
|
18
|
+
return null;
|
|
19
|
+
const start = Date.now();
|
|
20
|
+
try {
|
|
21
|
+
const res = await Promise.race([
|
|
22
|
+
fetch(endpoint, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: {
|
|
25
|
+
'content-type': 'application/json',
|
|
26
|
+
'x-pinta-relay-token': process.env.PINTA_RELAY_TOKEN ?? '',
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({ input }),
|
|
29
|
+
}),
|
|
30
|
+
sleep(TIMEOUT_MS),
|
|
31
|
+
]);
|
|
32
|
+
if (res.status !== 200) {
|
|
33
|
+
return { decision: 'ALLOW', reason: null, userMessage: null, durationMs: Date.now() - start, failOpenReason: 'error' };
|
|
34
|
+
}
|
|
35
|
+
const body = (await res.json());
|
|
36
|
+
return {
|
|
37
|
+
decision: body.decision,
|
|
38
|
+
reason: body.reason,
|
|
39
|
+
userMessage: body.userMessage ?? null,
|
|
40
|
+
durationMs: body.durationMs ?? (Date.now() - start),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const reason = err.name === 'TimeoutError' ? 'timeout' : 'error';
|
|
45
|
+
return { decision: 'ALLOW', reason: null, userMessage: null, durationMs: Date.now() - start, failOpenReason: reason };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../../src/core/guard.ts"],"names":[],"mappings":";;AAgCA,sCAsCC;AApDD,uEAAuE;AACvE,6EAA6E;AAC7E,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,IAAI,EAAE,CAAC;AAEpE,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAC/B,UAAU,CAAC,GAAG,EAAE;QACd,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QACjD,GAAG,CAAC,IAAI,GAAG,cAAc,CAAC;QAC1B,MAAM,CAAC,GAAG,CAAC,CAAC;IACd,CAAC,EAAE,EAAE,CAAC,CACP,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,aAAa,CACjC,KAAiB,EACjB,QAA4B;IAE5B,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;YAC7B,KAAK,CAAC,QAAQ,EAAE;gBACd,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,qBAAqB,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,EAAE;iBAC3D;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC;aAChC,CAAC;YACF,KAAK,CAAC,UAAU,CAAC;SAClB,CAAC,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,CAAC;QACzH,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAK7B,CAAC;QACF,OAAO;YACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,IAAI;YACrC,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;SACpD,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,MAAM,GAAmC,GAAa,CAAC,IAAI,KAAK,cAAc,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;QAC3G,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,CAAC;IACxH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { GuardResult } from "./guard.js";
|
|
2
|
+
import { type RawEvent } from "./types.js";
|
|
3
|
+
import type { Surface } from "./surface.js";
|
|
4
|
+
export interface OtlpAttribute {
|
|
5
|
+
key: string;
|
|
6
|
+
value: {
|
|
7
|
+
stringValue: string;
|
|
8
|
+
} | {
|
|
9
|
+
intValue: number;
|
|
10
|
+
} | {
|
|
11
|
+
doubleValue: number;
|
|
12
|
+
} | {
|
|
13
|
+
boolValue: boolean;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface OtlpSpan {
|
|
17
|
+
traceId: string;
|
|
18
|
+
spanId: string;
|
|
19
|
+
name: string;
|
|
20
|
+
kind: number;
|
|
21
|
+
startTimeUnixNano: string;
|
|
22
|
+
endTimeUnixNano: string;
|
|
23
|
+
attributes: OtlpAttribute[];
|
|
24
|
+
}
|
|
25
|
+
export interface ResourceSpans {
|
|
26
|
+
resource: {
|
|
27
|
+
attributes: OtlpAttribute[];
|
|
28
|
+
};
|
|
29
|
+
scopeSpans: Array<{
|
|
30
|
+
scope: {
|
|
31
|
+
name: string;
|
|
32
|
+
version: string;
|
|
33
|
+
};
|
|
34
|
+
spans: OtlpSpan[];
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
export interface OtlpPayload {
|
|
38
|
+
resourceSpans: ResourceSpans[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Convert a 26-char Crockford ULID into 32 lowercase hex chars (16 bytes)
|
|
42
|
+
* suitable for an OTLP traceId.
|
|
43
|
+
*/
|
|
44
|
+
export declare function ulidToTraceId(ulid: string): string;
|
|
45
|
+
/** Generate a fresh 16-hex-char (8-byte) span ID. */
|
|
46
|
+
export declare function newSpanId(): string;
|
|
47
|
+
export declare function buildOtlpPayload(args: {
|
|
48
|
+
event: RawEvent;
|
|
49
|
+
traceId: string;
|
|
50
|
+
surface: Surface;
|
|
51
|
+
now?: number;
|
|
52
|
+
guard?: GuardResult | null;
|
|
53
|
+
}): OtlpPayload;
|
|
54
|
+
/** Concatenate per-hook payloads' resourceSpans into one OTLP payload. */
|
|
55
|
+
export declare function mergeBatch(payloads: OtlpPayload[]): OtlpPayload;
|