@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/LICENSE +136 -0
  3. package/README.md +116 -0
  4. package/dist/core/config.d.ts +15 -0
  5. package/dist/core/config.js +19 -0
  6. package/dist/core/config.js.map +1 -0
  7. package/dist/core/env-bridge.d.ts +25 -0
  8. package/dist/core/env-bridge.js +43 -0
  9. package/dist/core/env-bridge.js.map +1 -0
  10. package/dist/core/guard.d.ts +14 -0
  11. package/dist/core/guard.js +48 -0
  12. package/dist/core/guard.js.map +1 -0
  13. package/dist/core/otlp.d.ts +55 -0
  14. package/dist/core/otlp.js +184 -0
  15. package/dist/core/otlp.js.map +1 -0
  16. package/dist/core/redact.d.ts +57 -0
  17. package/dist/core/redact.js +149 -0
  18. package/dist/core/redact.js.map +1 -0
  19. package/dist/core/retry-queue.d.ts +26 -0
  20. package/dist/core/retry-queue.js +127 -0
  21. package/dist/core/retry-queue.js.map +1 -0
  22. package/dist/core/surface.d.ts +21 -0
  23. package/dist/core/surface.js +13 -0
  24. package/dist/core/surface.js.map +1 -0
  25. package/dist/core/trace.d.ts +20 -0
  26. package/dist/core/trace.js +80 -0
  27. package/dist/core/trace.js.map +1 -0
  28. package/dist/core/transport.d.ts +18 -0
  29. package/dist/core/transport.js +127 -0
  30. package/dist/core/transport.js.map +1 -0
  31. package/dist/core/types.d.ts +49 -0
  32. package/dist/core/types.js +124 -0
  33. package/dist/core/types.js.map +1 -0
  34. package/dist/env-file.d.ts +4 -0
  35. package/dist/env-file.js +69 -0
  36. package/dist/env-file.js.map +1 -0
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.js +71 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/tools/doctor.d.ts +1 -0
  41. package/dist/tools/doctor.js +81 -0
  42. package/dist/tools/doctor.js.map +1 -0
  43. package/dist/tools/install-hooks.d.ts +1 -0
  44. package/dist/tools/install-hooks.js +101 -0
  45. package/dist/tools/install-hooks.js.map +1 -0
  46. 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;