@harness-engineering/cli 1.24.3 → 1.25.1
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/dist/agents/skills/claude-code/harness-architecture-advisor/SKILL.md +10 -6
- package/dist/agents/skills/claude-code/harness-code-review/SKILL.md +40 -5
- package/dist/agents/skills/claude-code/harness-git-workflow/SKILL.md +32 -11
- package/dist/agents/skills/claude-code/harness-planning/SKILL.md +19 -0
- package/dist/agents/skills/claude-code/harness-skill-authoring/SKILL.md +42 -7
- package/dist/agents/skills/claude-code/harness-verification/SKILL.md +25 -0
- package/dist/agents/skills/codex/harness-architecture-advisor/SKILL.md +10 -6
- package/dist/agents/skills/codex/harness-code-review/SKILL.md +40 -5
- package/dist/agents/skills/codex/harness-git-workflow/SKILL.md +32 -11
- package/dist/agents/skills/codex/harness-planning/SKILL.md +19 -0
- package/dist/agents/skills/codex/harness-skill-authoring/SKILL.md +42 -7
- package/dist/agents/skills/codex/harness-verification/SKILL.md +25 -0
- package/dist/agents/skills/cursor/harness-architecture-advisor/SKILL.md +10 -6
- package/dist/agents/skills/cursor/harness-code-review/SKILL.md +40 -5
- package/dist/agents/skills/cursor/harness-git-workflow/SKILL.md +32 -11
- package/dist/agents/skills/cursor/harness-planning/SKILL.md +19 -0
- package/dist/agents/skills/cursor/harness-skill-authoring/SKILL.md +42 -7
- package/dist/agents/skills/cursor/harness-verification/SKILL.md +25 -0
- package/dist/agents/skills/gemini-cli/harness-architecture-advisor/SKILL.md +10 -6
- package/dist/agents/skills/gemini-cli/harness-code-review/SKILL.md +40 -5
- package/dist/agents/skills/gemini-cli/harness-git-workflow/SKILL.md +32 -11
- package/dist/agents/skills/gemini-cli/harness-planning/SKILL.md +19 -0
- package/dist/agents/skills/gemini-cli/harness-skill-authoring/SKILL.md +42 -7
- package/dist/agents/skills/gemini-cli/harness-verification/SKILL.md +25 -0
- package/dist/agents-md-MCUM4SIZ.js +10 -0
- package/dist/{architecture-FBSLURIB.js → architecture-HNIO6AUX.js} +6 -5
- package/dist/{assess-project-74UVWPMB.js → assess-project-6MV5TNY3.js} +2 -1
- package/dist/bin/harness-mcp.js +18 -17
- package/dist/bin/harness.js +47 -31
- package/dist/{check-phase-gate-WY6UICCL.js → check-phase-gate-VCBQHQAC.js} +6 -5
- package/dist/{chunk-Q3XYV5UC.js → chunk-47N6R2F4.js} +5 -1
- package/dist/{chunk-3XAPHB2Z.js → chunk-4NK7Z3BP.js} +1 -1
- package/dist/{chunk-I4QR3HNH.js → chunk-5BQ5BOJL.js} +5 -1
- package/dist/{chunk-LM5Z2WCA.js → chunk-6VZQJ5CX.js} +5 -1
- package/dist/{chunk-NV4FPO7J.js → chunk-AT74HEQM.js} +1 -1
- package/dist/{chunk-TDLOFNNQ.js → chunk-BOQRTARD.js} +1 -1
- package/dist/{chunk-ZAMT24QN.js → chunk-CJXVNDZZ.js} +1537 -737
- package/dist/{chunk-A737JDL4.js → chunk-CXJTVICF.js} +4 -4
- package/dist/{chunk-LOUH2LIC.js → chunk-EHRZMOQ2.js} +5 -1
- package/dist/{chunk-L5UONZ53.js → chunk-F23H3U5U.js} +6 -2
- package/dist/{chunk-YPKKEP3O.js → chunk-FES2YEQU.js} +9 -9
- package/dist/{chunk-SPTKLCKC.js → chunk-FIQL2HND.js} +6 -2
- package/dist/{chunk-FP53DDB5.js → chunk-FSNPBT74.js} +5 -1
- package/dist/{chunk-TB427QOK.js → chunk-GEEYCQDS.js} +13 -9
- package/dist/{chunk-ZOVATVQC.js → chunk-HIK6OEUF.js} +6 -1
- package/dist/chunk-K2SON7TI.js +5382 -0
- package/dist/chunk-KFQGP6VL.js +33 -0
- package/dist/{chunk-V2FGX2KD.js → chunk-M55DGGF3.js} +9 -5
- package/dist/{chunk-H4U2QNY2.js → chunk-MI6MA6OP.js} +16888 -13143
- package/dist/{chunk-YDRB55Q4.js → chunk-P7PANON5.js} +5 -1
- package/dist/chunk-RL4VMEXL.js +53 -0
- package/dist/{chunk-XNEMDKV5.js → chunk-RRHUCDRD.js} +1 -1
- package/dist/{chunk-O6UF33QH.js → chunk-RX7TUMBR.js} +320 -136
- package/dist/{chunk-3VYWO6QN.js → chunk-TYV4EUAD.js} +12 -8
- package/dist/{chunk-FJYP32IV.js → chunk-UQZBZINS.js} +6 -6
- package/dist/{chunk-UFQQBGC3.js → chunk-UV3BZMGT.js} +64 -3
- package/dist/{chunk-5PMRARB5.js → chunk-WIQA4BSH.js} +12 -5
- package/dist/{chunk-UQEUYRBP.js → chunk-WWACIIBA.js} +1 -1
- package/dist/{chunk-LVYPPKZI.js → chunk-XTITAVUR.js} +5 -1
- package/dist/{chunk-WEN5Z7CL.js → chunk-YTP2UDPV.js} +3 -3
- package/dist/ci-workflow-RTM7VVTD.js +10 -0
- package/dist/{constants-5JGUXPEK.js → constants-P4M3C2T3.js} +1 -0
- package/dist/{create-skill-QCXINA5Q.js → create-skill-6QWJHQYS.js} +3 -2
- package/dist/{dist-666AAZQ6.js → dist-3EWNRFFQ.js} +6 -1
- package/dist/{dist-RRMRZRV7.js → dist-ABTKRYZH.js} +1 -0
- package/dist/{dist-QW5G3GX3.js → dist-RADHOOXG.js} +4 -1
- package/dist/{dist-7EBSGAHX.js → dist-WCSJHQPK.js} +106 -3
- package/dist/{docs-H34GBVRS.js → docs-UBOGGHTY.js} +6 -5
- package/dist/engine-MJJAP5CH.js +10 -0
- package/dist/{entropy-ZAY73R6A.js → entropy-EMSXF2PX.js} +5 -4
- package/dist/{feedback-TMEGYMWU.js → feedback-ZLUX72HD.js} +2 -1
- package/dist/{generate-agent-definitions-PQPG6SX5.js → generate-agent-definitions-AWLPJ27C.js} +6 -5
- package/dist/{glob-helper-SRXMZPKM.js → glob-helper-VCQXK5XY.js} +2 -0
- package/dist/{graph-loader-M6FXJAKK.js → graph-loader-JHQVQRUS.js} +2 -1
- package/dist/hooks/adoption-tracker.js +189 -0
- package/dist/hooks/block-no-verify.js +50 -0
- package/dist/hooks/cost-tracker.js +66 -0
- package/dist/hooks/pre-compact-state.js +115 -0
- package/dist/hooks/profiles.ts +48 -0
- package/dist/hooks/protect-config.js +75 -0
- package/dist/hooks/quality-gate.js +131 -0
- package/dist/hooks/sentinel-post.js +159 -0
- package/dist/hooks/sentinel-pre.js +244 -0
- package/dist/hooks/telemetry-reporter.js +248 -0
- package/dist/index.d.ts +13 -7
- package/dist/index.js +30 -29
- package/dist/loader-JVSJZSWZ.js +12 -0
- package/dist/mcp-2553PNUC.js +38 -0
- package/dist/{performance-N67YJJDG.js → performance-7AGWJUY4.js} +6 -5
- package/dist/review-pipeline-ZWVQJTJX.js +13 -0
- package/dist/{runner-TY7DJGQV.js → runner-AAEF2TXY.js} +1 -0
- package/dist/runtime-D6YUQPP2.js +11 -0
- package/dist/{scan-U67OKDRS.js → scan-MPJ6JHUY.js} +2 -1
- package/dist/security-JLZUAQYT.js +14 -0
- package/dist/skill-executor-MOCUIAYS.js +9 -0
- package/dist/templates/base/.agnix.toml +16 -0
- package/dist/templates/orchestrator/WORKFLOW.md +55 -7
- package/dist/templates/orchestrator/template.json +1 -0
- package/dist/tool-tiers-7QGZ3FKY.js +98 -0
- package/dist/validate-TIIHRPMA.js +14 -0
- package/dist/validate-cross-check-ZOWFA3DB.js +10 -0
- package/dist/{version-KFFPOQAX.js → version-CUI434OP.js} +1 -0
- package/package.json +4 -4
- package/dist/agents-md-PBKKTSQY.js +0 -9
- package/dist/chunk-5LMZA5LZ.js +0 -38
- package/dist/chunk-OZIHPMA7.js +0 -5171
- package/dist/ci-workflow-QZRHAIO2.js +0 -9
- package/dist/engine-VUQEAJFZ.js +0 -9
- package/dist/loader-Y6A42WBD.js +0 -11
- package/dist/mcp-LCHC4NZ5.js +0 -37
- package/dist/review-pipeline-YXF5ITL2.js +0 -12
- package/dist/runtime-XNJUJCSG.js +0 -10
- package/dist/security-L2YN3CTI.js +0 -9
- package/dist/skill-executor-GA7BDX3F.js +0 -8
- package/dist/validate-MNE25KLZ.js +0 -13
- package/dist/validate-cross-check-Y4PDR63C.js +0 -9
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* global console */
|
|
3
|
+
// sentinel-post.js — PostToolUse:* hook
|
|
4
|
+
// Sentinel prompt injection defense — scans tool outputs for injection patterns.
|
|
5
|
+
// Exit codes: always 0 (PostToolUse cannot block)
|
|
6
|
+
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
8
|
+
import { resolve, dirname } from 'node:path';
|
|
9
|
+
import process from 'node:process';
|
|
10
|
+
|
|
11
|
+
// Minimal inline patterns for when @harness-engineering/core isn't available.
|
|
12
|
+
// Keep in sync with @harness-engineering/core injection-patterns.ts ALL_PATTERNS.
|
|
13
|
+
// Covers all HIGH-severity patterns and key MEDIUM patterns for degraded-mode safety.
|
|
14
|
+
function inlineScan(text) {
|
|
15
|
+
console.error('[sentinel] Running in degraded mode: core import failed, using inline patterns');
|
|
16
|
+
const findings = [];
|
|
17
|
+
const lines = text.split('\n');
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
const line = lines[i];
|
|
20
|
+
// HIGH: INJ-UNI-001 — Zero-width characters
|
|
21
|
+
// eslint-disable-next-line no-misleading-character-class -- intentional: detects zero-width chars for security
|
|
22
|
+
if (/[\u200B\u200C\u200D\uFEFF\u2060]/.test(line)) {
|
|
23
|
+
findings.push({ severity: 'high', ruleId: 'INJ-UNI-001', match: line.trim(), line: i + 1 });
|
|
24
|
+
}
|
|
25
|
+
// HIGH: INJ-UNI-002 — RTL/LTR override characters
|
|
26
|
+
if (/[\u202A-\u202E\u2066-\u2069]/.test(line)) {
|
|
27
|
+
findings.push({ severity: 'high', ruleId: 'INJ-UNI-002', match: line.trim(), line: i + 1 });
|
|
28
|
+
}
|
|
29
|
+
// HIGH: INJ-REROL-001 — Ignore previous instructions
|
|
30
|
+
if (/(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above|earlier)\s+(?:instructions?|prompts?|context|rules?|guidelines?)/i.test(line)) {
|
|
31
|
+
findings.push({ severity: 'high', ruleId: 'INJ-REROL-001', match: line.trim(), line: i + 1 });
|
|
32
|
+
}
|
|
33
|
+
// HIGH: INJ-REROL-002 — Role reassignment
|
|
34
|
+
if (/you\s+are\s+now\s+(?:a\s+|an\s+)?(?:new\s+)?(?:helpful\s+)?(?:my\s+)?(?:\w+\s+)?(?:assistant|agent|AI|bot|chatbot|system|persona)\b/i.test(line)) {
|
|
35
|
+
findings.push({ severity: 'high', ruleId: 'INJ-REROL-002', match: line.trim(), line: i + 1 });
|
|
36
|
+
}
|
|
37
|
+
// HIGH: INJ-REROL-003 — Direct instruction override
|
|
38
|
+
if (/(?:new\s+)?(?:system\s+)?(?:instruction|directive|role|persona)\s*[:=]\s*/i.test(line)) {
|
|
39
|
+
findings.push({ severity: 'high', ruleId: 'INJ-REROL-003', match: line.trim(), line: i + 1 });
|
|
40
|
+
}
|
|
41
|
+
// HIGH: INJ-PERM-001 — Enable all tools/permissions
|
|
42
|
+
if (/(?:allow|enable|grant)\s+all\s+(?:tools?|permissions?|access)/i.test(line)) {
|
|
43
|
+
findings.push({ severity: 'high', ruleId: 'INJ-PERM-001', match: line.trim(), line: i + 1 });
|
|
44
|
+
}
|
|
45
|
+
// HIGH: INJ-PERM-002 — Disable safety/security
|
|
46
|
+
if (/(?:disable|turn\s+off|remove|bypass)\s+(?:all\s+)?(?:safety|security|restrictions?|guardrails?|protections?|checks?)/i.test(line)) {
|
|
47
|
+
findings.push({ severity: 'high', ruleId: 'INJ-PERM-002', match: line.trim(), line: i + 1 });
|
|
48
|
+
}
|
|
49
|
+
// HIGH: INJ-PERM-003 — Auto-approve directive
|
|
50
|
+
if (/(?:auto[- ]?approve|--no-verify|--dangerously-skip-permissions)/i.test(line)) {
|
|
51
|
+
findings.push({ severity: 'high', ruleId: 'INJ-PERM-003', match: line.trim(), line: i + 1 });
|
|
52
|
+
}
|
|
53
|
+
// HIGH: INJ-ENC-001 — Suspicious base64
|
|
54
|
+
if (/(?<!Bearer\s)(?<![:])(?<![A-Za-z0-9/])(?!eyJ)(?:[A-Za-z0-9+/]{4}){7,}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?(?![A-Za-z0-9/])/.test(line)) {
|
|
55
|
+
findings.push({ severity: 'high', ruleId: 'INJ-ENC-001', match: line.trim(), line: i + 1 });
|
|
56
|
+
}
|
|
57
|
+
// MEDIUM: INJ-CTX-001 — System prompt claims
|
|
58
|
+
if (/(?:the\s+)?(?:system\s+prompt|system\s+message|hidden\s+instructions?)\s+(?:says?|tells?|instructs?|contains?|is)/i.test(line)) {
|
|
59
|
+
findings.push({ severity: 'medium', ruleId: 'INJ-CTX-001', match: line.trim(), line: i + 1 });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return findings;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function main() {
|
|
66
|
+
let raw = '';
|
|
67
|
+
try {
|
|
68
|
+
raw = readFileSync(0, 'utf-8');
|
|
69
|
+
} catch {
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!raw.trim()) {
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let input;
|
|
78
|
+
try {
|
|
79
|
+
input = JSON.parse(raw);
|
|
80
|
+
} catch {
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const toolName = input?.tool_name ?? '';
|
|
86
|
+
const toolOutput = input?.tool_output ?? '';
|
|
87
|
+
const sessionId = input?.session_id;
|
|
88
|
+
const workspaceRoot = process.cwd();
|
|
89
|
+
|
|
90
|
+
if (!toolOutput || typeof toolOutput !== 'string') {
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let findings;
|
|
95
|
+
try {
|
|
96
|
+
const core = await import('@harness-engineering/core');
|
|
97
|
+
findings = core.scanForInjection(toolOutput);
|
|
98
|
+
} catch {
|
|
99
|
+
findings = inlineScan(toolOutput);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const actionable = findings.filter((f) => f.severity === 'high' || f.severity === 'medium');
|
|
103
|
+
|
|
104
|
+
if (actionable.length > 0) {
|
|
105
|
+
try {
|
|
106
|
+
const core = await import('@harness-engineering/core');
|
|
107
|
+
core.writeTaint(
|
|
108
|
+
workspaceRoot,
|
|
109
|
+
sessionId,
|
|
110
|
+
`Injection pattern detected in PostToolUse:${toolName} result`,
|
|
111
|
+
actionable,
|
|
112
|
+
`PostToolUse:${toolName}`
|
|
113
|
+
);
|
|
114
|
+
} catch {
|
|
115
|
+
// Fallback inline taint writer — merges with existing taint state
|
|
116
|
+
try {
|
|
117
|
+
const id = sessionId || 'default';
|
|
118
|
+
const taintPath = resolve(workspaceRoot, '.harness', `session-taint-${id}.json`);
|
|
119
|
+
mkdirSync(dirname(taintPath), { recursive: true });
|
|
120
|
+
const now = new Date().toISOString();
|
|
121
|
+
const maxSev = actionable.some((f) => f.severity === 'high') ? 'high' : 'medium';
|
|
122
|
+
const newFindings = actionable.map((f) => ({
|
|
123
|
+
ruleId: f.ruleId, severity: f.severity, match: f.match,
|
|
124
|
+
source: `PostToolUse:${toolName}`, detectedAt: now,
|
|
125
|
+
}));
|
|
126
|
+
// Read and merge existing taint state to preserve earlier taintedAt and findings
|
|
127
|
+
let existing = null;
|
|
128
|
+
try { existing = JSON.parse(readFileSync(taintPath, 'utf-8')); } catch { /* no existing */ }
|
|
129
|
+
const state = {
|
|
130
|
+
sessionId: id,
|
|
131
|
+
taintedAt: existing?.taintedAt ?? now,
|
|
132
|
+
expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
|
|
133
|
+
reason: existing?.reason ?? `Injection pattern detected in PostToolUse:${toolName} result`,
|
|
134
|
+
severity: maxSev,
|
|
135
|
+
findings: [...(existing?.findings ?? []), ...newFindings],
|
|
136
|
+
};
|
|
137
|
+
writeFileSync(taintPath, JSON.stringify(state, null, 2) + '\n');
|
|
138
|
+
} catch { /* best-effort */ }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const f of actionable) {
|
|
142
|
+
process.stderr.write(
|
|
143
|
+
`Sentinel [${f.severity}] ${f.ruleId}: detected in ${toolName} output\n`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const low = findings.filter((f) => f.severity === 'low');
|
|
149
|
+
for (const f of low) {
|
|
150
|
+
process.stderr.write(`Sentinel [low] ${f.ruleId}: ${f.match.slice(0, 80)}\n`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
process.exit(0);
|
|
154
|
+
} catch {
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main();
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* global console */
|
|
3
|
+
// sentinel-pre.js — PreToolUse:* hook
|
|
4
|
+
// Sentinel prompt injection defense — scans tool inputs for injection patterns
|
|
5
|
+
// and blocks destructive operations during tainted sessions.
|
|
6
|
+
// Exit codes: 0 = allow, 2 = block
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, unlinkSync, realpathSync } from 'node:fs';
|
|
9
|
+
import { resolve, dirname } from 'node:path';
|
|
10
|
+
import process from 'node:process';
|
|
11
|
+
|
|
12
|
+
// Destructive tool patterns blocked during taint.
|
|
13
|
+
// These are intentionally inline — this check runs before the @harness-engineering/core
|
|
14
|
+
// import attempt to ensure enforcement even when core is unavailable.
|
|
15
|
+
// Keep in sync with DESTRUCTIVE_BASH exported from @harness-engineering/core injection-patterns.ts.
|
|
16
|
+
const DESTRUCTIVE_BASH = [
|
|
17
|
+
/\bgit\s+push\b/,
|
|
18
|
+
/\bgit\s+commit\b/,
|
|
19
|
+
/\brm\s+-rf?\b/,
|
|
20
|
+
/\brm\s+-r\b/,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function isDestructiveBash(command) {
|
|
24
|
+
return DESTRUCTIVE_BASH.some((p) => p.test(command));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isOutsideWorkspace(filePath, workspaceRoot) {
|
|
28
|
+
if (!filePath || !workspaceRoot) return false;
|
|
29
|
+
const resolved = resolve(workspaceRoot, filePath);
|
|
30
|
+
// Resolve symlinks to prevent bypass via symlink pointing outside workspace
|
|
31
|
+
let realResolved = resolved;
|
|
32
|
+
try { realResolved = realpathSync(resolved); } catch { /* path doesn't exist yet — use resolved */ }
|
|
33
|
+
return !realResolved.startsWith(workspaceRoot);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Minimal inline patterns for when @harness-engineering/core isn't available.
|
|
37
|
+
// Keep in sync with @harness-engineering/core injection-patterns.ts ALL_PATTERNS.
|
|
38
|
+
// Covers all HIGH-severity patterns and key MEDIUM patterns for degraded-mode safety.
|
|
39
|
+
function inlineScan(text) {
|
|
40
|
+
console.error('[sentinel] Running in degraded mode: core import failed, using inline patterns');
|
|
41
|
+
const findings = [];
|
|
42
|
+
const lines = text.split('\n');
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
const line = lines[i];
|
|
45
|
+
// HIGH: INJ-UNI-001 — Zero-width characters
|
|
46
|
+
// eslint-disable-next-line no-misleading-character-class -- intentional: detects zero-width chars for security
|
|
47
|
+
if (/[\u200B\u200C\u200D\uFEFF\u2060]/.test(line)) {
|
|
48
|
+
findings.push({ severity: 'high', ruleId: 'INJ-UNI-001', match: line.trim(), line: i + 1 });
|
|
49
|
+
}
|
|
50
|
+
// HIGH: INJ-UNI-002 — RTL/LTR override characters
|
|
51
|
+
if (/[\u202A-\u202E\u2066-\u2069]/.test(line)) {
|
|
52
|
+
findings.push({ severity: 'high', ruleId: 'INJ-UNI-002', match: line.trim(), line: i + 1 });
|
|
53
|
+
}
|
|
54
|
+
// HIGH: INJ-REROL-001 — Ignore previous instructions
|
|
55
|
+
if (/(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above|earlier)\s+(?:instructions?|prompts?|context|rules?|guidelines?)/i.test(line)) {
|
|
56
|
+
findings.push({ severity: 'high', ruleId: 'INJ-REROL-001', match: line.trim(), line: i + 1 });
|
|
57
|
+
}
|
|
58
|
+
// HIGH: INJ-REROL-002 — Role reassignment
|
|
59
|
+
if (/you\s+are\s+now\s+(?:a\s+|an\s+)?(?:new\s+)?(?:helpful\s+)?(?:my\s+)?(?:\w+\s+)?(?:assistant|agent|AI|bot|chatbot|system|persona)\b/i.test(line)) {
|
|
60
|
+
findings.push({ severity: 'high', ruleId: 'INJ-REROL-002', match: line.trim(), line: i + 1 });
|
|
61
|
+
}
|
|
62
|
+
// HIGH: INJ-REROL-003 — Direct instruction override
|
|
63
|
+
if (/(?:new\s+)?(?:system\s+)?(?:instruction|directive|role|persona)\s*[:=]\s*/i.test(line)) {
|
|
64
|
+
findings.push({ severity: 'high', ruleId: 'INJ-REROL-003', match: line.trim(), line: i + 1 });
|
|
65
|
+
}
|
|
66
|
+
// HIGH: INJ-PERM-001 — Enable all tools/permissions
|
|
67
|
+
if (/(?:allow|enable|grant)\s+all\s+(?:tools?|permissions?|access)/i.test(line)) {
|
|
68
|
+
findings.push({ severity: 'high', ruleId: 'INJ-PERM-001', match: line.trim(), line: i + 1 });
|
|
69
|
+
}
|
|
70
|
+
// HIGH: INJ-PERM-002 — Disable safety/security
|
|
71
|
+
if (/(?:disable|turn\s+off|remove|bypass)\s+(?:all\s+)?(?:safety|security|restrictions?|guardrails?|protections?|checks?)/i.test(line)) {
|
|
72
|
+
findings.push({ severity: 'high', ruleId: 'INJ-PERM-002', match: line.trim(), line: i + 1 });
|
|
73
|
+
}
|
|
74
|
+
// HIGH: INJ-PERM-003 — Auto-approve directive
|
|
75
|
+
if (/(?:auto[- ]?approve|--no-verify|--dangerously-skip-permissions)/i.test(line)) {
|
|
76
|
+
findings.push({ severity: 'high', ruleId: 'INJ-PERM-003', match: line.trim(), line: i + 1 });
|
|
77
|
+
}
|
|
78
|
+
// HIGH: INJ-ENC-001 — Suspicious base64
|
|
79
|
+
if (/(?<!Bearer\s)(?<![:])(?<![A-Za-z0-9/])(?!eyJ)(?:[A-Za-z0-9+/]{4}){7,}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?(?![A-Za-z0-9/])/.test(line)) {
|
|
80
|
+
findings.push({ severity: 'high', ruleId: 'INJ-ENC-001', match: line.trim(), line: i + 1 });
|
|
81
|
+
}
|
|
82
|
+
// MEDIUM: INJ-CTX-001 — System prompt claims
|
|
83
|
+
if (/(?:the\s+)?(?:system\s+prompt|system\s+message|hidden\s+instructions?)\s+(?:says?|tells?|instructs?|contains?|is)/i.test(line)) {
|
|
84
|
+
findings.push({ severity: 'medium', ruleId: 'INJ-CTX-001', match: line.trim(), line: i + 1 });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return findings;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extractText(toolName, toolInput) {
|
|
91
|
+
if (toolName === 'Bash') return toolInput?.command ?? '';
|
|
92
|
+
if (toolName === 'Write') return toolInput?.content ?? '';
|
|
93
|
+
if (toolName === 'Edit') return `${toolInput?.old_string ?? ''}\n${toolInput?.new_string ?? ''}`;
|
|
94
|
+
if (toolName === 'Read') return toolInput?.file_path ?? '';
|
|
95
|
+
const parts = [];
|
|
96
|
+
for (const value of Object.values(toolInput || {})) {
|
|
97
|
+
if (typeof value === 'string') parts.push(value);
|
|
98
|
+
}
|
|
99
|
+
return parts.join('\n') || null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function main() {
|
|
103
|
+
let raw = '';
|
|
104
|
+
try {
|
|
105
|
+
raw = readFileSync(0, 'utf-8');
|
|
106
|
+
} catch {
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!raw.trim()) {
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let input;
|
|
115
|
+
try {
|
|
116
|
+
input = JSON.parse(raw);
|
|
117
|
+
} catch {
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const toolName = input?.tool_name ?? '';
|
|
123
|
+
const toolInput = input?.tool_input ?? {};
|
|
124
|
+
const sessionId = input?.session_id;
|
|
125
|
+
const workspaceRoot = process.cwd();
|
|
126
|
+
|
|
127
|
+
// Step 1: Check taint state — block destructive ops if tainted
|
|
128
|
+
let tainted = false;
|
|
129
|
+
try {
|
|
130
|
+
const taintPath = resolve(
|
|
131
|
+
workspaceRoot,
|
|
132
|
+
'.harness',
|
|
133
|
+
`session-taint-${sessionId || 'default'}.json`
|
|
134
|
+
);
|
|
135
|
+
const taintRaw = readFileSync(taintPath, 'utf-8');
|
|
136
|
+
const taintState = JSON.parse(taintRaw);
|
|
137
|
+
|
|
138
|
+
const expiresAt = new Date(taintState.expiresAt);
|
|
139
|
+
if (new Date() >= expiresAt) {
|
|
140
|
+
try { unlinkSync(taintPath); } catch { /* ignore */ }
|
|
141
|
+
process.stderr.write(
|
|
142
|
+
'Sentinel: session taint expired. Destructive operations re-enabled.\n'
|
|
143
|
+
);
|
|
144
|
+
} else {
|
|
145
|
+
tainted = true;
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
// No taint file or malformed — not tainted
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (tainted) {
|
|
152
|
+
if (toolName === 'Bash') {
|
|
153
|
+
const command = toolInput?.command ?? '';
|
|
154
|
+
if (isDestructiveBash(command)) {
|
|
155
|
+
process.stderr.write(
|
|
156
|
+
`BLOCKED by Sentinel: "${toolName}" blocked during tainted session. ` +
|
|
157
|
+
`Destructive operations are restricted. Run "harness taint clear" to lift.\n`
|
|
158
|
+
);
|
|
159
|
+
process.exit(2);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (toolName === 'Write' || toolName === 'Edit') {
|
|
164
|
+
const filePath = toolInput?.file_path ?? '';
|
|
165
|
+
if (isOutsideWorkspace(filePath, workspaceRoot)) {
|
|
166
|
+
process.stderr.write(
|
|
167
|
+
`BLOCKED by Sentinel: "${toolName}" to "${filePath}" blocked during tainted session. ` +
|
|
168
|
+
`File is outside workspace. Run "harness taint clear" to lift.\n`
|
|
169
|
+
);
|
|
170
|
+
process.exit(2);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Step 2: Scan tool inputs for injection patterns
|
|
176
|
+
const textToScan = extractText(toolName, toolInput);
|
|
177
|
+
if (textToScan) {
|
|
178
|
+
let findings;
|
|
179
|
+
try {
|
|
180
|
+
const core = await import('@harness-engineering/core');
|
|
181
|
+
findings = core.scanForInjection(textToScan);
|
|
182
|
+
} catch {
|
|
183
|
+
findings = inlineScan(textToScan);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const actionable = findings.filter((f) => f.severity === 'high' || f.severity === 'medium');
|
|
187
|
+
|
|
188
|
+
if (actionable.length > 0) {
|
|
189
|
+
try {
|
|
190
|
+
const core = await import('@harness-engineering/core');
|
|
191
|
+
core.writeTaint(
|
|
192
|
+
workspaceRoot,
|
|
193
|
+
sessionId,
|
|
194
|
+
`Injection pattern detected in PreToolUse:${toolName} input`,
|
|
195
|
+
actionable,
|
|
196
|
+
`PreToolUse:${toolName}`
|
|
197
|
+
);
|
|
198
|
+
} catch {
|
|
199
|
+
// Fallback inline taint writer — merges with existing taint state
|
|
200
|
+
try {
|
|
201
|
+
const id = sessionId || 'default';
|
|
202
|
+
const taintPath = resolve(workspaceRoot, '.harness', `session-taint-${id}.json`);
|
|
203
|
+
mkdirSync(dirname(taintPath), { recursive: true });
|
|
204
|
+
const now = new Date().toISOString();
|
|
205
|
+
const maxSev = actionable.some((f) => f.severity === 'high') ? 'high' : 'medium';
|
|
206
|
+
const newFindings = actionable.map((f) => ({
|
|
207
|
+
ruleId: f.ruleId, severity: f.severity, match: f.match,
|
|
208
|
+
source: `PreToolUse:${toolName}`, detectedAt: now,
|
|
209
|
+
}));
|
|
210
|
+
// Read and merge existing taint state to preserve earlier taintedAt and findings
|
|
211
|
+
let existing = null;
|
|
212
|
+
try { existing = JSON.parse(readFileSync(taintPath, 'utf-8')); } catch { /* no existing */ }
|
|
213
|
+
const state = {
|
|
214
|
+
sessionId: id,
|
|
215
|
+
taintedAt: existing?.taintedAt ?? now,
|
|
216
|
+
expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
|
|
217
|
+
reason: existing?.reason ?? `Injection pattern detected in PreToolUse:${toolName} input`,
|
|
218
|
+
severity: maxSev,
|
|
219
|
+
findings: [...(existing?.findings ?? []), ...newFindings],
|
|
220
|
+
};
|
|
221
|
+
writeFileSync(taintPath, JSON.stringify(state, null, 2) + '\n');
|
|
222
|
+
} catch { /* best-effort */ }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for (const f of actionable) {
|
|
226
|
+
process.stderr.write(
|
|
227
|
+
`Sentinel [${f.severity}] ${f.ruleId}: detected in ${toolName} input\n`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const low = findings.filter((f) => f.severity === 'low');
|
|
233
|
+
for (const f of low) {
|
|
234
|
+
process.stderr.write(`Sentinel [low] ${f.ruleId}: ${f.match.slice(0, 80)}\n`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
process.exit(0);
|
|
239
|
+
} catch {
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
main();
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* global setTimeout, fetch, AbortSignal */
|
|
3
|
+
// telemetry-reporter.js — Stop:* hook
|
|
4
|
+
// Reads adoption.jsonl, resolves consent, sends telemetry events to PostHog,
|
|
5
|
+
// and shows a one-time first-run privacy notice.
|
|
6
|
+
// Exit codes: 0 = allow (always, log-only hook — never blocks session teardown)
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import process from 'node:process';
|
|
12
|
+
|
|
13
|
+
// PostHog project API key — public, write-only (cannot read data)
|
|
14
|
+
const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY ?? 'phc_wNTdCMcfJXZPgdNeDociZW6vwoGGo4nb7vqEfWThFfsG'; // harness-ignore SEC-SEC-002: public PostHog write-only ingest key
|
|
15
|
+
const POSTHOG_BATCH_URL = 'https://app.posthog.com/batch';
|
|
16
|
+
const MAX_ATTEMPTS = 3;
|
|
17
|
+
const TIMEOUT_MS = 5000;
|
|
18
|
+
|
|
19
|
+
const FIRST_RUN_NOTICE = `Harness collects anonymous usage analytics to improve the tool.
|
|
20
|
+
No personal information is sent. Disable with:
|
|
21
|
+
DO_NOT_TRACK=1 or harness.config.json \u2192 telemetry.enabled: false\n`;
|
|
22
|
+
|
|
23
|
+
// --- Helpers ---
|
|
24
|
+
|
|
25
|
+
function readJsonSafe(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sleep(ms) {
|
|
34
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- Consent ---
|
|
38
|
+
|
|
39
|
+
function resolveConsent(cwd) {
|
|
40
|
+
if (process.env.DO_NOT_TRACK === '1') return { allowed: false };
|
|
41
|
+
if (process.env.HARNESS_TELEMETRY_OPTOUT === '1') return { allowed: false };
|
|
42
|
+
|
|
43
|
+
const config = readJsonSafe(join(cwd, 'harness.config.json'));
|
|
44
|
+
if (config?.telemetry?.enabled === false) return { allowed: false };
|
|
45
|
+
|
|
46
|
+
const installId = getOrCreateInstallId(cwd);
|
|
47
|
+
|
|
48
|
+
const identityFile = readJsonSafe(join(cwd, '.harness', 'telemetry.json'));
|
|
49
|
+
const identity = {};
|
|
50
|
+
if (identityFile?.identity) {
|
|
51
|
+
if (typeof identityFile.identity.project === 'string') identity.project = identityFile.identity.project;
|
|
52
|
+
if (typeof identityFile.identity.team === 'string') identity.team = identityFile.identity.team;
|
|
53
|
+
if (typeof identityFile.identity.alias === 'string') identity.alias = identityFile.identity.alias;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { allowed: true, installId, identity };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Install ID ---
|
|
60
|
+
|
|
61
|
+
const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
62
|
+
|
|
63
|
+
function getOrCreateInstallId(cwd) {
|
|
64
|
+
const harnessDir = join(cwd, '.harness');
|
|
65
|
+
const installIdFile = join(harnessDir, '.install-id');
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const existing = readFileSync(installIdFile, 'utf-8').trim();
|
|
69
|
+
if (UUID_V4_RE.test(existing)) return existing;
|
|
70
|
+
} catch {
|
|
71
|
+
// File does not exist
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const id = randomUUID();
|
|
75
|
+
mkdirSync(harnessDir, { recursive: true });
|
|
76
|
+
writeFileSync(installIdFile, id, { encoding: 'utf-8', mode: 0o600 });
|
|
77
|
+
return id;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Collector ---
|
|
81
|
+
|
|
82
|
+
function readAdoptionRecords(cwd) {
|
|
83
|
+
const adoptionFile = join(cwd, '.harness', 'metrics', 'adoption.jsonl');
|
|
84
|
+
let raw;
|
|
85
|
+
try {
|
|
86
|
+
raw = readFileSync(adoptionFile, 'utf-8');
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const records = [];
|
|
92
|
+
for (const line of raw.split('\n')) {
|
|
93
|
+
const trimmed = line.trim();
|
|
94
|
+
if (!trimmed) continue;
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(trimmed);
|
|
97
|
+
if (
|
|
98
|
+
typeof parsed.skill === 'string' &&
|
|
99
|
+
typeof parsed.startedAt === 'string' &&
|
|
100
|
+
typeof parsed.duration === 'number' &&
|
|
101
|
+
typeof parsed.outcome === 'string' &&
|
|
102
|
+
Array.isArray(parsed.phasesReached)
|
|
103
|
+
) {
|
|
104
|
+
records.push(parsed);
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// Skip malformed lines
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return records;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function collectEvents(cwd, consent) {
|
|
114
|
+
const records = readAdoptionRecords(cwd);
|
|
115
|
+
if (records.length === 0) return [];
|
|
116
|
+
|
|
117
|
+
const { installId, identity } = consent;
|
|
118
|
+
const distinctId = identity.alias ?? installId;
|
|
119
|
+
|
|
120
|
+
return records.map((record) => ({
|
|
121
|
+
event: 'skill_invocation',
|
|
122
|
+
distinctId,
|
|
123
|
+
timestamp: record.startedAt,
|
|
124
|
+
properties: {
|
|
125
|
+
installId,
|
|
126
|
+
os: process.platform,
|
|
127
|
+
nodeVersion: process.version,
|
|
128
|
+
harnessVersion: readHarnessVersion(cwd),
|
|
129
|
+
skillName: record.skill,
|
|
130
|
+
duration: record.duration,
|
|
131
|
+
outcome: record.outcome === 'completed' ? 'success' : 'failure',
|
|
132
|
+
phasesReached: record.phasesReached,
|
|
133
|
+
...(identity.project ? { project: identity.project } : {}),
|
|
134
|
+
...(identity.team ? { team: identity.team } : {}),
|
|
135
|
+
},
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readHarnessVersion(cwd) {
|
|
140
|
+
try {
|
|
141
|
+
const pkg = readJsonSafe(join(cwd, 'node_modules', '@harness-engineering', 'core', 'package.json'));
|
|
142
|
+
return pkg?.version ?? 'unknown';
|
|
143
|
+
} catch {
|
|
144
|
+
return 'unknown';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Transport ---
|
|
149
|
+
|
|
150
|
+
async function sendEvents(events) {
|
|
151
|
+
if (events.length === 0) return;
|
|
152
|
+
|
|
153
|
+
const payload = JSON.stringify({ api_key: POSTHOG_API_KEY, batch: events });
|
|
154
|
+
|
|
155
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
156
|
+
try {
|
|
157
|
+
const res = await fetch(POSTHOG_BATCH_URL, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: { 'Content-Type': 'application/json' },
|
|
160
|
+
body: payload,
|
|
161
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
162
|
+
});
|
|
163
|
+
if (res.ok) return;
|
|
164
|
+
if (res.status < 500) return; // 4xx = permanent, do not retry
|
|
165
|
+
} catch {
|
|
166
|
+
// Network error or timeout — retry
|
|
167
|
+
}
|
|
168
|
+
if (attempt < MAX_ATTEMPTS - 1) {
|
|
169
|
+
await sleep(1000 * (attempt + 1));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Silent failure — all retries exhausted
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- First-run notice ---
|
|
176
|
+
|
|
177
|
+
function showFirstRunNotice(cwd) {
|
|
178
|
+
const flagFile = join(cwd, '.harness', '.telemetry-notice-shown');
|
|
179
|
+
if (existsSync(flagFile)) return;
|
|
180
|
+
|
|
181
|
+
process.stderr.write(FIRST_RUN_NOTICE);
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
mkdirSync(join(cwd, '.harness'), { recursive: true });
|
|
185
|
+
writeFileSync(flagFile, new Date().toISOString(), { encoding: 'utf-8' });
|
|
186
|
+
} catch {
|
|
187
|
+
// Non-fatal — notice will show again next time
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --- Main ---
|
|
192
|
+
|
|
193
|
+
async function main() {
|
|
194
|
+
let raw = '';
|
|
195
|
+
try {
|
|
196
|
+
raw = readFileSync(0, 'utf-8');
|
|
197
|
+
} catch {
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!raw.trim()) {
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Parse stdin (stop hook receives session JSON)
|
|
206
|
+
try {
|
|
207
|
+
JSON.parse(raw);
|
|
208
|
+
} catch {
|
|
209
|
+
process.stderr.write('[telemetry-reporter] Could not parse stdin — skipping\n');
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const cwd = process.cwd();
|
|
215
|
+
const consent = resolveConsent(cwd);
|
|
216
|
+
|
|
217
|
+
if (!consent.allowed) {
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Show first-run notice (before sending, so user sees it even if send fails)
|
|
222
|
+
showFirstRunNotice(cwd);
|
|
223
|
+
|
|
224
|
+
const events = collectEvents(cwd, consent);
|
|
225
|
+
if (events.length === 0) {
|
|
226
|
+
process.stderr.write('[telemetry-reporter] No adoption records to report\n');
|
|
227
|
+
process.exit(0);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await sendEvents(events);
|
|
231
|
+
|
|
232
|
+
// Truncate adoption.jsonl after successful send to prevent re-sending
|
|
233
|
+
const adoptionFile = join(cwd, '.harness', 'metrics', 'adoption.jsonl');
|
|
234
|
+
try {
|
|
235
|
+
writeFileSync(adoptionFile, '', 'utf-8');
|
|
236
|
+
} catch {
|
|
237
|
+
// Non-fatal — records may be re-sent next session
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
process.stderr.write(`[telemetry-reporter] Sent ${events.length} telemetry event(s)\n`);
|
|
241
|
+
process.exit(0);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
process.stderr.write(`[telemetry-reporter] Failed: ${err.message}\n`);
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
main();
|