failproofai 1.0.2-beta.5 → 1.0.2-beta.7
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +3 -3
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1zkek3z._.js → [root-of-the-server]__1tg4pu6._.js} +1 -1
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_12moi5-._.js → _19uwam-._.js} +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +7 -7
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07gm3zl._.js +12 -15
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mgrjnx._.js +10 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0stn0gx._.js → [root-of-the-server]__1jjjg6g._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1tag8_-._.js → [root-of-the-server]__1opxw-7._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1kesqs-._.js → _0_nsohj._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1km9-gn._.js → _1uvml5y._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
- package/.next/standalone/.next/static/chunks/{3_b73tc78jwsq.js → 0icodhlwr9x_e.js} +1 -1
- package/.next/standalone/.next/static/chunks/{21r7iw3rmixf5.js → 0tvmd0p_1i_g6.js} +1 -1
- package/.next/standalone/.next/static/chunks/{33np9nrh1mvj9.js → 1d6c85605lsgp.js} +1 -1
- package/.next/standalone/.next/static/chunks/{26lfjhb2kzzgb.js → 1d7o3wnt3h6se.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2_27jraat038x.js → 1vzyn9h2kvj-i.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2hwc8e923g5df.js → 2-9lvhuto1iwo.js} +1 -1
- package/.next/standalone/.next/static/chunks/{36-8i_vbkh85w.js → 3jt6n16p2qpex.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0tyt-3a5rtzy8.js → 3swqstog1cd9u.js} +1 -1
- package/.next/standalone/.next/static/chunks/{391tpdqcas7rq.js → 3vqddiy9tvlwf.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3jqm8ubpv3i-4.js → 44oamw33aso19.js} +1 -1
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +853 -669
- package/dist/cli.mjs +4534 -4429
- package/dist/worker.mjs +2 -2
- package/package.json +9 -9
- package/src/audit/cli.ts +52 -39
- package/src/audit/index.ts +33 -2
- package/src/hooks/backfill-cli.ts +1 -1
- package/src/hooks/configure-wizard.ts +126 -18
- package/src/hooks/fp-reset.ts +1 -1
- package/src/hooks/handler.ts +13 -1
- package/src/hooks/manager.ts +60 -7
- package/src/hooks/onboarding-attempt.ts +4 -4
- package/src/hooks/pack-cli.ts +162 -21
- package/src/hooks/pack-store.ts +21 -8
- package/src/hooks/read-stdin.ts +78 -8
- package/src/hooks/tui.ts +173 -39
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15r6r7w._.js +0 -10
- /package/.next/standalone/.next/static/{59jsH11LoyXu9UBoHvgGM → PKwyBPAHjy3DB8qqapaH4}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{59jsH11LoyXu9UBoHvgGM → PKwyBPAHjy3DB8qqapaH4}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{59jsH11LoyXu9UBoHvgGM → PKwyBPAHjy3DB8qqapaH4}/_ssgManifest.js +0 -0
|
@@ -157,10 +157,11 @@ export function blockerCleared(attempt: OnboardingAttempt, probe: RetryProbe): b
|
|
|
157
157
|
// command would just repeat the hard-fail. Only a new release is a reason
|
|
158
158
|
// to ask again (it might add support for this platform).
|
|
159
159
|
return probe.cliVersion !== attempt.cliVersion;
|
|
160
|
-
case "not_a_tty":
|
|
161
160
|
case "running_as_sudo":
|
|
162
|
-
//
|
|
163
|
-
//
|
|
161
|
+
// A property of the invocation, not of the machine, so the next one may
|
|
162
|
+
// well be fine. (`not_a_tty` used to sit here too. It is gone because
|
|
163
|
+
// there is no such refusal any more: a run with no terminal has no
|
|
164
|
+
// questions to ask, so it applies rather than declining.)
|
|
164
165
|
return true;
|
|
165
166
|
default:
|
|
166
167
|
return true;
|
|
@@ -174,7 +175,6 @@ export function attemptHintLines(attempt: OnboardingAttempt): string[] {
|
|
|
174
175
|
daemon_failed: "the failproofaid service could not be started",
|
|
175
176
|
cancelled: "it was cancelled",
|
|
176
177
|
unsupported_platform: "failproofaid does not run on this platform",
|
|
177
|
-
not_a_tty: "there was no terminal to ask in",
|
|
178
178
|
running_as_sudo: "it was run under sudo",
|
|
179
179
|
};
|
|
180
180
|
const detail = why[attempt.reason] ?? "it did not finish";
|
package/src/hooks/pack-cli.ts
CHANGED
|
@@ -20,9 +20,7 @@ import {
|
|
|
20
20
|
PACK_MANIFEST_ASSET,
|
|
21
21
|
addPack,
|
|
22
22
|
checkPackArtifact,
|
|
23
|
-
CORE_ALIASES,
|
|
24
23
|
fetchPackPreview,
|
|
25
|
-
CORE_SOURCE,
|
|
26
24
|
packTagMatchesVersion,
|
|
27
25
|
removePack,
|
|
28
26
|
setPackPolicyEnabled,
|
|
@@ -63,34 +61,153 @@ function parseList(rest: string[], flag: string): string[] | undefined {
|
|
|
63
61
|
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Flags that take a separate value, so it is never mistaken for the source.
|
|
66
|
+
*
|
|
67
|
+
* Must list every flag that can REACH this lane, not just the ones this lane
|
|
68
|
+
* acts on. `--scope` and `--custom` are handled by the caller and passed
|
|
69
|
+
* through untouched — and their values were being read as the pack to install,
|
|
70
|
+
* so `policies add --scope project acme/x` tried to fetch a pack called
|
|
71
|
+
* "project". A flag this file ignores still has a value this file must skip.
|
|
72
|
+
*/
|
|
73
|
+
const VALUE_FLAGS = new Set([
|
|
74
|
+
"--only",
|
|
75
|
+
"--policy",
|
|
76
|
+
"--category",
|
|
77
|
+
"--cli",
|
|
78
|
+
"--scope",
|
|
79
|
+
"--custom",
|
|
80
|
+
"-c",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Every index `--cli` claims, so the source is not found among its values.
|
|
85
|
+
*
|
|
86
|
+
* `--cli` is the one flag that takes MORE than one token, because the other
|
|
87
|
+
* lane spells it `--cli claude codex` and somebody typing that here should not
|
|
88
|
+
* be quietly given a different answer. Shared with `packAddSource` so the two
|
|
89
|
+
* agree by construction: a token consumed as a CLI name is never also read as
|
|
90
|
+
* the pack to install.
|
|
91
|
+
*/
|
|
92
|
+
/**
|
|
93
|
+
* Could this token be an agent name rather than the pack to install?
|
|
94
|
+
*
|
|
95
|
+
* `--cli` consumes several tokens, so it has to know where its own list ends,
|
|
96
|
+
* and "not a flag" is not enough: `--cli claude codex acme/x` would swallow the
|
|
97
|
+
* SOURCE and leave the command with nothing to install. A pack source always
|
|
98
|
+
* carries a slash — `parsePackSource` accepts nothing else — and no agent name
|
|
99
|
+
* has one, which separates them exactly.
|
|
100
|
+
*
|
|
101
|
+
* Deliberately NOT "is it a known agent". An unknown name has to be consumed to
|
|
102
|
+
* be REJECTED: stopping at `claud` would hand it to the source parser instead,
|
|
103
|
+
* and the reply would be about pack syntax rather than about the typo.
|
|
104
|
+
*/
|
|
105
|
+
function looksLikeCliName(token: string): boolean {
|
|
106
|
+
return !token.startsWith("-") && !token.includes("/");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function cliValueIndices(rest: string[]): Set<number> {
|
|
110
|
+
const taken = new Set<number>();
|
|
111
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
112
|
+
if (rest[i] !== "--cli" && !rest[i].startsWith("--cli=")) continue;
|
|
113
|
+
if (rest[i].startsWith("--cli=")) continue; // its value is in the same token
|
|
114
|
+
for (let j = i + 1; j < rest.length && looksLikeCliName(rest[j]); j += 1) {
|
|
115
|
+
taken.add(j);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return taken;
|
|
119
|
+
}
|
|
68
120
|
|
|
69
121
|
/** Find the positional source without mistaking a flag's separate value for it. */
|
|
70
122
|
export function packAddSource(rest: string[]): string | undefined {
|
|
71
|
-
const consumed =
|
|
123
|
+
const consumed = cliValueIndices(rest);
|
|
72
124
|
for (let i = 0; i < rest.length; i += 1) {
|
|
73
125
|
if (VALUE_FLAGS.has(rest[i])) consumed.add(i + 1);
|
|
74
126
|
}
|
|
75
|
-
|
|
127
|
+
// ONE dash, not two. `-c` passed a `startsWith("--")` filter and was returned
|
|
128
|
+
// as the pack to install — and no GitHub owner or repo may begin with a
|
|
129
|
+
// hyphen, so nothing legitimate is excluded by widening it.
|
|
130
|
+
return rest.find((arg, index) => !arg.startsWith("-") && !consumed.has(index));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The agent CLIs named by `--cli`, in either spelling, all of them validated.
|
|
135
|
+
*
|
|
136
|
+
* Two separate silent failures lived here, and both reported success:
|
|
137
|
+
*
|
|
138
|
+
* - An unknown name was accepted. `--cli claud` installed the pack, printed
|
|
139
|
+
* "Installed", exited 0, and guarded NOTHING — the misspelling matched no
|
|
140
|
+
* agent, so the pack applied to none of them.
|
|
141
|
+
* - A space-separated list was truncated to its first entry.
|
|
142
|
+
* `--cli claude codex` recorded `["claude"]` and dropped codex on the floor,
|
|
143
|
+
* because this lane split on commas while the other split on spaces.
|
|
144
|
+
*
|
|
145
|
+
* Both now take either spelling and refuse a name that is not an agent. The set
|
|
146
|
+
* comes from `INTEGRATION_TYPES` rather than a list written out here, because
|
|
147
|
+
* the copy in `bin/failproofai.mjs` is already a second hand-maintained one and
|
|
148
|
+
* a third would be the one that drifts.
|
|
149
|
+
*/
|
|
150
|
+
function parseCliList(rest: string[]): { clis?: string[] } | { error: string[] } {
|
|
151
|
+
const idx = rest.findIndex((a) => a === "--cli" || a.startsWith("--cli="));
|
|
152
|
+
if (idx === -1) return {};
|
|
153
|
+
const values: string[] = [];
|
|
154
|
+
if (rest[idx].startsWith("--cli=")) {
|
|
155
|
+
values.push(...rest[idx].slice("--cli=".length).split(","));
|
|
156
|
+
} else {
|
|
157
|
+
for (let j = idx + 1; j < rest.length && looksLikeCliName(rest[j]); j += 1) {
|
|
158
|
+
values.push(...rest[j].split(","));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const names = values.map((v) => v.trim()).filter(Boolean);
|
|
162
|
+
if (names.length === 0) return { clis: [] };
|
|
163
|
+
|
|
164
|
+
const known = new Set<string>(INTEGRATION_TYPES);
|
|
165
|
+
const unknown = names.filter((n) => !known.has(n));
|
|
166
|
+
if (unknown.length > 0) {
|
|
167
|
+
// Thrown, not warned. A pack scoped to an agent that does not exist is
|
|
168
|
+
// enforcing on nothing, and saying so afterwards is no use to the script
|
|
169
|
+
// that already read exit 0 and moved on.
|
|
170
|
+
const near = (bad: string) =>
|
|
171
|
+
INTEGRATION_TYPES.find((k) => k.startsWith(bad.slice(0, 3)) || bad.startsWith(k.slice(0, 3)));
|
|
172
|
+
const hint = unknown.map((u) => (near(u) ? `${u} (did you mean ${near(u)}?)` : u)).join(", ");
|
|
173
|
+
return {
|
|
174
|
+
error: [
|
|
175
|
+
`Not an agent: ${hint}`,
|
|
176
|
+
`--cli takes any of: ${INTEGRATION_TYPES.join(", ")}`,
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return { clis: names };
|
|
76
181
|
}
|
|
77
182
|
|
|
78
183
|
/** Names taken by our own policies, so a selection can be checked before install. */
|
|
184
|
+
/**
|
|
185
|
+
* Exported for tests: the parse is where both `--cli` bugs lived, and asserting
|
|
186
|
+
* it through a real `add` would need a release server for a question that is
|
|
187
|
+
* purely about argument shapes.
|
|
188
|
+
*/
|
|
189
|
+
export function selectionFromForTest(rest: string[]) {
|
|
190
|
+
return selectionFrom(rest);
|
|
191
|
+
}
|
|
192
|
+
|
|
79
193
|
function selectionFrom(rest: string[]): {
|
|
80
194
|
only?: string[];
|
|
81
195
|
categories?: string[];
|
|
82
196
|
all?: boolean;
|
|
83
197
|
clis?: string[];
|
|
198
|
+
/** Present when the selection itself is unusable — see `parseCliList`. */
|
|
199
|
+
error?: string[];
|
|
84
200
|
} {
|
|
85
201
|
// `--policy` reads right for one ("give me this policy"), `--only` for a set.
|
|
86
202
|
// They are the same switch; taking both means neither is the wrong guess.
|
|
87
203
|
const only = parseList(rest, "--policy") ?? parseList(rest, "--only");
|
|
88
204
|
const categories = parseList(rest, "--category");
|
|
89
|
-
const
|
|
205
|
+
const parsedClis = parseCliList(rest);
|
|
206
|
+
if ("error" in parsedClis) return { error: parsedClis.error };
|
|
90
207
|
return {
|
|
91
208
|
...(only ? { only } : {}),
|
|
92
209
|
...(categories ? { categories } : {}),
|
|
93
|
-
...(clis ? { clis } : {}),
|
|
210
|
+
...(parsedClis.clis ? { clis: parsedClis.clis } : {}),
|
|
94
211
|
...(rest.includes("--all") ? { all: true } : {}),
|
|
95
212
|
};
|
|
96
213
|
}
|
|
@@ -175,6 +292,29 @@ async function build(rest: string[]): Promise<PackCliResult> {
|
|
|
175
292
|
]);
|
|
176
293
|
}
|
|
177
294
|
|
|
295
|
+
// Two policies cannot share a name, and this is where that has to be caught.
|
|
296
|
+
//
|
|
297
|
+
// Nothing downstream can resolve the ambiguity: a name is what `--policy`
|
|
298
|
+
// selects, what the picker toggles, and what the enabled list stores — so a
|
|
299
|
+
// duplicate makes one of the two unreachable and the other's on/off state
|
|
300
|
+
// decide for both. Bundling several files makes it easy to reach by accident:
|
|
301
|
+
// a starter written by `publish --init` into a folder that already had a
|
|
302
|
+
// policy of the same name published as exactly this, twice over, with no
|
|
303
|
+
// complaint at build time.
|
|
304
|
+
const byName = new Map<string, number>();
|
|
305
|
+
for (const hook of hooks) byName.set(hook.name, (byName.get(hook.name) ?? 0) + 1);
|
|
306
|
+
const duplicates = [...byName.entries()].filter(([, n]) => n > 1).map(([name]) => name);
|
|
307
|
+
if (duplicates.length > 0) {
|
|
308
|
+
return fail([
|
|
309
|
+
duplicates.length === 1
|
|
310
|
+
? `Two policies are called ${JSON.stringify(duplicates[0])}.`
|
|
311
|
+
: `${duplicates.length} names are used by more than one policy: ${duplicates.join(", ")}.`,
|
|
312
|
+
"A name is what --policy selects and what the picker toggles, so a pack",
|
|
313
|
+
"cannot carry the same one twice — one of them would be unreachable.",
|
|
314
|
+
"Rename or delete one of each pair.",
|
|
315
|
+
]);
|
|
316
|
+
}
|
|
317
|
+
|
|
178
318
|
const policies: unknown[] = [];
|
|
179
319
|
for (const [index, hook] of hooks.entries()) {
|
|
180
320
|
// `category` and `defaultEnabled` are pack-manifest fields a plain custom
|
|
@@ -274,7 +414,7 @@ export async function runPolicyPicker(
|
|
|
274
414
|
{
|
|
275
415
|
what: "No policies are installed yet.",
|
|
276
416
|
hint: "Take ours, or anyone's:",
|
|
277
|
-
cmd: "failproofai policies add
|
|
417
|
+
cmd: "failproofai policies add FailproofAI/policies",
|
|
278
418
|
},
|
|
279
419
|
opts,
|
|
280
420
|
),
|
|
@@ -1383,18 +1523,19 @@ async function pickFromSource(
|
|
|
1383
1523
|
async function add(rest: string[]): Promise<PackCliResult> {
|
|
1384
1524
|
const source = packAddSource(rest);
|
|
1385
1525
|
const selection = selectionFrom(rest);
|
|
1386
|
-
|
|
1387
|
-
//
|
|
1388
|
-
//
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
//
|
|
1392
|
-
//
|
|
1393
|
-
//
|
|
1394
|
-
//
|
|
1395
|
-
//
|
|
1396
|
-
|
|
1397
|
-
|
|
1526
|
+
// Refused before anything is fetched or written: a pack scoped to an agent
|
|
1527
|
+
// that does not exist enforces on nothing, and saying so afterwards is no use
|
|
1528
|
+
// to the script that already read exit 0 and moved on.
|
|
1529
|
+
if (selection.error) return fail(selection.error);
|
|
1530
|
+
|
|
1531
|
+
// No short name for our own pack any more. `core` resolved to CORE_SOURCE and
|
|
1532
|
+
// was fetched, verified and pinned like anybody else's — but a spelling only
|
|
1533
|
+
// WE get to use still made our policies read as part of the tool rather than
|
|
1534
|
+
// as one pack among others, which is the distinction this lane exists to
|
|
1535
|
+
// erase. Ours is typed the same way yours is: `FailproofAI/policies`.
|
|
1536
|
+
// `parsePackSource` still recognises the retired spellings, so typing one
|
|
1537
|
+
// gets told what to type instead.
|
|
1538
|
+
const resolvedSource = source;
|
|
1398
1539
|
|
|
1399
1540
|
if (!resolvedSource) {
|
|
1400
1541
|
return fail(["Usage: failproofai policies add <source> [--policy a,b] [--category x,y] [--all]"]);
|
package/src/hooks/pack-store.ts
CHANGED
|
@@ -91,6 +91,15 @@ function baseUrl(): string {
|
|
|
91
91
|
const OWNER_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
92
92
|
const TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/;
|
|
93
93
|
|
|
94
|
+
export const CORE_SOURCE = "FailproofAI/policies";
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Spellings that USED to resolve to our own pack, kept only so typing one gets
|
|
98
|
+
* an answer rather than a parse error. They are not resolved any more — see the
|
|
99
|
+
* throw in `parsePackSource`.
|
|
100
|
+
*/
|
|
101
|
+
export const RETIRED_CORE_ALIASES = new Set(["core", "failproofai", "official"]);
|
|
102
|
+
|
|
94
103
|
/**
|
|
95
104
|
* Parse a pack source into owner, repo and an optional tag.
|
|
96
105
|
*
|
|
@@ -136,6 +145,17 @@ export function parsePackSpec(source: string): PackSpec {
|
|
|
136
145
|
const tag = at > 0 ? withoutScheme.slice(at + 1) : null;
|
|
137
146
|
const slash = repoPart.indexOf("/");
|
|
138
147
|
if (slash <= 0 || slash === repoPart.length - 1) {
|
|
148
|
+
// `core` / `failproofai` / `official` used to be spellings of our own pack.
|
|
149
|
+
// They are gone on purpose: a short name only WE get to use makes our
|
|
150
|
+
// policies look like part of the tool rather than a pack like any other,
|
|
151
|
+
// which is exactly the distinction this whole surface exists to remove.
|
|
152
|
+
// Named explicitly here so the spelling that used to work says what to type
|
|
153
|
+
// instead of failing as an unparseable source.
|
|
154
|
+
if (RETIRED_CORE_ALIASES.has(repoPart.trim().toLowerCase())) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`${JSON.stringify(source)} is no longer a pack name — ours is a pack like anyone else's now. Use ${CORE_SOURCE} instead.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
139
159
|
throw new Error(
|
|
140
160
|
`pack source must be owner/repo, owner/repo@tag, or a github.com URL — got ${JSON.stringify(source)}`,
|
|
141
161
|
);
|
|
@@ -325,8 +345,6 @@ interface FetchedPack {
|
|
|
325
345
|
* gives our own policies a delivery path nobody else's pack can use. One lane,
|
|
326
346
|
* the same for everyone — ours are just the pack whose short name we spell.
|
|
327
347
|
*/
|
|
328
|
-
export const CORE_SOURCE = "FailproofAI/policies";
|
|
329
|
-
export const CORE_ALIASES = new Set(["core", "failproofai", "official"]);
|
|
330
348
|
|
|
331
349
|
export interface PackAddOptions {
|
|
332
350
|
only?: string[];
|
|
@@ -358,12 +376,7 @@ export async function addPackFromSource(
|
|
|
358
376
|
source: string,
|
|
359
377
|
opts: PackAddOptions = {},
|
|
360
378
|
): Promise<AddedPack> {
|
|
361
|
-
|
|
362
|
-
// HERE rather than in the CLI so the dashboard and the CLI cannot disagree
|
|
363
|
-
// about what the short name means — they did once, and a name the CLI could
|
|
364
|
-
// resolve failed in the browser.
|
|
365
|
-
const resolved = CORE_ALIASES.has(source.trim().toLowerCase()) ? CORE_SOURCE : source;
|
|
366
|
-
const result = await addPack(resolved, opts);
|
|
379
|
+
const result = await addPack(source, opts);
|
|
367
380
|
return {
|
|
368
381
|
id: result.id,
|
|
369
382
|
version: result.version,
|
package/src/hooks/read-stdin.ts
CHANGED
|
@@ -15,34 +15,104 @@ export interface StdinRead {
|
|
|
15
15
|
oversized: boolean;
|
|
16
16
|
/** `process.stdin` emitted an `error` event before `end`. */
|
|
17
17
|
readError: boolean;
|
|
18
|
+
/** No `end` arrived within the budget; the payload is `""`. */
|
|
19
|
+
timedOut: boolean;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
/**
|
|
23
|
+
* How long to wait for the agent to finish writing the payload.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately generous: the parent has the payload in hand before it spawns
|
|
26
|
+
* the hook, so a real read is milliseconds and this can only fire on a
|
|
27
|
+
* misconfiguration. It exists to bound that misconfiguration, not to police a
|
|
28
|
+
* slow write — a false timeout here would drop a real policy decision.
|
|
29
|
+
*/
|
|
30
|
+
const STDIN_TIMEOUT_MS = 10_000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read the hook payload, and NEVER wait forever for it.
|
|
34
|
+
*
|
|
35
|
+
* This runs on the enforcement path — every tool call, on all eleven agent
|
|
36
|
+
* CLIs — and it used to have no bound at all. The only early exit was
|
|
37
|
+
* `readableEnded`, which helps only when stdin is ALREADY closed, so a parent
|
|
38
|
+
* that spawned the hook with a pipe it had not yet closed, or with an inherited
|
|
39
|
+
* terminal, left this promise unsettled forever. A hung hook is worse than
|
|
40
|
+
* either verdict: it does not fail open or closed, it freezes the agent's tool
|
|
41
|
+
* call, and nothing on our side ever gives up.
|
|
42
|
+
*
|
|
43
|
+
* Two bounds, because there are two ways in:
|
|
44
|
+
*
|
|
45
|
+
* - A TTY on stdin means no payload is coming, ever. Nobody types a hook
|
|
46
|
+
* payload at a keyboard, so waiting for EOF on a terminal is waiting for a
|
|
47
|
+
* person to press ctrl-D — which is exactly what a hand-run
|
|
48
|
+
* `failproofai --hook PreToolUse` looked like.
|
|
49
|
+
* - An open pipe might still deliver, so it gets a clock rather than a
|
|
50
|
+
* refusal.
|
|
51
|
+
*
|
|
52
|
+
* Both resolve empty and flag what happened. That matches how a read error has
|
|
53
|
+
* always been treated — warn, report, evaluate an empty payload — rather than
|
|
54
|
+
* inventing a third verdict for a case that is a malformed invocation, not a
|
|
55
|
+
* policy decision.
|
|
56
|
+
*/
|
|
57
|
+
export async function readStdinPayload(
|
|
58
|
+
maxBytes = 1_048_576,
|
|
59
|
+
timeoutMs = STDIN_TIMEOUT_MS,
|
|
60
|
+
// Injectable so a test can hand in a stream that never ends — which is the
|
|
61
|
+
// whole condition under test — WITHOUT reassigning `process.stdin`. Doing
|
|
62
|
+
// that globally takes the test runner's own stdin away and kills it.
|
|
63
|
+
stream: NodeJS.ReadableStream & {
|
|
64
|
+
isTTY?: boolean;
|
|
65
|
+
readableEnded?: boolean;
|
|
66
|
+
destroy?: () => void;
|
|
67
|
+
} = process.stdin,
|
|
68
|
+
): Promise<StdinRead> {
|
|
21
69
|
let payload = "";
|
|
22
70
|
let oversized = false;
|
|
23
71
|
let readError = false;
|
|
72
|
+
let timedOut = false;
|
|
73
|
+
|
|
74
|
+
// A terminal never ends on its own. Checked before any listener is attached,
|
|
75
|
+
// so the hand-run case costs nothing and returns immediately.
|
|
76
|
+
if (stream.isTTY) {
|
|
77
|
+
return { payload: "", oversized: false, readError: false, timedOut: false };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
24
81
|
try {
|
|
25
82
|
payload = await new Promise<string>((resolve, reject) => {
|
|
26
83
|
const chunks: string[] = [];
|
|
27
84
|
let totalBytes = 0;
|
|
28
|
-
|
|
29
|
-
|
|
85
|
+
stream.setEncoding("utf8");
|
|
86
|
+
stream.on("data", (chunk: string) => {
|
|
30
87
|
totalBytes += Buffer.byteLength(chunk);
|
|
31
88
|
if (totalBytes > maxBytes) {
|
|
32
89
|
oversized = true;
|
|
33
|
-
|
|
90
|
+
stream.destroy?.();
|
|
34
91
|
resolve("");
|
|
35
92
|
return;
|
|
36
93
|
}
|
|
37
94
|
chunks.push(chunk);
|
|
38
95
|
});
|
|
39
|
-
|
|
40
|
-
|
|
96
|
+
stream.on("end", () => resolve(chunks.join("")));
|
|
97
|
+
stream.on("error", reject);
|
|
41
98
|
// If stdin is already closed or not piped, resolve immediately
|
|
42
|
-
if (
|
|
99
|
+
if (stream.readableEnded) resolve("");
|
|
100
|
+
|
|
101
|
+
timer = setTimeout(() => {
|
|
102
|
+
timedOut = true;
|
|
103
|
+
// Whatever arrived before the clock ran out is NOT used: a partial
|
|
104
|
+
// payload is not valid JSON, and half a tool call is not a thing any
|
|
105
|
+
// policy should be asked to judge.
|
|
106
|
+
stream.destroy?.();
|
|
107
|
+
resolve("");
|
|
108
|
+
}, timeoutMs);
|
|
109
|
+
// Nothing else keeps this process alive on account of the timer.
|
|
110
|
+
timer.unref?.();
|
|
43
111
|
});
|
|
44
112
|
} catch {
|
|
45
113
|
readError = true;
|
|
114
|
+
} finally {
|
|
115
|
+
if (timer) clearTimeout(timer);
|
|
46
116
|
}
|
|
47
|
-
return { payload, oversized, readError };
|
|
117
|
+
return { payload, oversized, readError, timedOut };
|
|
48
118
|
}
|