@piagent/platform 1.0.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/.gitattributes +7 -0
- package/AGENTS.md +31 -0
- package/CHANGELOG.md +296 -0
- package/CODE_OF_CONDUCT.md +36 -0
- package/CONTRIBUTING.md +66 -0
- package/LICENSE +21 -0
- package/README.md +588 -0
- package/SECURITY.md +38 -0
- package/adapters/backend-api/profile.json +88 -0
- package/adapters/be-readonly-fe/profile.json +120 -0
- package/adapters/data/profile.json +88 -0
- package/adapters/devops/profile.json +92 -0
- package/adapters/docs/profile.json +74 -0
- package/adapters/fullstack/profile.json +94 -0
- package/adapters/generic/profile.json +95 -0
- package/adapters/mobile/profile.json +87 -0
- package/adapters/node-typescript/profile.json +76 -0
- package/adapters/python/profile.json +75 -0
- package/adapters/web-frontend/profile.json +101 -0
- package/catalog/capabilities.json +206 -0
- package/docs/architecture.md +64 -0
- package/docs/auto-delegation-policy.md +192 -0
- package/docs/capability-packs.md +234 -0
- package/docs/command-reference-vietnamese.md +484 -0
- package/docs/context-window-policy.md +85 -0
- package/docs/distribution-standard.md +229 -0
- package/docs/herdr-workflow.md +58 -0
- package/docs/mcp-and-tools.md +316 -0
- package/docs/memory-policy.md +164 -0
- package/docs/model-options.md +165 -0
- package/docs/oauth-providers.md +53 -0
- package/docs/operator-manual-vietnamese.md +1080 -0
- package/docs/package-architecture-notes.md +101 -0
- package/docs/project-adapters.md +174 -0
- package/docs/project-onboarding.md +135 -0
- package/docs/publishing-for-teams.md +64 -0
- package/docs/quality-benchmark.md +74 -0
- package/docs/quickstart-vietnamese.md +350 -0
- package/docs/release-install-policy.md +223 -0
- package/docs/runtime-harness-standard.md +118 -0
- package/docs/runtime-policy-design.md +132 -0
- package/docs/runtime-quality-baseline.md +257 -0
- package/docs/security-redaction-benchmark.md +56 -0
- package/docs/security-threat-model.md +66 -0
- package/docs/subagent-orchestration-capabilities.md +104 -0
- package/docs/subagents-and-multiagent.md +346 -0
- package/docs/task-implementation-contract.md +200 -0
- package/docs/team-onboarding.md +333 -0
- package/docs/usage-observability.md +130 -0
- package/docs/vercel-docs-site.md +115 -0
- package/docs/workflow-recipes.md +125 -0
- package/docs-site/assets/piagent-logo.svg +35 -0
- package/docs-site/favicon.svg +18 -0
- package/docs-site/index.html +1613 -0
- package/docs-site/vercel.json +24 -0
- package/evals/fixtures/action-proposal.invalid.json +34 -0
- package/evals/fixtures/action-proposal.valid.json +34 -0
- package/evals/fixtures/capability-pack.invalid.json +123 -0
- package/evals/fixtures/capability-pack.valid.json +123 -0
- package/evals/fixtures/capability-recipe.invalid.json +81 -0
- package/evals/fixtures/capability-recipe.valid.json +82 -0
- package/evals/fixtures/context-index.invalid.json +25 -0
- package/evals/fixtures/context-index.valid.json +25 -0
- package/evals/fixtures/eval-scenario.invalid.json +40 -0
- package/evals/fixtures/eval-scenario.valid.json +45 -0
- package/evals/fixtures/project-profile.invalid.json +88 -0
- package/evals/fixtures/project-profile.valid.json +95 -0
- package/evals/fixtures/task-contract.invalid.json +73 -0
- package/evals/fixtures/task-contract.valid.json +74 -0
- package/evals/golden/enforcement-decisions.json +329 -0
- package/evals/scenarios/capability-resolution.json +45 -0
- package/package.json +132 -0
- package/packages/piagent-core/README.md +147 -0
- package/packages/piagent-core/capabilities/capability-core.js +1023 -0
- package/packages/piagent-core/capabilities/capability-sources.js +265 -0
- package/packages/piagent-core/extensions/guard-io.js +45 -0
- package/packages/piagent-core/extensions/guard-shell-analysis.ts +558 -0
- package/packages/piagent-core/extensions/guard-types.ts +406 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +5760 -0
- package/packages/piagent-core/extensions/policy-core.js +1070 -0
- package/packages/piagent-core/extensions/redaction-core.js +4 -0
- package/packages/piagent-core/extensions/runtime-evidence.js +215 -0
- package/packages/piagent-core/package.json +46 -0
- package/packages/piagent-core/policies/base-policy.json +249 -0
- package/packages/piagent-core/prompts/be-to-fe.md +82 -0
- package/packages/piagent-core/prompts/commit.md +27 -0
- package/packages/piagent-core/prompts/discuss.md +35 -0
- package/packages/piagent-core/prompts/memory-policy.md +52 -0
- package/packages/piagent-core/prompts/model-options.md +68 -0
- package/packages/piagent-core/prompts/onboard-project.md +147 -0
- package/packages/piagent-core/prompts/piagent-commands.md +68 -0
- package/packages/piagent-core/prompts/plan.md +35 -0
- package/packages/piagent-core/prompts/platform-improve.md +69 -0
- package/packages/piagent-core/prompts/pr.md +26 -0
- package/packages/piagent-core/prompts/review.md +38 -0
- package/packages/piagent-core/prompts/scout.md +50 -0
- package/packages/piagent-core/prompts/task.md +69 -0
- package/packages/piagent-core/security/sensitive-data.js +187 -0
- package/packages/piagent-core/skills/piagent-ops/SKILL.md +44 -0
- package/packages/piagent-core/skills/piagent-source-cache/SKILL.md +59 -0
- package/packages/piagent-core/skills/piagent-source-cache/checkout-source-repo.sh +144 -0
- package/packages/piagent-core/subagents/piagent-oracle.md +29 -0
- package/packages/piagent-core/subagents/piagent-planner.md +46 -0
- package/packages/piagent-core/subagents/piagent-reviewer.md +48 -0
- package/packages/piagent-core/subagents/piagent-scout.md +37 -0
- package/packages/piagent-core/subagents/piagent-worker.md +44 -0
- package/packs/engineering-base/pack.json +123 -0
- package/packs/engineering-base/recipes/bounded-change.json +82 -0
- package/packs/web-delivery/pack.json +72 -0
- package/packs/web-delivery/recipes/verified-web-change.json +57 -0
- package/schemas/action-proposal.schema.json +90 -0
- package/schemas/capability-pack.schema.json +190 -0
- package/schemas/capability-recipe.schema.json +72 -0
- package/schemas/context-index.schema.json +258 -0
- package/schemas/eval-scenario.schema.json +67 -0
- package/schemas/project-profile.schema.json +555 -0
- package/schemas/task-contract.schema.json +314 -0
- package/scripts/audit-runtime-host.sh +78 -0
- package/scripts/bootstrap-local.sh +25 -0
- package/scripts/capability-catalog.mjs +290 -0
- package/scripts/check-runtime-advisories.mjs +114 -0
- package/scripts/configure-mcp.sh +301 -0
- package/scripts/configure-model-scope.sh +143 -0
- package/scripts/configure-subagents.sh +241 -0
- package/scripts/import-agent-instructions.mjs +199 -0
- package/scripts/init-project.sh +369 -0
- package/scripts/install-global.sh +532 -0
- package/scripts/link-project.sh +25 -0
- package/scripts/migrate-project-state.mjs +176 -0
- package/scripts/pi-auto.sh +79 -0
- package/scripts/pi-model-catalog.sh +154 -0
- package/scripts/pi-session-stats.sh +135 -0
- package/scripts/piagent-cli.mjs +53 -0
- package/scripts/profile-doctor.sh +238 -0
- package/scripts/quality-benchmark.sh +185 -0
- package/scripts/redaction-benchmark.mjs +227 -0
- package/scripts/runtime-policy-smoke.sh +48 -0
- package/scripts/setup.sh +425 -0
- package/scripts/team-doctor.sh +461 -0
- package/scripts/verify-local.sh +540 -0
- package/scripts/verify-release-identity.mjs +85 -0
- package/scripts/verify-vercel-link.mjs +64 -0
- package/templates/global/mcp.json +9 -0
- package/templates/global/settings.json +37 -0
- package/templates/project/.mcp.json +9 -0
- package/templates/project/.pi/context-index.json +25 -0
- package/templates/project/.pi/gitignore.template +22 -0
- package/templates/project/.pi/mcp.json +9 -0
- package/templates/project/.pi/memory/MEMORY.md +11 -0
- package/templates/project/.pi/memory/memory_summary.md +9 -0
- package/templates/project/.pi/piagent-profile.json +107 -0
- package/templates/project/.pi/project-context.md +30 -0
- package/templates/project/.pi/settings.json +17 -0
- package/templates/project/.pi/task-contract.template.json +74 -0
- package/templates/project/.pi/tech-context/README.md +7 -0
- package/templates/project/.pi/tech-stack.json +10 -0
- package/templates/project/AGENTS.md +34 -0
- package/templates/project/REVIEW_GUIDELINES.md +23 -0
- package/types/pi-runtime-shims.d.ts +13 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [[ $# -lt 1 ]]; then
|
|
5
|
+
echo "Usage: $0 <project-path> [profile-json]"
|
|
6
|
+
exit 2
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
PROJECT_PATH="$1"
|
|
10
|
+
PROFILE_PATH="${2:-"$PROJECT_PATH/.pi/piagent-profile.json"}"
|
|
11
|
+
|
|
12
|
+
if [[ ! -d "$PROJECT_PATH" ]]; then
|
|
13
|
+
echo "FAIL: project path does not exist: $PROJECT_PATH"
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
if [[ ! -f "$PROFILE_PATH" ]]; then
|
|
18
|
+
echo "FAIL: profile not found: $PROFILE_PATH"
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
node --input-type=module - "$PROJECT_PATH" "$PROFILE_PATH" <<'NODE'
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
|
|
26
|
+
const [projectPath, profilePath] = process.argv.slice(2);
|
|
27
|
+
const profile = JSON.parse(fs.readFileSync(profilePath, "utf8"));
|
|
28
|
+
|
|
29
|
+
const errors = [];
|
|
30
|
+
const warnings = [];
|
|
31
|
+
|
|
32
|
+
function requireArray(name) {
|
|
33
|
+
if (!Array.isArray(profile[name])) {
|
|
34
|
+
errors.push(`${name} must be an array`);
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
return profile[name];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function optionalArray(name) {
|
|
41
|
+
if (profile[name] === undefined) return [];
|
|
42
|
+
if (!Array.isArray(profile[name])) {
|
|
43
|
+
errors.push(`${name} must be an array when provided`);
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
return profile[name];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function warnShellOnlyProtectedPaths(shellProtectedPaths, protectedPaths, readOnlyPaths) {
|
|
50
|
+
const writeGuardedPaths = new Set([...protectedPaths, ...readOnlyPaths]);
|
|
51
|
+
for (const candidate of shellProtectedPaths) {
|
|
52
|
+
if (typeof candidate !== "string" || candidate.trim().length === 0) continue;
|
|
53
|
+
if (writeGuardedPaths.has(candidate)) continue;
|
|
54
|
+
warnings.push(`shellProtectedPaths-only path ${candidate} blocks shell access only; add it to protectedPaths to block read/write, or readOnlyPaths to allow read but block write/shell`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (profile.schemaVersion !== 1) errors.push("schemaVersion must be 1");
|
|
59
|
+
if (!profile.projectId) errors.push("projectId is required");
|
|
60
|
+
if (!profile.displayName) errors.push("displayName is required");
|
|
61
|
+
if (!profile.mode) errors.push("mode is required");
|
|
62
|
+
|
|
63
|
+
const rootMarkers = requireArray("rootMarkers");
|
|
64
|
+
const protectedPaths = requireArray("protectedPaths");
|
|
65
|
+
const shellProtectedPaths = optionalArray("shellProtectedPaths");
|
|
66
|
+
const readOnlyPaths = optionalArray("readOnlyPaths");
|
|
67
|
+
const requiredContext = requireArray("requiredContext");
|
|
68
|
+
const mcpCapabilities = requireArray("mcpCapabilities");
|
|
69
|
+
|
|
70
|
+
warnShellOnlyProtectedPaths(shellProtectedPaths, protectedPaths, readOnlyPaths);
|
|
71
|
+
|
|
72
|
+
const capabilityPacks = profile.capabilityPacks ?? [];
|
|
73
|
+
if (!Array.isArray(capabilityPacks)) {
|
|
74
|
+
errors.push("capabilityPacks must be an array");
|
|
75
|
+
} else {
|
|
76
|
+
const seenPacks = new Set();
|
|
77
|
+
for (const [index, pack] of capabilityPacks.entries()) {
|
|
78
|
+
if (!pack || typeof pack !== "object" || Array.isArray(pack)) {
|
|
79
|
+
errors.push(`capabilityPacks[${index}] must be an object`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(pack.name ?? "")) {
|
|
83
|
+
errors.push(`capabilityPacks[${index}].name is invalid`);
|
|
84
|
+
}
|
|
85
|
+
if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/.test(pack.version ?? "")) {
|
|
86
|
+
errors.push(`capabilityPacks[${index}].version is invalid`);
|
|
87
|
+
}
|
|
88
|
+
const key = `${pack.name}@${pack.version}`;
|
|
89
|
+
if (seenPacks.has(key)) errors.push(`capabilityPacks contains duplicate ${key}`);
|
|
90
|
+
seenPacks.add(key);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const defaultCapabilityPolicy = {
|
|
95
|
+
allowedOwners: [],
|
|
96
|
+
allowedLifecycles: [],
|
|
97
|
+
allowedFilesystemRead: [],
|
|
98
|
+
allowedFilesystemWrite: [],
|
|
99
|
+
allowedNetworkDomains: [],
|
|
100
|
+
allowedExternalActions: []
|
|
101
|
+
};
|
|
102
|
+
const capabilityPolicy = profile.capabilityPolicy && typeof profile.capabilityPolicy === "object" && !Array.isArray(profile.capabilityPolicy)
|
|
103
|
+
? profile.capabilityPolicy
|
|
104
|
+
: defaultCapabilityPolicy;
|
|
105
|
+
if (profile.capabilityPolicy !== undefined && capabilityPolicy === defaultCapabilityPolicy) errors.push("capabilityPolicy must be an object");
|
|
106
|
+
for (const key of ["allowedOwners", "allowedLifecycles", "allowedFilesystemRead", "allowedFilesystemWrite", "allowedNetworkDomains", "allowedExternalActions"]) {
|
|
107
|
+
if (!Array.isArray(capabilityPolicy[key])) errors.push(`capabilityPolicy.${key} must be an array`);
|
|
108
|
+
}
|
|
109
|
+
for (const lifecycle of Array.isArray(capabilityPolicy.allowedLifecycles) ? capabilityPolicy.allowedLifecycles : []) {
|
|
110
|
+
if (!["experimental", "stable", "deprecated"].includes(lifecycle)) {
|
|
111
|
+
errors.push(`capabilityPolicy.allowedLifecycles contains invalid value ${lifecycle}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!profile.verifyCommands || typeof profile.verifyCommands !== "object" || Array.isArray(profile.verifyCommands)) {
|
|
116
|
+
errors.push("verifyCommands must be an object");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const existingRootMarkers = rootMarkers.filter((marker) => fs.existsSync(path.join(projectPath, marker)));
|
|
120
|
+
if (existingRootMarkers.length === 0) {
|
|
121
|
+
warnings.push(`no rootMarkers found in project: ${rootMarkers.join(", ")}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const contextPath of requiredContext) {
|
|
125
|
+
if (contextPath.includes("<")) {
|
|
126
|
+
warnings.push(`requiredContext has placeholder: ${contextPath}`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!fs.existsSync(path.join(projectPath, contextPath))) {
|
|
130
|
+
warnings.push(`requiredContext missing in project: ${contextPath}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (protectedPaths.length === 0) warnings.push("protectedPaths is empty");
|
|
135
|
+
if (mcpCapabilities.length === 0) warnings.push("mcpCapabilities is empty");
|
|
136
|
+
|
|
137
|
+
const defaultRuntimePolicy = {
|
|
138
|
+
execPolicy: "enforce",
|
|
139
|
+
contextBudget: "enforce",
|
|
140
|
+
toolRegistry: "advisory",
|
|
141
|
+
finalGate: "advisory"
|
|
142
|
+
};
|
|
143
|
+
const runtimePolicyOverrides = profile.runtimePolicy === undefined
|
|
144
|
+
? {}
|
|
145
|
+
: profile.runtimePolicy && typeof profile.runtimePolicy === "object" && !Array.isArray(profile.runtimePolicy)
|
|
146
|
+
? profile.runtimePolicy
|
|
147
|
+
: {};
|
|
148
|
+
if (profile.runtimePolicy !== undefined && runtimePolicyOverrides !== profile.runtimePolicy) errors.push("runtimePolicy must be an object");
|
|
149
|
+
const runtimePolicy = { ...defaultRuntimePolicy, ...runtimePolicyOverrides };
|
|
150
|
+
for (const [key, value] of Object.entries(runtimePolicy)) {
|
|
151
|
+
if (!["off", "advisory", "enforce"].includes(value)) {
|
|
152
|
+
errors.push(`runtimePolicy.${key} must be off, advisory, or enforce`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const contextIndexDefaults = {
|
|
157
|
+
enabled: true,
|
|
158
|
+
path: ".pi/context-index.json",
|
|
159
|
+
writePolicy: "onboarding-record",
|
|
160
|
+
requireCitations: true,
|
|
161
|
+
maxNodes: 120,
|
|
162
|
+
maxEdges: 240,
|
|
163
|
+
includeTechStack: true,
|
|
164
|
+
includeMemoryPointers: true
|
|
165
|
+
};
|
|
166
|
+
let contextIndex = contextIndexDefaults;
|
|
167
|
+
if (profile.contextIndex !== undefined) {
|
|
168
|
+
if (!profile.contextIndex || typeof profile.contextIndex !== "object" || Array.isArray(profile.contextIndex)) {
|
|
169
|
+
errors.push("contextIndex must be an object");
|
|
170
|
+
} else {
|
|
171
|
+
contextIndex = { ...contextIndexDefaults, ...profile.contextIndex };
|
|
172
|
+
if (typeof contextIndex.enabled !== "boolean") errors.push("contextIndex.enabled must be boolean");
|
|
173
|
+
if (typeof contextIndex.path !== "string" || contextIndex.path.trim().length === 0) errors.push("contextIndex.path must be a non-empty string");
|
|
174
|
+
if (!["onboarding-record", "approved-workflow", "off"].includes(contextIndex.writePolicy)) {
|
|
175
|
+
errors.push("contextIndex.writePolicy must be onboarding-record, approved-workflow, or off");
|
|
176
|
+
}
|
|
177
|
+
if (typeof contextIndex.requireCitations !== "boolean") errors.push("contextIndex.requireCitations must be boolean");
|
|
178
|
+
if (!Number.isInteger(contextIndex.maxNodes) || contextIndex.maxNodes < 1 || contextIndex.maxNodes > 500) errors.push("contextIndex.maxNodes must be an integer from 1 to 500");
|
|
179
|
+
if (!Number.isInteger(contextIndex.maxEdges) || contextIndex.maxEdges < 0 || contextIndex.maxEdges > 1000) errors.push("contextIndex.maxEdges must be an integer from 0 to 1000");
|
|
180
|
+
if (typeof contextIndex.includeTechStack !== "boolean") errors.push("contextIndex.includeTechStack must be boolean");
|
|
181
|
+
if (typeof contextIndex.includeMemoryPointers !== "boolean") errors.push("contextIndex.includeMemoryPointers must be boolean");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (contextIndex.enabled && contextIndex.writePolicy !== "off") {
|
|
186
|
+
const indexPath = path.resolve(projectPath, contextIndex.path);
|
|
187
|
+
const relativeIndexPath = path.relative(projectPath, indexPath);
|
|
188
|
+
if (relativeIndexPath.startsWith("..") || path.isAbsolute(relativeIndexPath)) {
|
|
189
|
+
errors.push(`contextIndex.path escapes project root: ${contextIndex.path}`);
|
|
190
|
+
} else if (!fs.existsSync(indexPath)) {
|
|
191
|
+
warnings.push(`context index missing: ${contextIndex.path}; run /onboard-project or record piagent_context_index_record`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const verifyEntries = Object.entries(profile.verifyCommands ?? {});
|
|
196
|
+
if (verifyEntries.length === 0) {
|
|
197
|
+
errors.push("verifyCommands has no entries");
|
|
198
|
+
}
|
|
199
|
+
for (const [key, commands] of verifyEntries) {
|
|
200
|
+
if (!Array.isArray(commands) || commands.length === 0) {
|
|
201
|
+
errors.push(`verifyCommands.${key} must be a non-empty array`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
for (const command of commands) {
|
|
205
|
+
if (typeof command !== "string" || command.trim().length === 0) {
|
|
206
|
+
errors.push(`verifyCommands.${key} contains an empty command`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const report = {
|
|
212
|
+
projectPath,
|
|
213
|
+
profilePath,
|
|
214
|
+
projectId: profile.projectId,
|
|
215
|
+
mode: profile.mode,
|
|
216
|
+
rootMarkersFound: existingRootMarkers,
|
|
217
|
+
requiredContextCount: requiredContext.length,
|
|
218
|
+
protectedPathCount: protectedPaths.length,
|
|
219
|
+
shellProtectedPathCount: shellProtectedPaths.length,
|
|
220
|
+
readOnlyPathCount: readOnlyPaths.length,
|
|
221
|
+
verifyProfiles: verifyEntries.map(([key]) => key),
|
|
222
|
+
mcpCapabilities,
|
|
223
|
+
capabilityPacks: Array.isArray(capabilityPacks)
|
|
224
|
+
? capabilityPacks
|
|
225
|
+
.filter((pack) => pack && typeof pack === "object" && !Array.isArray(pack))
|
|
226
|
+
.map((pack) => `${String(pack.name ?? "<invalid>")}@${String(pack.version ?? "<invalid>")}`)
|
|
227
|
+
: [],
|
|
228
|
+
capabilityPolicy,
|
|
229
|
+
runtimePolicy,
|
|
230
|
+
contextIndex,
|
|
231
|
+
warnings,
|
|
232
|
+
errors
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
console.log(JSON.stringify(report, null, 2));
|
|
236
|
+
|
|
237
|
+
if (errors.length > 0) process.exit(1);
|
|
238
|
+
NODE
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
usage() {
|
|
5
|
+
cat <<'USAGE'
|
|
6
|
+
Usage:
|
|
7
|
+
scripts/quality-benchmark.sh <project-path> --init
|
|
8
|
+
scripts/quality-benchmark.sh <project-path> --record --scenario <name> --surface <name> --result <pass|fail|partial> [options]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--task-file <path> File containing task prompt/spec.
|
|
12
|
+
--verify <command> Verification command used for the run.
|
|
13
|
+
--tokens <number> Total tokens reported by the agent/provider.
|
|
14
|
+
--cost <number> Cost reported for the run.
|
|
15
|
+
--duration <seconds> Wall-clock duration.
|
|
16
|
+
--notes <text> Short notes or quality observations.
|
|
17
|
+
--agent <name> Backward-compatible alias for --surface.
|
|
18
|
+
|
|
19
|
+
Output:
|
|
20
|
+
<project-path>/.pi/benchmarks/quality-runs.jsonl
|
|
21
|
+
USAGE
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
25
|
+
usage
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
PROJECT_PATH="${1:-}"
|
|
30
|
+
if [[ -z "$PROJECT_PATH" ]]; then
|
|
31
|
+
usage
|
|
32
|
+
exit 2
|
|
33
|
+
fi
|
|
34
|
+
shift || true
|
|
35
|
+
|
|
36
|
+
PROJECT_PATH="$(cd "$PROJECT_PATH" && pwd)"
|
|
37
|
+
MODE=""
|
|
38
|
+
SCENARIO=""
|
|
39
|
+
SURFACE=""
|
|
40
|
+
RESULT=""
|
|
41
|
+
TASK_FILE=""
|
|
42
|
+
VERIFY_CMD=""
|
|
43
|
+
TOKENS=""
|
|
44
|
+
COST=""
|
|
45
|
+
DURATION=""
|
|
46
|
+
NOTES=""
|
|
47
|
+
|
|
48
|
+
while [[ $# -gt 0 ]]; do
|
|
49
|
+
case "$1" in
|
|
50
|
+
--init)
|
|
51
|
+
MODE="init"
|
|
52
|
+
shift
|
|
53
|
+
;;
|
|
54
|
+
--record)
|
|
55
|
+
MODE="record"
|
|
56
|
+
shift
|
|
57
|
+
;;
|
|
58
|
+
--scenario)
|
|
59
|
+
SCENARIO="${2:-}"
|
|
60
|
+
shift 2
|
|
61
|
+
;;
|
|
62
|
+
--surface|--agent)
|
|
63
|
+
SURFACE="${2:-}"
|
|
64
|
+
shift 2
|
|
65
|
+
;;
|
|
66
|
+
--result)
|
|
67
|
+
RESULT="${2:-}"
|
|
68
|
+
shift 2
|
|
69
|
+
;;
|
|
70
|
+
--task-file)
|
|
71
|
+
TASK_FILE="${2:-}"
|
|
72
|
+
shift 2
|
|
73
|
+
;;
|
|
74
|
+
--verify)
|
|
75
|
+
VERIFY_CMD="${2:-}"
|
|
76
|
+
shift 2
|
|
77
|
+
;;
|
|
78
|
+
--tokens)
|
|
79
|
+
TOKENS="${2:-}"
|
|
80
|
+
shift 2
|
|
81
|
+
;;
|
|
82
|
+
--cost)
|
|
83
|
+
COST="${2:-}"
|
|
84
|
+
shift 2
|
|
85
|
+
;;
|
|
86
|
+
--duration)
|
|
87
|
+
DURATION="${2:-}"
|
|
88
|
+
shift 2
|
|
89
|
+
;;
|
|
90
|
+
--notes)
|
|
91
|
+
NOTES="${2:-}"
|
|
92
|
+
shift 2
|
|
93
|
+
;;
|
|
94
|
+
-h|--help)
|
|
95
|
+
usage
|
|
96
|
+
exit 0
|
|
97
|
+
;;
|
|
98
|
+
*)
|
|
99
|
+
echo "Unknown argument: $1" >&2
|
|
100
|
+
usage
|
|
101
|
+
exit 2
|
|
102
|
+
;;
|
|
103
|
+
esac
|
|
104
|
+
done
|
|
105
|
+
|
|
106
|
+
BENCHMARK_DIR="$PROJECT_PATH/.pi/benchmarks"
|
|
107
|
+
mkdir -p "$BENCHMARK_DIR"
|
|
108
|
+
|
|
109
|
+
if [[ "$MODE" == "init" ]]; then
|
|
110
|
+
cat > "$BENCHMARK_DIR/quality-scenarios.md" <<'SCENARIOS'
|
|
111
|
+
# Agent quality benchmark scenarios
|
|
112
|
+
|
|
113
|
+
Use the same scenario, acceptance criteria, and verification command across approved agent surfaces before making quality, token, or cost claims.
|
|
114
|
+
|
|
115
|
+
## Scenario 1: read-only scout
|
|
116
|
+
|
|
117
|
+
- Goal: inspect repo structure and produce implementation plan.
|
|
118
|
+
- Must not edit files.
|
|
119
|
+
- Evidence: context files read, plan, risks, verify command proposal.
|
|
120
|
+
|
|
121
|
+
## Scenario 2: bounded source fix
|
|
122
|
+
|
|
123
|
+
- Goal: implement a small bugfix with clear acceptance criteria.
|
|
124
|
+
- Evidence: changed files, verify command exit 0, trace/handoff.
|
|
125
|
+
|
|
126
|
+
## Scenario 3: backend-readonly to frontend implementation
|
|
127
|
+
|
|
128
|
+
- Goal: scout backend/spec read-only, implement frontend mapping only.
|
|
129
|
+
- Evidence: backend contract snapshot, frontend changed files, frontend verify pass.
|
|
130
|
+
SCENARIOS
|
|
131
|
+
echo "Initialized benchmark scenarios: $BENCHMARK_DIR/quality-scenarios.md"
|
|
132
|
+
exit 0
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
if [[ "$MODE" != "record" || -z "$SCENARIO" || -z "$SURFACE" || -z "$RESULT" ]]; then
|
|
136
|
+
usage
|
|
137
|
+
exit 2
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
case "$RESULT" in
|
|
141
|
+
pass|fail|partial) ;;
|
|
142
|
+
*)
|
|
143
|
+
echo "--result must be pass, fail, or partial" >&2
|
|
144
|
+
exit 2
|
|
145
|
+
;;
|
|
146
|
+
esac
|
|
147
|
+
|
|
148
|
+
export PI_BENCHMARK_PROJECT_PATH="$PROJECT_PATH"
|
|
149
|
+
export PI_BENCHMARK_SCENARIO="$SCENARIO"
|
|
150
|
+
export PI_BENCHMARK_SURFACE="$SURFACE"
|
|
151
|
+
export PI_BENCHMARK_RESULT="$RESULT"
|
|
152
|
+
export PI_BENCHMARK_TASK_FILE="$TASK_FILE"
|
|
153
|
+
export PI_BENCHMARK_VERIFY="$VERIFY_CMD"
|
|
154
|
+
export PI_BENCHMARK_TOKENS="$TOKENS"
|
|
155
|
+
export PI_BENCHMARK_COST="$COST"
|
|
156
|
+
export PI_BENCHMARK_DURATION="$DURATION"
|
|
157
|
+
export PI_BENCHMARK_NOTES="$NOTES"
|
|
158
|
+
|
|
159
|
+
node --input-type=module <<'NODE'
|
|
160
|
+
import fs from "node:fs";
|
|
161
|
+
import path from "node:path";
|
|
162
|
+
|
|
163
|
+
const root = process.env.PI_BENCHMARK_PROJECT_PATH;
|
|
164
|
+
const target = path.join(root, ".pi", "benchmarks", "quality-runs.jsonl");
|
|
165
|
+
const numberOrNull = (value) => {
|
|
166
|
+
if (!value) return null;
|
|
167
|
+
const parsed = Number(value);
|
|
168
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
169
|
+
};
|
|
170
|
+
const payload = {
|
|
171
|
+
schemaVersion: 1,
|
|
172
|
+
recordedAt: new Date().toISOString(),
|
|
173
|
+
scenario: process.env.PI_BENCHMARK_SCENARIO,
|
|
174
|
+
surface: process.env.PI_BENCHMARK_SURFACE,
|
|
175
|
+
result: process.env.PI_BENCHMARK_RESULT,
|
|
176
|
+
taskFile: process.env.PI_BENCHMARK_TASK_FILE || null,
|
|
177
|
+
verifyCommand: process.env.PI_BENCHMARK_VERIFY || null,
|
|
178
|
+
tokens: numberOrNull(process.env.PI_BENCHMARK_TOKENS),
|
|
179
|
+
cost: numberOrNull(process.env.PI_BENCHMARK_COST),
|
|
180
|
+
durationSeconds: numberOrNull(process.env.PI_BENCHMARK_DURATION),
|
|
181
|
+
notes: process.env.PI_BENCHMARK_NOTES || null
|
|
182
|
+
};
|
|
183
|
+
fs.appendFileSync(target, `${JSON.stringify(payload)}\n`);
|
|
184
|
+
console.log(`Recorded benchmark run: ${target}`);
|
|
185
|
+
NODE
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import {
|
|
5
|
+
redactForStorage,
|
|
6
|
+
redactSensitiveText
|
|
7
|
+
} from "../packages/piagent-core/security/sensitive-data.js";
|
|
8
|
+
|
|
9
|
+
const REDACTION = "[REDACTED_SECRET]";
|
|
10
|
+
const join = (...parts) => parts.join("");
|
|
11
|
+
|
|
12
|
+
const contextualCases = [
|
|
13
|
+
["aws-assignment", "assignment", join("AWS_SECRET_ACCESS_KEY=", "wJalrXUtnFEMI/", "K7MDENG/bPxRfiCYEXAMPLEKEY")],
|
|
14
|
+
["aws-whitespace", "assignment", join("aws_secret_access_key ", "wJalrXUtnFEMI/", "K7MDENG/bPxRfiCYEXAMPLEKEY")],
|
|
15
|
+
["github-fine", "vendor", join("github_pat_", "11AAAAAAA0", "abcdefghijklmnopqrstuvwxyz")],
|
|
16
|
+
["github-classic", "vendor", join("ghp_", "abcdefghijklmnopqrstuvwxyz1234567890")],
|
|
17
|
+
["openai-project", "vendor", join("sk-", "proj-", "abcdefghijklmnopqrstuvwxyz1234567890")],
|
|
18
|
+
["stripe-live", "vendor", join("sk_", "live_", "51NxTExampleSecretValue123456")],
|
|
19
|
+
["slack-bot", "vendor", join("xoxb-", "123456789012-123456789012-", "AbCdEfGhIjKlMnOp")],
|
|
20
|
+
["google-api", "vendor", join("AIza", "SyDExampleKeyWithEnoughLength123456")],
|
|
21
|
+
["jwt", "vendor", join("eyJhbGciOiJIUzI1NiJ9.", "eyJzdWIiOiIxMjM0NTY3ODkwIn0.", "signature123456")],
|
|
22
|
+
["pem-pkcs8", "private-key", join("-----BEGIN ", "PRIVATE KEY-----\nabc123\n-----END PRIVATE KEY-----")],
|
|
23
|
+
["pem-openssh", "private-key", join("-----BEGIN ", "OPENSSH PRIVATE KEY-----\nabc123\n-----END OPENSSH PRIVATE KEY-----")],
|
|
24
|
+
["postgres-url", "credential-url", "postgres://app:synthetic-pass-42@db.example.invalid/prod"],
|
|
25
|
+
["mysql-url", "credential-url", "mysql://app:synthetic-pass-42@db.example.invalid/prod"],
|
|
26
|
+
["mariadb-url", "credential-url", "mariadb://app:synthetic-pass-42@db.example.invalid/prod"],
|
|
27
|
+
["mongodb-url", "credential-url", "mongodb://app:synthetic-pass-42@db.example.invalid/prod"],
|
|
28
|
+
["redis-url", "credential-url", "redis://app:synthetic-pass-42@cache.example.invalid/0"],
|
|
29
|
+
["http-userinfo", "credential-url", "https://robot:synthetic-http-pass-42@example.invalid/api"],
|
|
30
|
+
["ftp-userinfo", "credential-url", "ftp://robot:synthetic-ftp-pass-42@example.invalid/archive"],
|
|
31
|
+
["bearer", "authorization", "Authorization: Bearer syntheticBearerToken_1234567890"],
|
|
32
|
+
["basic-auth", "authorization", "Authorization: Basic dXNlcjpzeW50aGV0aWMtcGFzcy00Mg=="],
|
|
33
|
+
["token-auth", "authorization", "Authorization: Token synthetic-auth-token-123"],
|
|
34
|
+
["api-key-auth", "authorization", "Authorization: ApiKey synthetic-auth-key-123"],
|
|
35
|
+
["digest-auth", "authorization", "Authorization: Digest username=robot, response=synthetic-response-123"],
|
|
36
|
+
["password-key", "assignment", "PASSWORD=synthetic-pass-42"],
|
|
37
|
+
["pwd-key", "assignment", "pwd: synthetic-pass-42"],
|
|
38
|
+
["api-key-json", "assignment", "{\"api_key\":\"synthetic-key-123456\"}"],
|
|
39
|
+
["client-secret", "assignment", "client_secret=synthetic-client-123"],
|
|
40
|
+
["access-token", "assignment", "access_token=synthetic-access-123"],
|
|
41
|
+
["refresh-token", "assignment", "refresh_token=synthetic-refresh-123"],
|
|
42
|
+
["credential-key", "assignment", "credential: synthetic-credential-123"],
|
|
43
|
+
["prefixed-token", "assignment", "CI_JOB_TOKEN=synthetic-job-token-123"],
|
|
44
|
+
["alphabetic-token", "assignment", "token=abcdefghijklmnopqrstuvwx"],
|
|
45
|
+
["alphabetic-api-key", "assignment", "apiKey=ABCDEFGHIJKLMNOPQRSTUVWXYZ"],
|
|
46
|
+
["alphabetic-authorization", "authorization", "Authorization: Token abcdefghijklmnopqrstuvwx"],
|
|
47
|
+
["webhook-secret", "assignment", "WEBHOOK_SECRET=synthetic-webhook-123"],
|
|
48
|
+
["session-token", "assignment", "session_token=synthetic-session-123"],
|
|
49
|
+
["query-token", "assignment", "https://example.invalid/callback?access_token=synthetic-query-token-123"],
|
|
50
|
+
["query-refresh-token", "assignment", "https://example.invalid/callback?refresh_token=synthetic-query-refresh-123&ok=1"],
|
|
51
|
+
["hyphen-header", "assignment", "X-Api-Key: synthetic-header-key-123"],
|
|
52
|
+
["private-token-header", "assignment", "PRIVATE-TOKEN: synthetic-private-token-123"],
|
|
53
|
+
["camel-api-key", "assignment", "{\"apiKey\":\"synthetic-camel-key-123\"}"],
|
|
54
|
+
["camel-client-secret", "assignment", "{\"clientSecret\":\"synthetic-camel-secret-123\"}"],
|
|
55
|
+
["camel-access-token", "assignment", "{\"accessToken\":\"synthetic-camel-access-123\"}"],
|
|
56
|
+
["passphrase", "assignment", "passphrase=synthetic-passphrase-123"],
|
|
57
|
+
["private-key-field", "assignment", "{\"private_key\":\"synthetic-private-material-123\"}"],
|
|
58
|
+
["signing-key-field", "assignment", "signingKey=synthetic-signing-material-123"],
|
|
59
|
+
["quoted-export", "assignment", "export TOKEN='synthetic-export-token-123'"],
|
|
60
|
+
["quoted-password-spaces", "assignment", "password=\"Correct Horse Battery 42\""],
|
|
61
|
+
["quoted-client-secret-spaces", "assignment", "clientSecret='synthetic client secret 123'"],
|
|
62
|
+
["quoted-password-escape", "assignment", "password=\"Correct \\\"Horse\\\" Battery 42\""],
|
|
63
|
+
["quoted-secret-escape", "assignment", "secret='synthetic \\'quoted\\' secret 123'"],
|
|
64
|
+
["multiline", "assignment", "name=service\npassword=synthetic-multiline-42\nstatus=ok"]
|
|
65
|
+
].map(([name, category, text]) => ({ name, category, text }));
|
|
66
|
+
|
|
67
|
+
const unlabeledEntropyCases = [
|
|
68
|
+
["base64-32", "QWxhZGRpbjpvcGVuIHNlc2FtZTEyMzQ1Njc4"],
|
|
69
|
+
["urlsafe-40", "Hf8_3nQvZ0sL1aYp7Tt9mK2rW4xC6bD8eF0gJ5hN"],
|
|
70
|
+
["mixed-48", "aB9xP2mN7qR4tV8zK1cD6fG0hJ3lS5wY9uE2iO7p"],
|
|
71
|
+
["hex-64", "9f86d081884c7d659a2feaa0c55ad015bf4f1b2b0b822cd15d6c15b0f00a08aa"],
|
|
72
|
+
["base32-52", "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXPJBSQ"],
|
|
73
|
+
["opaque-id", "01J4Z3N8K2Q7V5X9C1B6M0R4T8Y2W7S5"]
|
|
74
|
+
].map(([name, text]) => ({ name, text }));
|
|
75
|
+
|
|
76
|
+
const benignCases = [
|
|
77
|
+
["prose-secret", "The secret: always run tests before final."],
|
|
78
|
+
["null-token", "token: null"],
|
|
79
|
+
["placeholder", "api_key: placeholder"],
|
|
80
|
+
["short-password", "password: short"],
|
|
81
|
+
["unset-secret", "clientSecret=<not-set>"],
|
|
82
|
+
["source-code", "if (token === undefined) return;"],
|
|
83
|
+
["git-sha", "commit 4431c9edaf7c72b56943d0c74c17ee0214690eb5"],
|
|
84
|
+
["sha256", "sha256:d8a46b50ea37b68544add1e73ccb382b9ba06f6fe673dd82c50b294fdf412ec6"],
|
|
85
|
+
["uuid", "request_id=123e4567-e89b-12d3-a456-426614174000"],
|
|
86
|
+
["example-url", "https://example.invalid/docs"],
|
|
87
|
+
["package-integrity", "integrity sha512-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789+/=="],
|
|
88
|
+
["public-key", "-----BEGIN PUBLIC KEY-----\nabc123\n-----END PUBLIC KEY-----"],
|
|
89
|
+
["artifact-id", "artifact_id=Hf8_3nQvZ0sL1aYp7Tt9mK2rW4xC6bD8"],
|
|
90
|
+
["trace-id", "trace_id=aB9xP2mN7qR4tV8zK1cD6fG0hJ3lS5wY9uE2"],
|
|
91
|
+
["checksum", "checksum=9f86d081884c7d659a2feaa0c55ad015"],
|
|
92
|
+
["model-id", "model=provider-model-2026-07-21"],
|
|
93
|
+
["file-name", "generated file auth.json.example"],
|
|
94
|
+
["command-doc", "Run npm test and tsc --noEmit."],
|
|
95
|
+
["boolean-secret", "secret=false"],
|
|
96
|
+
["redacted-marker", "password=[REDACTED_SECRET]"],
|
|
97
|
+
["quoted-redacted-marker", "password=\"[REDACTED_SECRET]\""],
|
|
98
|
+
["authorization-placeholder", "Authorization: Token placeholder"],
|
|
99
|
+
["quoted-placeholder", "clientSecret='not-set'"],
|
|
100
|
+
["your-key", "Set apiKey=your-key-here before local testing."],
|
|
101
|
+
["ordinary-number", "retry_token_count=12"],
|
|
102
|
+
["semantic-token-name", "The parser emits a token node."],
|
|
103
|
+
["password-policy", "Password policy requires twelve characters."],
|
|
104
|
+
["credential-doc", "Credential rotation is required quarterly."],
|
|
105
|
+
["password-policy-field", "passwordPolicy=minimum-length"],
|
|
106
|
+
["semantic-token-type-field", "semanticTokenType=namespace-declaration"]
|
|
107
|
+
].map(([name, text]) => ({ name, text }));
|
|
108
|
+
|
|
109
|
+
const structuredInput = {
|
|
110
|
+
password: "synthetic-structured-pass-42",
|
|
111
|
+
apiKey: "synthetic-structured-key-42",
|
|
112
|
+
nested: {
|
|
113
|
+
clientSecret: "synthetic-structured-client-42",
|
|
114
|
+
sessionToken: "synthetic-structured-session-42"
|
|
115
|
+
},
|
|
116
|
+
tokens: ["synthetic-array-token-123"],
|
|
117
|
+
apiKeys: ["synthetic-array-key-123"],
|
|
118
|
+
passwords: ["hunter42"],
|
|
119
|
+
credentials: ["userpass"],
|
|
120
|
+
id: "Hf8_3nQvZ0sL1aYp7Tt9mK2rW4xC6bD8",
|
|
121
|
+
checksum: "9f86d081884c7d659a2feaa0c55ad015",
|
|
122
|
+
labels: ["provider-model-2026-07-21"],
|
|
123
|
+
semanticTokens: ["namespace-declaration-123"],
|
|
124
|
+
colorTokens: ["primary-blue-500"],
|
|
125
|
+
passwordPolicy: "minimum-length",
|
|
126
|
+
semanticTokenType: "namespace-declaration"
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function percentage(value, total) {
|
|
130
|
+
return total === 0 ? 1 : value / total;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function evaluateRedactionBenchmark(options = {}) {
|
|
134
|
+
const contextualMisses = contextualCases
|
|
135
|
+
.filter(({ text }) => !redactSensitiveText(text).redacted)
|
|
136
|
+
.map(({ name }) => name);
|
|
137
|
+
const entropyDetected = unlabeledEntropyCases
|
|
138
|
+
.filter(({ text }) => redactSensitiveText(text).redacted)
|
|
139
|
+
.map(({ name }) => name);
|
|
140
|
+
const falsePositives = benignCases
|
|
141
|
+
.filter(({ text }) => redactSensitiveText(text).redacted)
|
|
142
|
+
.map(({ name }) => name);
|
|
143
|
+
const safeStructured = redactForStorage(structuredInput);
|
|
144
|
+
const structuredSensitiveChecks = [
|
|
145
|
+
safeStructured.password === REDACTION,
|
|
146
|
+
safeStructured.apiKey === REDACTION,
|
|
147
|
+
safeStructured.nested?.clientSecret === REDACTION,
|
|
148
|
+
safeStructured.nested?.sessionToken === REDACTION,
|
|
149
|
+
safeStructured.tokens?.[0] === REDACTION,
|
|
150
|
+
safeStructured.apiKeys?.[0] === REDACTION,
|
|
151
|
+
safeStructured.passwords?.[0] === REDACTION,
|
|
152
|
+
safeStructured.credentials?.[0] === REDACTION
|
|
153
|
+
];
|
|
154
|
+
const structuredBenignChecks = [
|
|
155
|
+
safeStructured.id === structuredInput.id,
|
|
156
|
+
safeStructured.checksum === structuredInput.checksum,
|
|
157
|
+
safeStructured.labels?.[0] === structuredInput.labels[0],
|
|
158
|
+
safeStructured.semanticTokens?.[0] === structuredInput.semanticTokens[0],
|
|
159
|
+
safeStructured.colorTokens?.[0] === structuredInput.colorTokens[0],
|
|
160
|
+
safeStructured.passwordPolicy === structuredInput.passwordPolicy,
|
|
161
|
+
safeStructured.semanticTokenType === structuredInput.semanticTokenType
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const iterations = Math.max(1, Math.min(Number(options.iterations ?? 250), 10_000));
|
|
165
|
+
const workload = [...contextualCases, ...unlabeledEntropyCases, ...benignCases];
|
|
166
|
+
const started = performance.now();
|
|
167
|
+
for (let iteration = 0; iteration < iterations; iteration += 1) {
|
|
168
|
+
for (const { text } of workload) redactSensitiveText(text);
|
|
169
|
+
}
|
|
170
|
+
const durationMs = performance.now() - started;
|
|
171
|
+
const evaluations = iterations * workload.length;
|
|
172
|
+
const largeOutput = `${"ordinary build output\n".repeat(8_192)}X-Api-Key: synthetic-large-output-key-123`;
|
|
173
|
+
const largeStarted = performance.now();
|
|
174
|
+
const largeResult = redactSensitiveText(largeOutput);
|
|
175
|
+
const largeDurationMs = performance.now() - largeStarted;
|
|
176
|
+
|
|
177
|
+
const result = {
|
|
178
|
+
contextual: {
|
|
179
|
+
total: contextualCases.length,
|
|
180
|
+
detected: contextualCases.length - contextualMisses.length,
|
|
181
|
+
recall: percentage(contextualCases.length - contextualMisses.length, contextualCases.length),
|
|
182
|
+
missed: contextualMisses
|
|
183
|
+
},
|
|
184
|
+
unlabeledEntropy: {
|
|
185
|
+
total: unlabeledEntropyCases.length,
|
|
186
|
+
detected: entropyDetected.length,
|
|
187
|
+
observationalRecall: percentage(entropyDetected.length, unlabeledEntropyCases.length),
|
|
188
|
+
detectedCases: entropyDetected,
|
|
189
|
+
gated: false
|
|
190
|
+
},
|
|
191
|
+
benign: {
|
|
192
|
+
total: benignCases.length,
|
|
193
|
+
falsePositives: falsePositives.length,
|
|
194
|
+
falsePositiveRate: percentage(falsePositives.length, benignCases.length),
|
|
195
|
+
cases: falsePositives
|
|
196
|
+
},
|
|
197
|
+
structured: {
|
|
198
|
+
sensitiveTotal: structuredSensitiveChecks.length,
|
|
199
|
+
sensitiveRedacted: structuredSensitiveChecks.filter(Boolean).length,
|
|
200
|
+
benignTotal: structuredBenignChecks.length,
|
|
201
|
+
benignPreserved: structuredBenignChecks.filter(Boolean).length
|
|
202
|
+
},
|
|
203
|
+
performance: {
|
|
204
|
+
iterations,
|
|
205
|
+
evaluations,
|
|
206
|
+
durationMs: Number(durationMs.toFixed(3)),
|
|
207
|
+
averageMicroseconds: Number(((durationMs * 1000) / evaluations).toFixed(3))
|
|
208
|
+
},
|
|
209
|
+
largeOutput: {
|
|
210
|
+
bytes: Buffer.byteLength(largeOutput),
|
|
211
|
+
durationMs: Number(largeDurationMs.toFixed(3)),
|
|
212
|
+
sensitiveValueRedacted: largeResult.redacted && !largeResult.text.includes("synthetic-large-output-key-123")
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
result.ok = result.contextual.recall === 1
|
|
216
|
+
&& result.benign.falsePositives === 0
|
|
217
|
+
&& result.structured.sensitiveRedacted === result.structured.sensitiveTotal
|
|
218
|
+
&& result.structured.benignPreserved === result.structured.benignTotal
|
|
219
|
+
&& result.largeOutput.sensitiveValueRedacted;
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
224
|
+
const result = evaluateRedactionBenchmark();
|
|
225
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
226
|
+
if (!result.ok) process.exitCode = 1;
|
|
227
|
+
}
|