@mrciphersmith/keryx 0.3.51 → 0.3.52
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/cli.js
CHANGED
|
@@ -20094,7 +20094,7 @@ var package_default;
|
|
|
20094
20094
|
var init_package = __esm(() => {
|
|
20095
20095
|
package_default = {
|
|
20096
20096
|
name: "@mrciphersmith/keryx",
|
|
20097
|
-
version: "0.3.
|
|
20097
|
+
version: "0.3.52",
|
|
20098
20098
|
description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
20099
20099
|
private: false,
|
|
20100
20100
|
publishConfig: {
|
|
@@ -109149,7 +109149,7 @@ var init_serve_credential = __esm(() => {
|
|
|
109149
109149
|
});
|
|
109150
109150
|
|
|
109151
109151
|
// src/remote/shell-token.ts
|
|
109152
|
-
import { createHash as createHash62, randomBytes as randomBytes10 } from "crypto";
|
|
109152
|
+
import { createHash as createHash62, createHmac as createHmac5, randomBytes as randomBytes10 } from "crypto";
|
|
109153
109153
|
import { statSync as statSync16 } from "fs";
|
|
109154
109154
|
import path246 from "path";
|
|
109155
109155
|
function shellTokenPath(dir) {
|
|
@@ -109191,16 +109191,59 @@ function mintShellToken(dir) {
|
|
|
109191
109191
|
function readShellToken(dir) {
|
|
109192
109192
|
return readValidated(shellTokenPath(dir));
|
|
109193
109193
|
}
|
|
109194
|
+
function mac(token, parts) {
|
|
109195
|
+
const hmac = createHmac5("sha256", token);
|
|
109196
|
+
for (const part of parts) {
|
|
109197
|
+
const bytes = Buffer.from(part, "utf8");
|
|
109198
|
+
hmac.update(`${bytes.byteLength}:`);
|
|
109199
|
+
hmac.update(bytes);
|
|
109200
|
+
}
|
|
109201
|
+
return hmac.digest("base64url");
|
|
109202
|
+
}
|
|
109203
|
+
function macEqual(a, b) {
|
|
109204
|
+
return constantTimeEqual(createHash62("sha256").update(a, "utf8").digest(), createHash62("sha256").update(b, "utf8").digest());
|
|
109205
|
+
}
|
|
109206
|
+
function shellRequestCredential(token) {
|
|
109207
|
+
const nonce = randomBytes10(18).toString("base64url");
|
|
109208
|
+
return { nonce, bearer: `${DERIVED_BEARER_PREFIX}${nonce}.${mac(token, [AUTH_DOMAIN, nonce])}` };
|
|
109209
|
+
}
|
|
109210
|
+
function derivedBearerNonce(presented) {
|
|
109211
|
+
if (!presented.startsWith(DERIVED_BEARER_PREFIX)) {
|
|
109212
|
+
return;
|
|
109213
|
+
}
|
|
109214
|
+
const [nonce, presentedMac, ...rest] = presented.slice(DERIVED_BEARER_PREFIX.length).split(".");
|
|
109215
|
+
if (rest.length > 0 || nonce === undefined || presentedMac === undefined || !NONCE_SHAPE.test(nonce) || !MAC_SHAPE.test(presentedMac)) {
|
|
109216
|
+
return;
|
|
109217
|
+
}
|
|
109218
|
+
return nonce;
|
|
109219
|
+
}
|
|
109220
|
+
function serveResponseProof(token, nonce, route, status, body) {
|
|
109221
|
+
return mac(token, [PROOF_DOMAIN, nonce, route, String(status), body]);
|
|
109222
|
+
}
|
|
109223
|
+
function verifyServeResponseProof(token, nonce, route, status, body, presented) {
|
|
109224
|
+
if (presented === null || !MAC_SHAPE.test(presented)) {
|
|
109225
|
+
return false;
|
|
109226
|
+
}
|
|
109227
|
+
return macEqual(serveResponseProof(token, nonce, route, status, body), presented);
|
|
109228
|
+
}
|
|
109194
109229
|
function createShellTokenVerifier(token) {
|
|
109195
109230
|
const expected = createHash62("sha256").update(token, "utf8").digest();
|
|
109196
|
-
return (presented) =>
|
|
109231
|
+
return (presented) => {
|
|
109232
|
+
const nonce = derivedBearerNonce(presented);
|
|
109233
|
+
if (nonce !== undefined) {
|
|
109234
|
+
return macEqual(presented, `${DERIVED_BEARER_PREFIX}${nonce}.${mac(token, [AUTH_DOMAIN, nonce])}`);
|
|
109235
|
+
}
|
|
109236
|
+
return constantTimeEqual(createHash62("sha256").update(presented, "utf8").digest(), expected);
|
|
109237
|
+
};
|
|
109197
109238
|
}
|
|
109198
|
-
var SHELL_TOKEN_FILE = "shell-token", TOKEN_SHAPE;
|
|
109239
|
+
var SHELL_TOKEN_FILE = "shell-token", TOKEN_SHAPE, SERVE_PROOF_HEADER = "x-keryx-serve-proof", DERIVED_BEARER_PREFIX = "ksp1.", AUTH_DOMAIN = "keryx-shell-auth-v1", PROOF_DOMAIN = "keryx-serve-proof-v1", NONCE_SHAPE, MAC_SHAPE;
|
|
109199
109240
|
var init_shell_token = __esm(() => {
|
|
109200
109241
|
init_config_dir();
|
|
109201
109242
|
init_serve_credential();
|
|
109202
109243
|
init_paths4();
|
|
109203
109244
|
TOKEN_SHAPE = /^[A-Za-z0-9_-]{32,128}$/;
|
|
109245
|
+
NONCE_SHAPE = /^[A-Za-z0-9_-]{22,64}$/;
|
|
109246
|
+
MAC_SHAPE = /^[A-Za-z0-9_-]{43}$/;
|
|
109204
109247
|
});
|
|
109205
109248
|
|
|
109206
109249
|
// src/remote/client.ts
|
|
@@ -109376,6 +109419,7 @@ class RemoteClient {
|
|
|
109376
109419
|
});
|
|
109377
109420
|
}
|
|
109378
109421
|
target(route, query = "") {
|
|
109422
|
+
const token = readShellToken(this.options.dir);
|
|
109379
109423
|
const endpoint = readEndpoint(this.options.dir);
|
|
109380
109424
|
if (!endpoint.ok) {
|
|
109381
109425
|
throw new TransientClientError(endpoint.reason);
|
|
@@ -109386,7 +109430,6 @@ class RemoteClient {
|
|
|
109386
109430
|
if (!this.isAlive(endpoint.value.pid)) {
|
|
109387
109431
|
throw new TransientClientError(`the serve named in the endpoint file (pid ${endpoint.value.pid}) is not running as this user; refusing to send the shell token to whatever listens on that port`);
|
|
109388
109432
|
}
|
|
109389
|
-
const token = readShellToken(this.options.dir);
|
|
109390
109433
|
if (!token.ok) {
|
|
109391
109434
|
throw new TransientClientError(token.reason);
|
|
109392
109435
|
}
|
|
@@ -109394,12 +109437,19 @@ class RemoteClient {
|
|
|
109394
109437
|
}
|
|
109395
109438
|
async post(route, body) {
|
|
109396
109439
|
const { url, token } = this.target(route);
|
|
109397
|
-
|
|
109440
|
+
const { nonce, bearer } = shellRequestCredential(token);
|
|
109441
|
+
const response = await this.fetchImpl(url, {
|
|
109398
109442
|
method: "POST",
|
|
109399
|
-
|
|
109443
|
+
redirect: "manual",
|
|
109444
|
+
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
109400
109445
|
body: JSON.stringify(body),
|
|
109401
109446
|
signal: AbortSignal.any([AbortSignal.timeout(this.requestTimeoutMs), this.lifetime.signal])
|
|
109402
109447
|
});
|
|
109448
|
+
const text = await response.text();
|
|
109449
|
+
if (!verifyServeResponseProof(token, nonce, route, response.status, text, response.headers.get(SERVE_PROOF_HEADER))) {
|
|
109450
|
+
throw new TransientClientError(UNVERIFIED_SERVE_MESSAGE);
|
|
109451
|
+
}
|
|
109452
|
+
return new Response(text, { status: response.status, headers: response.headers });
|
|
109403
109453
|
}
|
|
109404
109454
|
async heartbeat() {
|
|
109405
109455
|
if (this.stopped || this.name === undefined) {
|
|
@@ -109461,11 +109511,18 @@ class RemoteClient {
|
|
|
109461
109511
|
const abort = new AbortController;
|
|
109462
109512
|
this.streamAbort = abort;
|
|
109463
109513
|
const { url, token } = this.target("stream", `?sessionId=${encodeURIComponent(this.options.sessionId)}`);
|
|
109464
|
-
const
|
|
109514
|
+
const { nonce, bearer } = shellRequestCredential(token);
|
|
109515
|
+
const headers = { authorization: `Bearer ${bearer}`, accept: "text/event-stream" };
|
|
109465
109516
|
if (this.lastCompleted > 0) {
|
|
109466
109517
|
headers["last-event-id"] = String(this.lastCompleted);
|
|
109467
109518
|
}
|
|
109468
|
-
const response = await this.fetchImpl(url, { method: "GET", headers, signal: AbortSignal.any([abort.signal, this.lifetime.signal]) });
|
|
109519
|
+
const response = await this.fetchImpl(url, { method: "GET", redirect: "manual", headers, signal: AbortSignal.any([abort.signal, this.lifetime.signal]) });
|
|
109520
|
+
if (!verifyServeResponseProof(token, nonce, "stream", response.status, "", response.headers.get(SERVE_PROOF_HEADER))) {
|
|
109521
|
+
await response.body?.cancel().catch(() => {
|
|
109522
|
+
return;
|
|
109523
|
+
});
|
|
109524
|
+
throw new TransientClientError(UNVERIFIED_SERVE_MESSAGE);
|
|
109525
|
+
}
|
|
109469
109526
|
if (!response.ok || response.body === null) {
|
|
109470
109527
|
await response.text().catch(() => "");
|
|
109471
109528
|
if (response.status === 404) {
|
|
@@ -109619,7 +109676,7 @@ class RemoteClient {
|
|
|
109619
109676
|
this.earlyDecisions.clear();
|
|
109620
109677
|
}
|
|
109621
109678
|
}
|
|
109622
|
-
var FatalClientError, TransientClientError, DEFAULT_BACKOFF_INITIAL_MS = 250, DEFAULT_BACKOFF_MAX_MS = 15000, DEFAULT_REQUEST_TIMEOUT_MS = 1e4, MAX_EARLY_DECISIONS = 32, MAX_COMPLETED_IDS = 256;
|
|
109679
|
+
var UNVERIFIED_SERVE_MESSAGE = "the program answering on keryx serve's port did not prove it is the keryx serve this shell trusts; its answer was ignored. Restart `keryx serve` (an older serve cannot prove itself; update keryx on both sides)", FatalClientError, TransientClientError, DEFAULT_BACKOFF_INITIAL_MS = 250, DEFAULT_BACKOFF_MAX_MS = 15000, DEFAULT_REQUEST_TIMEOUT_MS = 1e4, MAX_EARLY_DECISIONS = 32, MAX_COMPLETED_IDS = 256;
|
|
109623
109680
|
var init_client3 = __esm(() => {
|
|
109624
109681
|
init_serve_config();
|
|
109625
109682
|
init_lease();
|
|
@@ -110205,6 +110262,7 @@ class ChannelsClient {
|
|
|
110205
110262
|
return { ok: true, value: { deleted, remaining, topicsDeleted: hubWasRunning && remaining === 0, erased, message: `${topics} ${gone}` } };
|
|
110206
110263
|
}
|
|
110207
110264
|
target(route) {
|
|
110265
|
+
const token = readShellToken(this.options.dir);
|
|
110208
110266
|
const endpoint = readEndpoint(this.options.dir);
|
|
110209
110267
|
if (!endpoint.ok) {
|
|
110210
110268
|
throw new ServeDown("serve-down", `keryx serve is not running (${endpoint.reason})`);
|
|
@@ -110215,28 +110273,23 @@ class ChannelsClient {
|
|
|
110215
110273
|
if (!this.isAlive(endpoint.value.pid)) {
|
|
110216
110274
|
throw new ServeDown("serve-down", `keryx serve is not running (pid ${endpoint.value.pid} is gone); start it with \`keryx serve\``);
|
|
110217
110275
|
}
|
|
110218
|
-
const token = readShellToken(this.options.dir);
|
|
110219
110276
|
if (!token.ok) {
|
|
110220
110277
|
throw new ServeDown("serve-down", `keryx serve is not accepting the shell yet (${token.reason})`);
|
|
110221
110278
|
}
|
|
110222
110279
|
return { url: `http://${authority(endpoint.value.address, endpoint.value.port)}${channelsRoutePath(route)}`, token: token.value };
|
|
110223
110280
|
}
|
|
110224
|
-
async
|
|
110281
|
+
async send(route) {
|
|
110225
110282
|
const { url, token } = this.target(route);
|
|
110226
|
-
|
|
110227
|
-
|
|
110228
|
-
|
|
110229
|
-
|
|
110283
|
+
const { nonce, bearer } = shellRequestCredential(token);
|
|
110284
|
+
const post = CHANNELS_ROUTE_METHODS[route] === "POST";
|
|
110285
|
+
const response = await this.fetchImpl(url, {
|
|
110286
|
+
method: post ? "POST" : "GET",
|
|
110287
|
+
redirect: "manual",
|
|
110288
|
+
headers: post ? { authorization: `Bearer ${bearer}`, "content-type": "application/json" } : { authorization: `Bearer ${bearer}` },
|
|
110289
|
+
...post ? { body: "{}" } : {},
|
|
110230
110290
|
signal: AbortSignal.timeout(route === "channels-disconnect" ? this.requestTimeoutMs * DISCONNECT_TIMEOUT_FACTOR : this.requestTimeoutMs)
|
|
110231
110291
|
});
|
|
110232
|
-
|
|
110233
|
-
async get(route) {
|
|
110234
|
-
const { url, token } = this.target(route);
|
|
110235
|
-
return this.fetchImpl(url, {
|
|
110236
|
-
method: "GET",
|
|
110237
|
-
headers: { authorization: `Bearer ${token}` },
|
|
110238
|
-
signal: AbortSignal.timeout(this.requestTimeoutMs)
|
|
110239
|
-
});
|
|
110292
|
+
return { response, token, nonce };
|
|
110240
110293
|
}
|
|
110241
110294
|
reachable() {
|
|
110242
110295
|
try {
|
|
@@ -110250,9 +110303,11 @@ class ChannelsClient {
|
|
|
110250
110303
|
}
|
|
110251
110304
|
}
|
|
110252
110305
|
async call(route, guard) {
|
|
110253
|
-
let
|
|
110306
|
+
let sent;
|
|
110307
|
+
let text;
|
|
110254
110308
|
try {
|
|
110255
|
-
|
|
110309
|
+
sent = await this.send(route);
|
|
110310
|
+
text = await sent.response.text();
|
|
110256
110311
|
} catch (error) {
|
|
110257
110312
|
if (error instanceof ServeDown) {
|
|
110258
110313
|
return { ok: false, code: error.code, reason: error.message };
|
|
@@ -110266,9 +110321,13 @@ class ChannelsClient {
|
|
|
110266
110321
|
reason: "keryx serve did not answer in time or dropped the connection, so the outcome is unknown. Check /channels, then retry if needed."
|
|
110267
110322
|
};
|
|
110268
110323
|
}
|
|
110324
|
+
const { response, token, nonce } = sent;
|
|
110325
|
+
if (!verifyServeResponseProof(token, nonce, route, response.status, text, response.headers.get(SERVE_PROOF_HEADER))) {
|
|
110326
|
+
return { ok: false, code: "unverified-serve", reason: UNVERIFIED_SERVE_REASON };
|
|
110327
|
+
}
|
|
110269
110328
|
let parsed;
|
|
110270
110329
|
try {
|
|
110271
|
-
parsed = JSON.parse(
|
|
110330
|
+
parsed = JSON.parse(text);
|
|
110272
110331
|
} catch {
|
|
110273
110332
|
parsed = undefined;
|
|
110274
110333
|
}
|
|
@@ -110314,7 +110373,7 @@ function refusedConnection(error) {
|
|
|
110314
110373
|
function isPairing(body) {
|
|
110315
110374
|
return typeof body.state === "string" && typeof body.expiresAt === "number" && Array.isArray(body.problems);
|
|
110316
110375
|
}
|
|
110317
|
-
var DEFAULT_REQUEST_TIMEOUT_MS2 = 15000, DISCONNECT_TIMEOUT_FACTOR = 8, ServeDown;
|
|
110376
|
+
var DEFAULT_REQUEST_TIMEOUT_MS2 = 15000, UNVERIFIED_SERVE_REASON = "The program answering on keryx serve's port did not prove it is the keryx serve this shell trusts, so its answer was ignored and nothing was written. Restart `keryx serve` (an older serve cannot prove itself; update keryx on both sides).", DISCONNECT_TIMEOUT_FACTOR = 8, ServeDown;
|
|
110318
110377
|
var init_channels_client = __esm(() => {
|
|
110319
110378
|
init_serve_config();
|
|
110320
110379
|
init_config_dir();
|
|
@@ -110688,7 +110747,7 @@ function channelsStatusLines(snapshot) {
|
|
|
110688
110747
|
return [
|
|
110689
110748
|
"Telegram: pairing in progress",
|
|
110690
110749
|
...machine,
|
|
110691
|
-
"Resume shows the one-time code again; Cancel pairing abandons it and erases the token."
|
|
110750
|
+
"Resume shows the one-time code again, or connects a pairing that already finished; Cancel pairing abandons it and erases the token."
|
|
110692
110751
|
];
|
|
110693
110752
|
case "connected":
|
|
110694
110753
|
return [
|
|
@@ -162700,7 +162759,8 @@ async function importProjectSkills(options) {
|
|
|
162700
162759
|
}
|
|
162701
162760
|
const existingFlags = await projectReviewerFlags(options.projectRoot);
|
|
162702
162761
|
const plans = await Promise.all(sources.map((source) => planOne(options, source)));
|
|
162703
|
-
const
|
|
162762
|
+
const refusedPackages = new Set(plans.flatMap((plan) => ("row" in plan) && isRefusedStatus(plan.row.status) ? [plan.row.name] : []));
|
|
162763
|
+
const ruleDecisions = await decideReferencedRules(options, sources.filter((source) => !skippedAsDeprecated(source)).filter((source) => options.force === true || !BUNDLED_NAMES.has(source.name)), refusedPackages);
|
|
162704
162764
|
await refuseUncontainedWrites(label, options.projectRoot, [
|
|
162705
162765
|
...plans.flatMap((plan) => ("write" in plan) ? projectSkillWritePaths(plan.write.module, plan.write.name, "single") : []),
|
|
162706
162766
|
...ruleDecisions.flatMap((decision) => ("placement" in decision) ? [decision.placement.target] : [])
|
|
@@ -162712,7 +162772,7 @@ async function importProjectSkills(options) {
|
|
|
162712
162772
|
addFlagWarnings(imported, sources, existingFlags);
|
|
162713
162773
|
const rules = [];
|
|
162714
162774
|
for (const decision of ruleDecisions) {
|
|
162715
|
-
rules.push("row" in decision ? decision.row : options.dryRun ? unwrittenRow(decision.placement, decision.dryRunReason) : await writePlacedRule(options, decision.placement));
|
|
162775
|
+
rules.push("row" in decision ? decision.row : options.dryRun ? unwrittenRow(decision.placement, [decision.dryRunReason, ...gateNotes(decision.gate, true)].filter(isString2).join(" \u2014 ") || undefined, decision.gate.security) : await writePlacedRule(options, decision.placement, decision.gate));
|
|
162716
162776
|
}
|
|
162717
162777
|
return {
|
|
162718
162778
|
from: options.from,
|
|
@@ -162770,7 +162830,7 @@ function renderImportProjectSkillsMarkdown(result) {
|
|
|
162770
162830
|
`from: ${result.from}`,
|
|
162771
162831
|
`dry-run: ${result.dryRun ? "yes" : "no"}`,
|
|
162772
162832
|
`force: ${result.force ? "yes" : "no"}`,
|
|
162773
|
-
`imported: ${counts.imported} overwritten: ${counts.overwritten} updated: ${counts.updated} skipped: ${counts.skipped} would-import: ${counts["would-import"]} would-overwrite: ${counts["would-overwrite"]}`,
|
|
162833
|
+
`imported: ${counts.imported} overwritten: ${counts.overwritten} updated: ${counts.updated} skipped: ${counts.skipped} would-import: ${counts["would-import"]} would-overwrite: ${counts["would-overwrite"]}${counts.refused > 0 ? ` refused: ${counts.refused}` : ""}${counts["would-refuse"] > 0 ? ` would-refuse: ${counts["would-refuse"]}` : ""}`,
|
|
162774
162834
|
""
|
|
162775
162835
|
];
|
|
162776
162836
|
if (result.dryRun) {
|
|
@@ -162795,6 +162855,9 @@ function renderImportProjectSkillsMarkdown(result) {
|
|
|
162795
162855
|
if (result.imported.some((row) => row.status === "skipped" && row.reason === DEPRECATED_REASON)) {
|
|
162796
162856
|
lines.push("A package whose frontmatter says `deprecated: true` is skipped in a tree import. To import one anyway, pass its own directory as --from.", "");
|
|
162797
162857
|
}
|
|
162858
|
+
if (result.imported.some((row) => isRefusedStatus(row.status)) || result.rules.some((rule) => isRefusedStatus(rule.status))) {
|
|
162859
|
+
lines.push(refusedNote(result), "");
|
|
162860
|
+
}
|
|
162798
162861
|
if (result.rules.length > 0) {
|
|
162799
162862
|
lines.push("## rules the skills cite", "");
|
|
162800
162863
|
for (const rule of result.rules) {
|
|
@@ -162806,10 +162869,10 @@ function renderImportProjectSkillsMarkdown(result) {
|
|
|
162806
162869
|
lines.push("A reviewer that cites `<dir>/<name>.mdc` reads `.metaproject/rules/project/<dir>/<name>.mdc` when that file exists,", "and `.metaproject/rules/<dir>/<name>.mdc` otherwise. `keryx init`, `keryx update` and `keryx skills install` overwrite", "rules/core with keryx's own rules and leave rules/project alone. `keryx review reviewers` lists each such reference", "under `shadowedRules`.", "");
|
|
162807
162870
|
}
|
|
162808
162871
|
}
|
|
162809
|
-
if (result.imported.some((row) => row.module === PROJECT_REVIEWER_MODULE && row
|
|
162872
|
+
if (result.imported.some((row) => row.module === PROJECT_REVIEWER_MODULE && isWrittenRow(row))) {
|
|
162810
162873
|
lines.push(`Reviewers: \`keryx review reviewers\` must list every imported ${PROJECT_REVIEWER_MODULE}/* name. That is the same call review-orchestrator makes.`);
|
|
162811
162874
|
}
|
|
162812
|
-
if (result.imported.some((row) => row.module !== PROJECT_REVIEWER_MODULE && row
|
|
162875
|
+
if (result.imported.some((row) => row.module !== PROJECT_REVIEWER_MODULE && isWrittenRow(row))) {
|
|
162813
162876
|
lines.push("Non-review modules are registered for `keryx skills route`. They are NOT injected into flow-orchestrator's fixed pipeline \u2014 name the skill in a dispatch if you want it there.");
|
|
162814
162877
|
}
|
|
162815
162878
|
return `${lines.join(`
|
|
@@ -162894,7 +162957,7 @@ function soleCarrierWarning(flag, overwritten, remaining) {
|
|
|
162894
162957
|
return `flag ${flag} is dropped by this version of ${overwritten}, which leaves ${remaining} its only carrier: ${flag} now dispatches ${remaining} outright instead of selecting it path-gated.`;
|
|
162895
162958
|
}
|
|
162896
162959
|
function addFlagWarnings(rows, sources, existingFlags) {
|
|
162897
|
-
const isWritten = (row) => row.module === PROJECT_REVIEWER_MODULE && row
|
|
162960
|
+
const isWritten = (row) => row.module === PROJECT_REVIEWER_MODULE && isWrittenRow(row);
|
|
162898
162961
|
const replaced = new Set(rows.filter(isWritten).map((row) => row.name));
|
|
162899
162962
|
const flagsAfter = new Map(existingFlags);
|
|
162900
162963
|
for (const [index, row] of rows.entries()) {
|
|
@@ -163106,6 +163169,110 @@ async function skillPackageDirs(root) {
|
|
|
163106
163169
|
}
|
|
163107
163170
|
return { dirs, tree: true };
|
|
163108
163171
|
}
|
|
163172
|
+
function isRefusedStatus(status) {
|
|
163173
|
+
return status === "refused" || status === "would-refuse";
|
|
163174
|
+
}
|
|
163175
|
+
function refusedNote(result) {
|
|
163176
|
+
const refused = [...result.imported, ...result.rules].filter((row) => isRefusedStatus(row.status));
|
|
163177
|
+
const overridable = refused.some((row) => row.security?.findings.some((finding) => finding.policyId.startsWith("prompt-injection.")));
|
|
163178
|
+
return overridable ? `${REFUSED_NOTE} ${ALLOW_FLAGGED_HINT}` : REFUSED_NOTE;
|
|
163179
|
+
}
|
|
163180
|
+
function exitNonZeroOnRefusal(result) {
|
|
163181
|
+
if ([...result.imported, ...result.rules].some((row) => row.status === "refused")) {
|
|
163182
|
+
process.exitCode = 1;
|
|
163183
|
+
}
|
|
163184
|
+
}
|
|
163185
|
+
function isWrittenRow(row) {
|
|
163186
|
+
return row.status !== "skipped" && !isRefusedStatus(row.status);
|
|
163187
|
+
}
|
|
163188
|
+
async function gateText(projectRoot, text, target, allowFlagged) {
|
|
163189
|
+
const guard = await guardOutput({
|
|
163190
|
+
cwd: projectRoot,
|
|
163191
|
+
content: text,
|
|
163192
|
+
target: "skill",
|
|
163193
|
+
source: "untrusted-external",
|
|
163194
|
+
path: target
|
|
163195
|
+
});
|
|
163196
|
+
let findings = guard.decision.findings.map(({ policyId, category, action }) => ({ policyId, category, action }));
|
|
163197
|
+
let action = guard.decision.action;
|
|
163198
|
+
let warning = formatGuardWarning(guard.decision);
|
|
163199
|
+
if (guard.allowed && !await isSecurityEnabled(projectRoot)) {
|
|
163200
|
+
const own = detectInjection(text);
|
|
163201
|
+
if (own.length > 0) {
|
|
163202
|
+
findings = own.map(({ policyId, category }) => ({ policyId, category, action: "warn" }));
|
|
163203
|
+
action = "warn";
|
|
163204
|
+
warning = `[security] the security module is disabled, so the import ran its own injection check: ${own.length} finding(s) (prompt-injection:${own.length})`;
|
|
163205
|
+
}
|
|
163206
|
+
}
|
|
163207
|
+
const summary = findings;
|
|
163208
|
+
const security = (redacted, refused) => ({
|
|
163209
|
+
action,
|
|
163210
|
+
findings: summary,
|
|
163211
|
+
redacted,
|
|
163212
|
+
refused
|
|
163213
|
+
});
|
|
163214
|
+
const injection = findings.some((finding) => finding.policyId.startsWith("prompt-injection."));
|
|
163215
|
+
const blockedByMode = !guard.allowed && findings.length > 0;
|
|
163216
|
+
if (blockedByMode || injection && !allowFlagged) {
|
|
163217
|
+
return { kind: "refuse", reason: warning ?? guard.reason ?? "security gate blocked", security: security(false, true) };
|
|
163218
|
+
}
|
|
163219
|
+
const output = prepareOutputForPersistence(guard, text);
|
|
163220
|
+
if (!output.allowed)
|
|
163221
|
+
return { kind: "blocked", reason: output.reason };
|
|
163222
|
+
const redacted = !output.bytesPreserved;
|
|
163223
|
+
return {
|
|
163224
|
+
kind: "write",
|
|
163225
|
+
content: output.content,
|
|
163226
|
+
warning,
|
|
163227
|
+
overridden: injection,
|
|
163228
|
+
redacted,
|
|
163229
|
+
...findings.length > 0 || redacted ? { security: security(redacted, false) } : {}
|
|
163230
|
+
};
|
|
163231
|
+
}
|
|
163232
|
+
function gateRowParts(gate, dryRun) {
|
|
163233
|
+
const detail = gate.warning !== null ? ` (${gate.warning})` : "";
|
|
163234
|
+
const security = gate.security !== undefined ? { security: gate.security } : {};
|
|
163235
|
+
if (gate.overridden) {
|
|
163236
|
+
const redacted = gate.redacted ? ", redacted" : "";
|
|
163237
|
+
return {
|
|
163238
|
+
reason: `flagged by the security gate${detail}${redacted}, ${dryRun ? "would be written" : "written"} because --allow-flagged`,
|
|
163239
|
+
...security
|
|
163240
|
+
};
|
|
163241
|
+
}
|
|
163242
|
+
if (gate.redacted) {
|
|
163243
|
+
return { reason: `${dryRun ? "would be redacted" : "redacted"} by the security gate${detail}`, ...security };
|
|
163244
|
+
}
|
|
163245
|
+
if (gate.warning !== null) {
|
|
163246
|
+
return { reason: `flagged by the security gate${detail}`, ...security };
|
|
163247
|
+
}
|
|
163248
|
+
return security;
|
|
163249
|
+
}
|
|
163250
|
+
function gateNotes(gate, dryRun) {
|
|
163251
|
+
return [gateRowParts(gate, dryRun).reason].filter(isString2);
|
|
163252
|
+
}
|
|
163253
|
+
function plannedSkillText(content, header) {
|
|
163254
|
+
const version = parseSkillFrontmatter(content).metadataVersion ?? "0.1.0";
|
|
163255
|
+
return stampImportHeader(content, [
|
|
163256
|
+
`Version: ${version}`,
|
|
163257
|
+
`Target: ${header.name}`,
|
|
163258
|
+
`Module: ${header.module}`,
|
|
163259
|
+
`Origin: ${header.origin.trim()}`,
|
|
163260
|
+
`Origin Hash: ${hashOriginContent(content)}`,
|
|
163261
|
+
`Imported At: ${new Date().toISOString()}`,
|
|
163262
|
+
"Status: active",
|
|
163263
|
+
"Last Verified: never",
|
|
163264
|
+
""
|
|
163265
|
+
].join(`
|
|
163266
|
+
`));
|
|
163267
|
+
}
|
|
163268
|
+
function refusedSkillRow(base, verdict, dryRun) {
|
|
163269
|
+
return {
|
|
163270
|
+
...base,
|
|
163271
|
+
status: dryRun ? "would-refuse" : "refused",
|
|
163272
|
+
reason: verdict.kind === "blocked" ? `blocked by the security gate: ${verdict.reason}` : verdict.reason,
|
|
163273
|
+
...verdict.kind === "refuse" ? { security: verdict.security } : {}
|
|
163274
|
+
};
|
|
163275
|
+
}
|
|
163109
163276
|
async function planOne(options, source) {
|
|
163110
163277
|
const dest = projectSkillPackagePath(source.module, source.name);
|
|
163111
163278
|
const destAbs = path320.join(options.projectRoot, dest, "SKILL.md");
|
|
@@ -163148,10 +163315,18 @@ async function planOne(options, source) {
|
|
|
163148
163315
|
}
|
|
163149
163316
|
};
|
|
163150
163317
|
}
|
|
163151
|
-
|
|
163318
|
+
const verdict = await gateText(options.projectRoot, plannedSkillText(source.content, { name: source.name, module: source.module, origin: source.origin }), toPosix(path320.join(dest, "SKILL.md")), options.allowFlagged === true);
|
|
163319
|
+
if (verdict.kind !== "write") {
|
|
163320
|
+
return {
|
|
163321
|
+
row: refusedSkillRow({ name: source.name, module: source.module, path: dest, origin: source.origin }, verdict, options.dryRun === true)
|
|
163322
|
+
};
|
|
163323
|
+
}
|
|
163324
|
+
return { write: source, exists, gate: verdict };
|
|
163152
163325
|
}
|
|
163153
|
-
async function writeOne(options, { write: source, exists }) {
|
|
163326
|
+
async function writeOne(options, { write: source, exists, gate }) {
|
|
163327
|
+
const notes = importNotes(source);
|
|
163154
163328
|
if (options.dryRun) {
|
|
163329
|
+
const parts = gateRowParts(gate, true);
|
|
163155
163330
|
return {
|
|
163156
163331
|
name: source.name,
|
|
163157
163332
|
module: source.module,
|
|
@@ -163159,7 +163334,7 @@ async function writeOne(options, { write: source, exists }) {
|
|
|
163159
163334
|
path: projectSkillPackagePath(source.module, source.name),
|
|
163160
163335
|
origin: source.origin,
|
|
163161
163336
|
wired: wiringNote(source.module),
|
|
163162
|
-
...
|
|
163337
|
+
...mergeNotes(notes, parts)
|
|
163163
163338
|
};
|
|
163164
163339
|
}
|
|
163165
163340
|
const created = await createProjectSkill(options.projectRoot, {
|
|
@@ -163172,7 +163347,7 @@ async function writeOne(options, { write: source, exists }) {
|
|
|
163172
163347
|
...versionOption(source.content),
|
|
163173
163348
|
format: "single"
|
|
163174
163349
|
});
|
|
163175
|
-
await
|
|
163350
|
+
await writeGatedSkill(options.projectRoot, created, gate);
|
|
163176
163351
|
return {
|
|
163177
163352
|
name: source.name,
|
|
163178
163353
|
module: source.module,
|
|
@@ -163180,7 +163355,15 @@ async function writeOne(options, { write: source, exists }) {
|
|
|
163180
163355
|
path: created.skillPath,
|
|
163181
163356
|
origin: source.origin,
|
|
163182
163357
|
wired: wiringNote(source.module),
|
|
163183
|
-
...
|
|
163358
|
+
...mergeNotes(notes, gateRowParts(gate, false))
|
|
163359
|
+
};
|
|
163360
|
+
}
|
|
163361
|
+
function mergeNotes(notes, parts) {
|
|
163362
|
+
return {
|
|
163363
|
+
...notes.pathsSource !== undefined ? { pathsSource: notes.pathsSource } : {},
|
|
163364
|
+
...notes.warnings !== undefined ? { warnings: notes.warnings } : {},
|
|
163365
|
+
...parts.reason !== undefined ? { reason: parts.reason } : {},
|
|
163366
|
+
...parts.security !== undefined ? { security: parts.security } : {}
|
|
163184
163367
|
};
|
|
163185
163368
|
}
|
|
163186
163369
|
async function planUpdate(options, entry) {
|
|
@@ -163209,9 +163392,15 @@ async function planUpdate(options, entry) {
|
|
|
163209
163392
|
content = await readFile163(resolved, "utf8");
|
|
163210
163393
|
origin = portableOriginRef(resolved, options.projectRoot);
|
|
163211
163394
|
}
|
|
163212
|
-
|
|
163395
|
+
const verdict = await gateText(options.projectRoot, plannedSkillText(content, { name: entry.name, module: entry.module, origin }), toPosix(path320.join(entry.path, "SKILL.md")), options.allowFlagged === true);
|
|
163396
|
+
if (verdict.kind !== "write") {
|
|
163397
|
+
return {
|
|
163398
|
+
row: refusedSkillRow({ name: entry.name, module: entry.module, path: entry.path, origin }, verdict, options.dryRun === true)
|
|
163399
|
+
};
|
|
163400
|
+
}
|
|
163401
|
+
return { entry, origin, content, gate: verdict };
|
|
163213
163402
|
}
|
|
163214
|
-
async function writeUpdate(options, { entry, origin, content }) {
|
|
163403
|
+
async function writeUpdate(options, { entry, origin, content, gate }) {
|
|
163215
163404
|
if (options.dryRun) {
|
|
163216
163405
|
return {
|
|
163217
163406
|
name: entry.name,
|
|
@@ -163219,7 +163408,8 @@ async function writeUpdate(options, { entry, origin, content }) {
|
|
|
163219
163408
|
status: "would-overwrite",
|
|
163220
163409
|
path: entry.path,
|
|
163221
163410
|
origin,
|
|
163222
|
-
wired: wiringNote(entry.module)
|
|
163411
|
+
wired: wiringNote(entry.module),
|
|
163412
|
+
...mergeNotes({}, gateRowParts(gate, true))
|
|
163223
163413
|
};
|
|
163224
163414
|
}
|
|
163225
163415
|
const created = await createProjectSkill(options.projectRoot, {
|
|
@@ -163232,40 +163422,19 @@ async function writeUpdate(options, { entry, origin, content }) {
|
|
|
163232
163422
|
...versionOption(content),
|
|
163233
163423
|
format: "single"
|
|
163234
163424
|
});
|
|
163235
|
-
await
|
|
163425
|
+
await writeGatedSkill(options.projectRoot, created, gate);
|
|
163236
163426
|
return {
|
|
163237
163427
|
name: entry.name,
|
|
163238
163428
|
module: entry.module,
|
|
163239
163429
|
status: "updated",
|
|
163240
163430
|
path: created.skillPath,
|
|
163241
163431
|
origin,
|
|
163242
|
-
wired: wiringNote(entry.module)
|
|
163432
|
+
wired: wiringNote(entry.module),
|
|
163433
|
+
...mergeNotes({}, gateRowParts(gate, false))
|
|
163243
163434
|
};
|
|
163244
163435
|
}
|
|
163245
|
-
async function
|
|
163246
|
-
|
|
163247
|
-
const scaffold = await readFile163(path320.join(projectRoot, relative), "utf8");
|
|
163248
|
-
const header = extractImportHeader(scaffold, parseSkillFrontmatter(source).metadataVersion);
|
|
163249
|
-
const stamped = stampImportHeader(source, header);
|
|
163250
|
-
const guard = await guardOutput({
|
|
163251
|
-
cwd: projectRoot,
|
|
163252
|
-
content: stamped,
|
|
163253
|
-
target: "skill",
|
|
163254
|
-
source: "untrusted-external",
|
|
163255
|
-
path: relative
|
|
163256
|
-
});
|
|
163257
|
-
const output = prepareOutputForPersistence(guard, stamped);
|
|
163258
|
-
if (!output.allowed) {
|
|
163259
|
-
throw new Error(`Project skill blocked by the security gate: ${output.reason}`);
|
|
163260
|
-
}
|
|
163261
|
-
await writeContained(projectRoot, relative, output.content);
|
|
163262
|
-
}
|
|
163263
|
-
function extractImportHeader(scaffold, version) {
|
|
163264
|
-
const lines = scaffold.split(`
|
|
163265
|
-
`).filter((line) => IMPORT_HEADER_LINE.test(line)).map((line) => version && line.startsWith("Version:") ? `Version: ${version}` : line);
|
|
163266
|
-
return lines.length > 0 ? `${lines.join(`
|
|
163267
|
-
`)}
|
|
163268
|
-
` : "";
|
|
163436
|
+
async function writeGatedSkill(projectRoot, created, gate) {
|
|
163437
|
+
await writeContained(projectRoot, toPosix(path320.join(created.skillPath, "SKILL.md")), gate.content);
|
|
163269
163438
|
}
|
|
163270
163439
|
function stampImportHeader(source, header) {
|
|
163271
163440
|
if (!header)
|
|
@@ -163340,8 +163509,9 @@ function collectRuleCitations(sources) {
|
|
|
163340
163509
|
}
|
|
163341
163510
|
return [...byRef.values()].map((citation) => ({ ...citation, citedBy: citation.citedBy.sort() })).sort((a, b) => a.ref.localeCompare(b.ref));
|
|
163342
163511
|
}
|
|
163343
|
-
function unwrittenRow(placement, reason) {
|
|
163512
|
+
function unwrittenRow(placement, reason, security) {
|
|
163344
163513
|
const { ref, citedBy, origin, target } = placement;
|
|
163514
|
+
const gate = security !== undefined ? { security } : {};
|
|
163345
163515
|
if (placement.existing !== undefined) {
|
|
163346
163516
|
return {
|
|
163347
163517
|
ref,
|
|
@@ -163351,17 +163521,43 @@ function unwrittenRow(placement, reason) {
|
|
|
163351
163521
|
existing: placement.existing.path,
|
|
163352
163522
|
target,
|
|
163353
163523
|
written: false,
|
|
163354
|
-
...reason !== undefined ? { reason } : {}
|
|
163524
|
+
...reason !== undefined ? { reason } : {},
|
|
163525
|
+
...gate
|
|
163355
163526
|
};
|
|
163356
163527
|
}
|
|
163357
|
-
return {
|
|
163528
|
+
return {
|
|
163529
|
+
ref,
|
|
163530
|
+
citedBy,
|
|
163531
|
+
status: placement.toProjectSlot ? "would-import-project" : "would-import",
|
|
163532
|
+
origin,
|
|
163533
|
+
target,
|
|
163534
|
+
...reason !== undefined ? { reason } : {},
|
|
163535
|
+
...gate
|
|
163536
|
+
};
|
|
163358
163537
|
}
|
|
163359
|
-
function writtenRow(placement) {
|
|
163538
|
+
function writtenRow(placement, gate) {
|
|
163360
163539
|
const { ref, citedBy, origin, target } = placement;
|
|
163540
|
+
const notes = gateNotes(gate, false).join(" \u2014 ");
|
|
163541
|
+
const extra = {
|
|
163542
|
+
...notes !== "" ? { reason: notes } : {},
|
|
163543
|
+
...gate.security !== undefined ? { security: gate.security } : {}
|
|
163544
|
+
};
|
|
163361
163545
|
if (placement.existing !== undefined) {
|
|
163362
|
-
return { ref, citedBy, status: "differs", origin, existing: placement.existing.path, target, written: true };
|
|
163546
|
+
return { ref, citedBy, status: "differs", origin, existing: placement.existing.path, target, written: true, ...extra };
|
|
163363
163547
|
}
|
|
163364
|
-
return { ref, citedBy, status: placement.toProjectSlot ? "imported-project" : "imported", origin, target };
|
|
163548
|
+
return { ref, citedBy, status: placement.toProjectSlot ? "imported-project" : "imported", origin, target, ...extra };
|
|
163549
|
+
}
|
|
163550
|
+
function refusedRuleRow(placement, verdict, dryRun) {
|
|
163551
|
+
const { ref, citedBy, origin } = placement;
|
|
163552
|
+
return {
|
|
163553
|
+
ref,
|
|
163554
|
+
citedBy,
|
|
163555
|
+
status: dryRun ? "would-refuse" : "refused",
|
|
163556
|
+
origin,
|
|
163557
|
+
...placement.existing !== undefined ? { existing: placement.existing.path } : {},
|
|
163558
|
+
reason: verdict.reason,
|
|
163559
|
+
security: verdict.security
|
|
163560
|
+
};
|
|
163365
163561
|
}
|
|
163366
163562
|
async function decideRule(options, citation, bundledRules) {
|
|
163367
163563
|
const base = { ref: citation.ref, citedBy: citation.citedBy };
|
|
@@ -163410,32 +163606,35 @@ async function decideRule(options, citation, bundledRules) {
|
|
|
163410
163606
|
if (replacesProjectCopy && options.force !== true) {
|
|
163411
163607
|
return { row: unwrittenRow(placement, "left as it is; pass --force to replace it with the overlay's") };
|
|
163412
163608
|
}
|
|
163413
|
-
|
|
163414
|
-
|
|
163415
|
-
|
|
163416
|
-
const { ref, citedBy, origin, target } = placement;
|
|
163417
|
-
const guard = await guardOutput({
|
|
163418
|
-
cwd: options.projectRoot,
|
|
163419
|
-
content: placement.content,
|
|
163420
|
-
target: "skill",
|
|
163421
|
-
source: "untrusted-external",
|
|
163422
|
-
path: target
|
|
163423
|
-
});
|
|
163424
|
-
const output = prepareOutputForPersistence(guard, placement.content);
|
|
163425
|
-
if (!output.allowed) {
|
|
163426
|
-
const reason = `blocked by the security gate: ${output.reason}`;
|
|
163427
|
-
return placement.existing !== undefined ? unwrittenRow(placement, reason) : { ref, citedBy, status: "unresolved", origin, reason };
|
|
163609
|
+
const verdict = await gateText(options.projectRoot, content, target, options.allowFlagged === true);
|
|
163610
|
+
if (verdict.kind === "refuse") {
|
|
163611
|
+
return { row: refusedRuleRow(placement, verdict, options.dryRun === true) };
|
|
163428
163612
|
}
|
|
163429
|
-
if (
|
|
163430
|
-
|
|
163613
|
+
if (verdict.kind === "blocked") {
|
|
163614
|
+
const reason = `blocked by the security gate: ${verdict.reason}`;
|
|
163615
|
+
return {
|
|
163616
|
+
row: placement.existing !== undefined ? unwrittenRow(placement, reason) : { ...base, status: "unresolved", origin, reason }
|
|
163617
|
+
};
|
|
163618
|
+
}
|
|
163619
|
+
if (placement.existing?.content === verdict.content) {
|
|
163620
|
+
return { row: { ...base, status: "present", origin, existing: placement.existing.path } };
|
|
163431
163621
|
}
|
|
163432
|
-
|
|
163433
|
-
return writtenRow(placement);
|
|
163622
|
+
return replacesProjectCopy ? { placement, gate: verdict, dryRunReason: "would be replaced with the overlay's (--force)" } : { placement, gate: verdict };
|
|
163434
163623
|
}
|
|
163435
|
-
async function
|
|
163624
|
+
async function writePlacedRule(options, placement, gate) {
|
|
163625
|
+
await writeContained(options.projectRoot, placement.target, gate.content);
|
|
163626
|
+
return writtenRow(placement, gate);
|
|
163627
|
+
}
|
|
163628
|
+
async function decideReferencedRules(options, sources, refusedPackages) {
|
|
163436
163629
|
const bundledRules = bundledRulesSourcePath();
|
|
163437
163630
|
const decisions = [];
|
|
163438
163631
|
for (const citation of collectRuleCitations(sources)) {
|
|
163632
|
+
if (citation.citedBy.every((name) => refusedPackages.has(name))) {
|
|
163633
|
+
decisions.push({
|
|
163634
|
+
row: { ref: citation.ref, citedBy: citation.citedBy, status: "skipped", reason: "cited only by a refused package" }
|
|
163635
|
+
});
|
|
163636
|
+
continue;
|
|
163637
|
+
}
|
|
163439
163638
|
decisions.push(await decideRule(options, citation, bundledRules));
|
|
163440
163639
|
}
|
|
163441
163640
|
return decisions;
|
|
@@ -163495,7 +163694,9 @@ function countByStatus(rows) {
|
|
|
163495
163694
|
skipped: 0,
|
|
163496
163695
|
"would-import": 0,
|
|
163497
163696
|
"would-overwrite": 0,
|
|
163498
|
-
updated: 0
|
|
163697
|
+
updated: 0,
|
|
163698
|
+
refused: 0,
|
|
163699
|
+
"would-refuse": 0
|
|
163499
163700
|
};
|
|
163500
163701
|
for (const row of rows) {
|
|
163501
163702
|
counts[row.status] += 1;
|
|
@@ -163521,13 +163722,15 @@ async function runSkillsImportCommand(args) {
|
|
|
163521
163722
|
...skillName !== undefined ? { name: skillName } : {},
|
|
163522
163723
|
only: onlyOption(args, "keryx skills import"),
|
|
163523
163724
|
dryRun: args.includes("--dry-run"),
|
|
163524
|
-
force: args.includes("--force")
|
|
163725
|
+
force: args.includes("--force"),
|
|
163726
|
+
allowFlagged: args.includes(ALLOW_FLAGGED_FLAG)
|
|
163525
163727
|
});
|
|
163526
163728
|
if (args.includes("--json")) {
|
|
163527
163729
|
console.log(JSON.stringify(result, null, 2));
|
|
163528
|
-
|
|
163730
|
+
} else {
|
|
163731
|
+
console.log(renderImportProjectSkillsMarkdown(result));
|
|
163529
163732
|
}
|
|
163530
|
-
|
|
163733
|
+
exitNonZeroOnRefusal(result);
|
|
163531
163734
|
}
|
|
163532
163735
|
async function runSkillsUpdateCommand(args) {
|
|
163533
163736
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -163541,13 +163744,15 @@ async function runSkillsUpdateCommand(args) {
|
|
|
163541
163744
|
...positional[0] !== undefined ? { skill: positional[0] } : {},
|
|
163542
163745
|
all: args.includes("--all"),
|
|
163543
163746
|
...from !== undefined ? { from } : {},
|
|
163544
|
-
dryRun: args.includes("--dry-run")
|
|
163747
|
+
dryRun: args.includes("--dry-run"),
|
|
163748
|
+
allowFlagged: args.includes(ALLOW_FLAGGED_FLAG)
|
|
163545
163749
|
});
|
|
163546
163750
|
if (args.includes("--json")) {
|
|
163547
163751
|
console.log(JSON.stringify(result, null, 2));
|
|
163548
|
-
|
|
163752
|
+
} else {
|
|
163753
|
+
console.log(renderImportProjectSkillsMarkdown(result));
|
|
163549
163754
|
}
|
|
163550
|
-
|
|
163755
|
+
exitNonZeroOnRefusal(result);
|
|
163551
163756
|
}
|
|
163552
163757
|
function printSkillsImportHelp() {
|
|
163553
163758
|
console.log(`keryx skills import
|
|
@@ -163558,7 +163763,7 @@ detectable.
|
|
|
163558
163763
|
|
|
163559
163764
|
Usage:
|
|
163560
163765
|
keryx skills import --from <dir|SKILL.md|https-url> [--module <module>] [--name <name>]
|
|
163561
|
-
[--only <glob>]... [--dry-run] [--force] [--json]
|
|
163766
|
+
[--only <glob>]... [--dry-run] [--force] [--allow-flagged] [--json]
|
|
163562
163767
|
|
|
163563
163768
|
--from:
|
|
163564
163769
|
a skill package directory, a SKILL.md file, a parent tree that contains
|
|
@@ -163592,6 +163797,8 @@ skipped. Pass its own directory as --from to import it anyway.
|
|
|
163592
163797
|
|
|
163593
163798
|
A name that collides with a bundled keryx skill is skipped unless --force.
|
|
163594
163799
|
|
|
163800
|
+
${SECURITY_GATE_HELP}
|
|
163801
|
+
|
|
163595
163802
|
A package landing in module \`review\` is imported with a warning for each of:
|
|
163596
163803
|
- neither \`metadata.paths\` nor a glob in its description (a file path named
|
|
163597
163804
|
without a glob is not a trigger): it is dispatched on every round.
|
|
@@ -163639,8 +163846,10 @@ function printSkillsUpdateHelp() {
|
|
|
163639
163846
|
Re-read a project-skill's Origin and overwrite SKILL.md when the source moved on.
|
|
163640
163847
|
|
|
163641
163848
|
Usage:
|
|
163642
|
-
keryx skills update <module>/<name> [--from <new-origin>] [--dry-run] [--json]
|
|
163643
|
-
keryx skills update --all [--dry-run] [--json]
|
|
163849
|
+
keryx skills update <module>/<name> [--from <new-origin>] [--dry-run] [--allow-flagged] [--json]
|
|
163850
|
+
keryx skills update --all [--dry-run] [--allow-flagged] [--json]
|
|
163851
|
+
|
|
163852
|
+
${SECURITY_GATE_HELP}
|
|
163644
163853
|
|
|
163645
163854
|
Before anything is written, every destination is checked for a symlink on the
|
|
163646
163855
|
way that resolves outside the project, as \`keryx skills import\` does. One is
|
|
@@ -163654,7 +163863,19 @@ Examples:
|
|
|
163654
163863
|
var BUNDLED_NAMES, PATHS_NONE_WARNING = 'paths: none \u2014 dispatched on every round. Declare `metadata.paths: "<glob>, <glob>"` in its frontmatter to gate it on the diff.', DEFAULT_FETCHER = async (url) => {
|
|
163655
163864
|
const response = await fetch(url);
|
|
163656
163865
|
return { ok: response.ok, status: response.status, text: await response.text() };
|
|
163657
|
-
}, DEPRECATED_REASON = "deprecated", KNOWN_CATEGORIES, IMPORT_HEADER_LABELS, IMPORT_HEADER_LINE, BYTE_ORDER_MARK = 65279
|
|
163866
|
+
}, DEPRECATED_REASON = "deprecated", KNOWN_CATEGORIES, isString2 = (value) => value !== undefined, REFUSED_NOTE = "A refused file was not written.", ALLOW_FLAGGED_HINT = "A prompt-injection refusal can be overridden after you read the file: run the command again with --allow-flagged.", IMPORT_HEADER_LABELS, IMPORT_HEADER_LINE, BYTE_ORDER_MARK = 65279, ALLOW_FLAGGED_FLAG = "--allow-flagged", SECURITY_GATE_HELP = `Security gate: a SKILL.md or rule is written as the gate returns it, so a secret is
|
|
163867
|
+
masked and the row says it was redacted, and any other finding is reported on the
|
|
163868
|
+
row. Text with a prompt-injection finding is refused in every security mode: the
|
|
163869
|
+
row is \`refused\` (\`would-refuse\` on a dry run) with the gate's own summary,
|
|
163870
|
+
nothing is written, and the other packages and rules go on. After reading the
|
|
163871
|
+
file, --allow-flagged writes it anyway and the row says it was flagged and written
|
|
163872
|
+
because of the flag. It does not override a block by the project's security mode:
|
|
163873
|
+
under mode enforced a secret is refused either way. With the security module
|
|
163874
|
+
disabled the import runs the injection check itself, so injection text is refused
|
|
163875
|
+
there too (the row says the module is disabled); a secret is still masked. A rule
|
|
163876
|
+
cited only by refused packages is not written (\`skipped \u2014 cited only by a refused
|
|
163877
|
+
package\`). A real run that refused anything exits 1 after printing every row; a
|
|
163878
|
+
dry run (\`would-refuse\`) exits 0. --json rows carry a \`security\` object.`;
|
|
163658
163879
|
var init_import_skills = __esm(() => {
|
|
163659
163880
|
init_args();
|
|
163660
163881
|
init_contained_write();
|
|
@@ -163666,6 +163887,7 @@ var init_import_skills = __esm(() => {
|
|
|
163666
163887
|
init_project_skills();
|
|
163667
163888
|
init_project_reviewers();
|
|
163668
163889
|
init_reviewer_triggers();
|
|
163890
|
+
init_injection();
|
|
163669
163891
|
init_guard2();
|
|
163670
163892
|
BUNDLED_NAMES = new Set(BUNDLED_GDSKILLS.map((entry) => entry.name));
|
|
163671
163893
|
KNOWN_CATEGORIES = new Set(["review", "quality", "orchestration", "planning", "platform", "core"]);
|
|
@@ -167565,7 +167787,8 @@ async function importOverlayReviewers(options) {
|
|
|
167565
167787
|
commandLabel: REVIEW_IMPORT_LABEL,
|
|
167566
167788
|
...options.only !== undefined ? { only: options.only } : {},
|
|
167567
167789
|
...options.dryRun !== undefined ? { dryRun: options.dryRun } : {},
|
|
167568
|
-
...options.force !== undefined ? { force: options.force } : {}
|
|
167790
|
+
...options.force !== undefined ? { force: options.force } : {},
|
|
167791
|
+
...options.allowFlagged !== undefined ? { allowFlagged: options.allowFlagged } : {}
|
|
167569
167792
|
});
|
|
167570
167793
|
}
|
|
167571
167794
|
async function runImportReviewers(args) {
|
|
@@ -167583,13 +167806,15 @@ async function runImportReviewers(args) {
|
|
|
167583
167806
|
from,
|
|
167584
167807
|
only: onlyOption(args, REVIEW_IMPORT_LABEL),
|
|
167585
167808
|
dryRun: args.includes("--dry-run"),
|
|
167586
|
-
force: args.includes("--force")
|
|
167809
|
+
force: args.includes("--force"),
|
|
167810
|
+
allowFlagged: args.includes(ALLOW_FLAGGED_FLAG)
|
|
167587
167811
|
});
|
|
167588
167812
|
if (args.includes("--json")) {
|
|
167589
167813
|
console.log(JSON.stringify({ ...result, inventory: await collectReviewers(process.cwd()) }, null, 2));
|
|
167590
|
-
|
|
167814
|
+
} else {
|
|
167815
|
+
console.log(renderImportProjectSkillsMarkdown(result));
|
|
167591
167816
|
}
|
|
167592
|
-
|
|
167817
|
+
exitNonZeroOnRefusal(result);
|
|
167593
167818
|
}
|
|
167594
167819
|
function printImportHelp() {
|
|
167595
167820
|
console.log(`keryx review import
|
|
@@ -167601,7 +167826,7 @@ The review-shaped spelling of:
|
|
|
167601
167826
|
Same importer, same rules; the module is implied.
|
|
167602
167827
|
|
|
167603
167828
|
Usage:
|
|
167604
|
-
keryx review import --from <package-dir|tree> [--only <glob>]... [--dry-run] [--force] [--json]
|
|
167829
|
+
keryx review import --from <package-dir|tree> [--only <glob>]... [--dry-run] [--force] [--allow-flagged] [--json]
|
|
167605
167830
|
|
|
167606
167831
|
--from:
|
|
167607
167832
|
one reviewer package directory, or a tree that holds several (a directory of
|
|
@@ -167623,6 +167848,8 @@ In a tree import a package whose frontmatter says \`deprecated: true\` is
|
|
|
167623
167848
|
skipped; pass its own directory as --from to import it anyway. A name that
|
|
167624
167849
|
collides with a bundled keryx skill is skipped unless --force.
|
|
167625
167850
|
|
|
167851
|
+
${SECURITY_GATE_HELP}
|
|
167852
|
+
|
|
167626
167853
|
A reviewer is imported with a warning for each of:
|
|
167627
167854
|
- neither \`metadata.paths\` nor a glob in its description (a file path named
|
|
167628
167855
|
without a glob is not a trigger): it is dispatched on every round.
|
|
@@ -172033,7 +172260,7 @@ var init_review = __esm(() => {
|
|
|
172033
172260
|
COMPLETE_FLAGS = ["--finding", "--disposition", "--evidence"];
|
|
172034
172261
|
STACK_FLAGS = ["--json"];
|
|
172035
172262
|
REVIEWERS_FLAGS = ["--json"];
|
|
172036
|
-
IMPORT_FLAGS = ["--from", "--only", "--dry-run", "--force", "--json", "--help"];
|
|
172263
|
+
IMPORT_FLAGS = ["--from", "--only", "--dry-run", "--force", "--allow-flagged", "--json", "--help"];
|
|
172037
172264
|
SESSION_SOURCE_LABELS = {
|
|
172038
172265
|
flags: "flags (--session-provider/--session-model)",
|
|
172039
172266
|
env: "environment (KERYX_SESSION_PROVIDER/KERYX_SESSION_MODEL)",
|
|
@@ -187025,6 +187252,28 @@ class RemoteHttpSurface {
|
|
|
187025
187252
|
return this.options.verifyShellToken(presented);
|
|
187026
187253
|
}
|
|
187027
187254
|
async handle(route, request, io) {
|
|
187255
|
+
const response = await this.dispatch(route, request, io);
|
|
187256
|
+
const authorization = request.headers.get("authorization") ?? "";
|
|
187257
|
+
const nonce = /^Bearer /i.test(authorization) ? derivedBearerNonce(authorization.slice(7).trim()) : undefined;
|
|
187258
|
+
if (nonce === undefined || this.options.proveResponse === undefined) {
|
|
187259
|
+
return response;
|
|
187260
|
+
}
|
|
187261
|
+
if (route === "stream") {
|
|
187262
|
+
const proof = this.options.proveResponse(nonce, route, response.status, "");
|
|
187263
|
+
if (proof !== undefined) {
|
|
187264
|
+
response.headers.set(SERVE_PROOF_HEADER, proof);
|
|
187265
|
+
}
|
|
187266
|
+
return response;
|
|
187267
|
+
}
|
|
187268
|
+
const body = await response.text();
|
|
187269
|
+
const proof = this.options.proveResponse(nonce, route, response.status, body);
|
|
187270
|
+
const headers = new Headers(response.headers);
|
|
187271
|
+
if (proof !== undefined) {
|
|
187272
|
+
headers.set(SERVE_PROOF_HEADER, proof);
|
|
187273
|
+
}
|
|
187274
|
+
return new Response(body, { status: response.status, headers });
|
|
187275
|
+
}
|
|
187276
|
+
async dispatch(route, request, io) {
|
|
187028
187277
|
if (isChannelsRoute(route)) {
|
|
187029
187278
|
if (this.closed || this.channels === undefined) {
|
|
187030
187279
|
return fail4(503, "remote-unavailable", "channels are not available in this serve");
|
|
@@ -187471,6 +187720,7 @@ var init_http_surface = __esm(() => {
|
|
|
187471
187720
|
init_service2();
|
|
187472
187721
|
init_hub();
|
|
187473
187722
|
init_protocol2();
|
|
187723
|
+
init_shell_token();
|
|
187474
187724
|
CONTROL_CHARS2 = /[\u0000-\u001f\u007f]/;
|
|
187475
187725
|
JSON_HEADERS2 = { "content-type": "application/json; charset=utf-8" };
|
|
187476
187726
|
});
|
|
@@ -187499,6 +187749,7 @@ class Pairing {
|
|
|
187499
187749
|
expiresAt;
|
|
187500
187750
|
ttlMs;
|
|
187501
187751
|
earlyGroups = new Map;
|
|
187752
|
+
earlyMigrations = [];
|
|
187502
187753
|
userId;
|
|
187503
187754
|
chatId;
|
|
187504
187755
|
chatTitle;
|
|
@@ -187510,6 +187761,7 @@ class Pairing {
|
|
|
187510
187761
|
timers;
|
|
187511
187762
|
expiryTimer;
|
|
187512
187763
|
finished = false;
|
|
187764
|
+
begun = false;
|
|
187513
187765
|
queue = Promise.resolve();
|
|
187514
187766
|
constructor(options, bot) {
|
|
187515
187767
|
this.options = options;
|
|
@@ -187529,6 +187781,13 @@ class Pairing {
|
|
|
187529
187781
|
});
|
|
187530
187782
|
}
|
|
187531
187783
|
static async open(options) {
|
|
187784
|
+
const prepared = await Pairing.prepare(options);
|
|
187785
|
+
if (prepared.ok) {
|
|
187786
|
+
prepared.pairing.begin();
|
|
187787
|
+
}
|
|
187788
|
+
return prepared;
|
|
187789
|
+
}
|
|
187790
|
+
static async prepare(options) {
|
|
187532
187791
|
let bot;
|
|
187533
187792
|
try {
|
|
187534
187793
|
bot = await options.api.getMe();
|
|
@@ -187538,11 +187797,14 @@ class Pairing {
|
|
|
187538
187797
|
}
|
|
187539
187798
|
return { ok: false, kind: "unreachable", reason: `could not reach Telegram: ${describe3(error)}` };
|
|
187540
187799
|
}
|
|
187541
|
-
|
|
187542
|
-
pairing.begin();
|
|
187543
|
-
return { ok: true, pairing };
|
|
187800
|
+
return { ok: true, pairing: new Pairing(options, bot) };
|
|
187544
187801
|
}
|
|
187545
187802
|
begin() {
|
|
187803
|
+
if (this.begun || FINAL.has(this.state)) {
|
|
187804
|
+
return;
|
|
187805
|
+
}
|
|
187806
|
+
this.begun = true;
|
|
187807
|
+
this.expiresAt = this.now() + this.ttlMs;
|
|
187546
187808
|
this.armExpiryTimer();
|
|
187547
187809
|
this.poller.start();
|
|
187548
187810
|
}
|
|
@@ -187572,10 +187834,17 @@ class Pairing {
|
|
|
187572
187834
|
this.expireIfDue();
|
|
187573
187835
|
return FINAL.has(this.state);
|
|
187574
187836
|
}
|
|
187837
|
+
isReady() {
|
|
187838
|
+
this.expireIfDue();
|
|
187839
|
+
return this.state === "ready";
|
|
187840
|
+
}
|
|
187575
187841
|
async recheck() {
|
|
187576
187842
|
if (this.state === "waiting-for-group" && this.candidate !== undefined) {
|
|
187577
|
-
|
|
187578
|
-
|
|
187843
|
+
await this.enqueue(async () => {
|
|
187844
|
+
if (this.state === "waiting-for-group" && this.candidate !== undefined) {
|
|
187845
|
+
await this.inspectGroup(this.candidate);
|
|
187846
|
+
}
|
|
187847
|
+
});
|
|
187579
187848
|
}
|
|
187580
187849
|
}
|
|
187581
187850
|
async cancel() {
|
|
@@ -187639,6 +187908,7 @@ class Pairing {
|
|
|
187639
187908
|
await this.takeCode(update);
|
|
187640
187909
|
} else if (this.state === "waiting-for-group") {
|
|
187641
187910
|
await this.takeGroup(update);
|
|
187911
|
+
await this.takeMigration(update);
|
|
187642
187912
|
}
|
|
187643
187913
|
}
|
|
187644
187914
|
});
|
|
@@ -187664,15 +187934,31 @@ class Pairing {
|
|
|
187664
187934
|
return;
|
|
187665
187935
|
});
|
|
187666
187936
|
const early = [...this.earlyGroups.values()];
|
|
187937
|
+
const earlyMoves = this.earlyMigrations;
|
|
187667
187938
|
this.earlyGroups.clear();
|
|
187939
|
+
this.earlyMigrations = [];
|
|
187668
187940
|
for (const event of early) {
|
|
187669
187941
|
if (this.state !== "waiting-for-group") {
|
|
187670
187942
|
break;
|
|
187671
187943
|
}
|
|
187672
187944
|
await this.takeGroup(event);
|
|
187673
187945
|
}
|
|
187946
|
+
for (const event of earlyMoves) {
|
|
187947
|
+
if (this.state !== "waiting-for-group") {
|
|
187948
|
+
break;
|
|
187949
|
+
}
|
|
187950
|
+
await this.takeMigration(event);
|
|
187951
|
+
}
|
|
187674
187952
|
}
|
|
187675
187953
|
rememberGroupEvent(update) {
|
|
187954
|
+
const message = update.message;
|
|
187955
|
+
if (message !== undefined && (typeof message.migrate_to_chat_id === "number" || typeof message.migrate_from_chat_id === "number")) {
|
|
187956
|
+
this.earlyMigrations.push(update);
|
|
187957
|
+
if (this.earlyMigrations.length > EARLY_GROUP_EVENTS) {
|
|
187958
|
+
this.earlyMigrations.shift();
|
|
187959
|
+
}
|
|
187960
|
+
return;
|
|
187961
|
+
}
|
|
187676
187962
|
const change = update.my_chat_member;
|
|
187677
187963
|
if (change === undefined) {
|
|
187678
187964
|
return;
|
|
@@ -187705,6 +187991,23 @@ class Pairing {
|
|
|
187705
187991
|
}
|
|
187706
187992
|
await this.inspectGroup(change.chat.id);
|
|
187707
187993
|
}
|
|
187994
|
+
async takeMigration(update) {
|
|
187995
|
+
const message = update.message;
|
|
187996
|
+
if (message === undefined || this.candidate === undefined || FINAL.has(this.state)) {
|
|
187997
|
+
return;
|
|
187998
|
+
}
|
|
187999
|
+
let next;
|
|
188000
|
+
if (typeof message.migrate_to_chat_id === "number" && message.chat.id === this.candidate) {
|
|
188001
|
+
next = message.migrate_to_chat_id;
|
|
188002
|
+
} else if (typeof message.migrate_from_chat_id === "number" && message.migrate_from_chat_id === this.candidate) {
|
|
188003
|
+
next = message.chat.id;
|
|
188004
|
+
}
|
|
188005
|
+
if (next === undefined || next === this.candidate) {
|
|
188006
|
+
return;
|
|
188007
|
+
}
|
|
188008
|
+
this.candidate = next;
|
|
188009
|
+
await this.inspectGroup(next);
|
|
188010
|
+
}
|
|
187708
188011
|
async inspectGroup(chatId) {
|
|
187709
188012
|
if (FINAL.has(this.state)) {
|
|
187710
188013
|
return;
|
|
@@ -187800,7 +188103,7 @@ class ChannelsController {
|
|
|
187800
188103
|
if (this.host.hub() !== undefined) {
|
|
187801
188104
|
return { state: "connected" };
|
|
187802
188105
|
}
|
|
187803
|
-
if (this.pairing !== undefined && !this.pairing.isFinal()) {
|
|
188106
|
+
if (this.pairing !== undefined && (!this.pairing.isFinal() || this.pairing.isReady())) {
|
|
187804
188107
|
return { state: "pairing" };
|
|
187805
188108
|
}
|
|
187806
188109
|
if (this.configured()) {
|
|
@@ -187831,12 +188134,11 @@ class ChannelsController {
|
|
|
187831
188134
|
if (this.host.hub() !== undefined) {
|
|
187832
188135
|
return fail4(409, "already-connected", "Telegram is already connected on this machine; disconnect it first.");
|
|
187833
188136
|
}
|
|
187834
|
-
await this.dropPairing();
|
|
187835
188137
|
const api = this.host.openApi();
|
|
187836
188138
|
if (!api.ok) {
|
|
187837
188139
|
return fail4(422, "no-token", api.reason);
|
|
187838
188140
|
}
|
|
187839
|
-
const opened = await Pairing.
|
|
188141
|
+
const opened = await Pairing.prepare({
|
|
187840
188142
|
api: api.api,
|
|
187841
188143
|
...this.options.now === undefined ? {} : { now: this.options.now },
|
|
187842
188144
|
...this.options.pairing
|
|
@@ -187845,9 +188147,13 @@ class ChannelsController {
|
|
|
187845
188147
|
return fail4(opened.kind === "rejected" ? 422 : 502, opened.kind === "rejected" ? "token-rejected" : "telegram-unreachable", opened.reason);
|
|
187846
188148
|
}
|
|
187847
188149
|
if (mine !== this.generation) {
|
|
187848
|
-
await opened.pairing.cancel();
|
|
187849
188150
|
return superseded();
|
|
187850
188151
|
}
|
|
188152
|
+
await this.dropPairing();
|
|
188153
|
+
if (mine !== this.generation) {
|
|
188154
|
+
return superseded();
|
|
188155
|
+
}
|
|
188156
|
+
opened.pairing.begin();
|
|
187851
188157
|
this.pairing = opened.pairing;
|
|
187852
188158
|
return ok2({ ...opened.pairing.snapshot() });
|
|
187853
188159
|
});
|
|
@@ -187872,13 +188178,13 @@ class ChannelsController {
|
|
|
187872
188178
|
reload() {
|
|
187873
188179
|
this.generation += 1;
|
|
187874
188180
|
return this.exclusive(async () => {
|
|
187875
|
-
await this.dropPairing();
|
|
187876
188181
|
const config = loadRemoteConfig(this.host.dir);
|
|
187877
188182
|
const token = loadBotToken(this.host.dir);
|
|
187878
188183
|
if (!config.ok || !token.ok) {
|
|
187879
188184
|
const reason = !config.ok ? config.reason : !token.ok ? token.reason : "";
|
|
187880
188185
|
return fail4(422, "cannot-connect", this.host.hub() === undefined ? reason : `${reason} The running channel was left as it was.`);
|
|
187881
188186
|
}
|
|
188187
|
+
await this.dropPairing();
|
|
187882
188188
|
if (this.host.hub() !== undefined) {
|
|
187883
188189
|
await this.host.stopHub("reloading remote control");
|
|
187884
188190
|
}
|
|
@@ -188052,8 +188358,10 @@ function openRemoteService(options = {}) {
|
|
|
188052
188358
|
});
|
|
188053
188359
|
const machine = options.machine ?? localMachineName();
|
|
188054
188360
|
let verifyShellToken;
|
|
188361
|
+
let shellToken;
|
|
188055
188362
|
const surface = new RemoteHttpSurface({
|
|
188056
188363
|
verifyShellToken: (presented) => verifyShellToken?.(presented) ?? false,
|
|
188364
|
+
proveResponse: (nonce, route, status, body) => shellToken === undefined ? undefined : serveResponseProof(shellToken, nonce, route, status, body),
|
|
188057
188365
|
...options.now === undefined ? {} : { now: options.now },
|
|
188058
188366
|
...options.timers === undefined ? {} : { timers: options.timers },
|
|
188059
188367
|
...options.surface
|
|
@@ -188219,11 +188527,15 @@ function openRemoteService(options = {}) {
|
|
|
188219
188527
|
return refuse(lock.reason);
|
|
188220
188528
|
}
|
|
188221
188529
|
}
|
|
188530
|
+
try {
|
|
188531
|
+
unlinkSync11(endpointPath(options.dir));
|
|
188532
|
+
} catch {}
|
|
188222
188533
|
const token = mintShellToken(options.dir);
|
|
188223
188534
|
if (!token.ok) {
|
|
188224
188535
|
return refuse(token.reason);
|
|
188225
188536
|
}
|
|
188226
188537
|
verifyShellToken = createShellTokenVerifier(token.value);
|
|
188538
|
+
shellToken = token.value;
|
|
188227
188539
|
try {
|
|
188228
188540
|
ensureRemoteDir(options.dir);
|
|
188229
188541
|
const body = { address: endpoint.address, port: endpoint.port, pid: process.pid };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrciphersmith/keryx",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.52",
|
|
4
4
|
"description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -171,9 +171,10 @@ Read three things off it before running for real:
|
|
|
171
171
|
the reviewer will cite a rule the project will not have. `differs` means a
|
|
172
172
|
file already answers that reference with other content.
|
|
173
173
|
|
|
174
|
-
A dry-run
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
A dry-run runs the same security gate a real run does, on the text that would
|
|
175
|
+
be written, so a `would-…` status already says what the gate would do. A file
|
|
176
|
+
the gate would refuse is `would-refuse`, and a file it would change or flag
|
|
177
|
+
carries the reason on its row (section 3, "The security gate on import").
|
|
177
178
|
|
|
178
179
|
### 3. The real run, and what it writes
|
|
179
180
|
|
|
@@ -305,6 +306,107 @@ to overwrite`, and the rule rows are re-resolved — `present` for both files
|
|
|
305
306
|
written above, the second with `the reviewer reads
|
|
306
307
|
.metaproject/rules/project/core/error-handling.mdc`.
|
|
307
308
|
|
|
309
|
+
**The security gate on import.** `keryx skills import`, `keryx review import`
|
|
310
|
+
and `keryx skills update` pass every `SKILL.md` and rule through the security
|
|
311
|
+
gate on the final text, while planning, so a dry run reports it too. The gate
|
|
312
|
+
does not write silently:
|
|
313
|
+
|
|
314
|
+
- A secret is masked in what is written, and the row says so: `redacted by the
|
|
315
|
+
security gate (…)`. On a dry run, `would be redacted by the security gate (…)`.
|
|
316
|
+
- Any other finding follows the project's security mode and is reported on the
|
|
317
|
+
row as `flagged by the security gate (…)`.
|
|
318
|
+
- A **prompt-injection** finding refuses the file in every security mode. The row
|
|
319
|
+
status is `refused` (`would-refuse` on a dry run), the reason is the gate's own
|
|
320
|
+
leak-safe summary (policy ids and counts, never the matched text), nothing is
|
|
321
|
+
written and no scaffold is left. The other packages and rules go on.
|
|
322
|
+
- **With the security module disabled** the guard reports nothing, so the import
|
|
323
|
+
runs the deterministic prompt-injection detector itself and refuses on a match
|
|
324
|
+
exactly as above; the row reason says `the security module is disabled, so the
|
|
325
|
+
import ran its own injection check`. A secret is still masked by the floor
|
|
326
|
+
redaction; clean text imports as usual.
|
|
327
|
+
- A rule cited **only** by refused (`would-refuse`) packages is not written. Its
|
|
328
|
+
row reads `skipped — cited only by a refused package`, on a dry run and a real
|
|
329
|
+
run alike; a rule an imported package also cites is handled as before.
|
|
330
|
+
- **Exit code.** A real run that produced any `refused` row prints every row (text
|
|
331
|
+
or `--json`) and then exits 1. A dry run, whose rows say `would-refuse`, exits 0.
|
|
332
|
+
- The file is gated once, before anything is written, and the text written is the
|
|
333
|
+
text gated: a refusal leaves no scaffold, registry entry or catalog row, and
|
|
334
|
+
never overwrites an installed `SKILL.md` on `--force` or update.
|
|
335
|
+
- `--allow-flagged` writes a prompt-injection-flagged file anyway, after you have
|
|
336
|
+
read it. The row then reads `flagged by the security gate (…), written because
|
|
337
|
+
--allow-flagged` (`would be written because --allow-flagged` on a dry run). It
|
|
338
|
+
never overrides a block by the project's security mode: under mode `enforced`
|
|
339
|
+
a secret is refused with or without the flag.
|
|
340
|
+
|
|
341
|
+
The flag belongs to all three commands: `keryx skills import … [--allow-flagged]`,
|
|
342
|
+
`keryx review import … [--allow-flagged]`, `keryx skills update … [--allow-flagged]`.
|
|
343
|
+
The summary line gains `refused: N` (`would-refuse: N` on a dry run) only when
|
|
344
|
+
N is above zero, and the note under it names `--allow-flagged` only when a refused
|
|
345
|
+
row carries a prompt-injection finding (the flag cannot change any other
|
|
346
|
+
refusal). This is the output for an overlay with two reviewers, one whose
|
|
347
|
+
body tries to override the agent's instructions and asks it to disclose its
|
|
348
|
+
own context, and one that holds the AWS documentation placeholder access key:
|
|
349
|
+
|
|
350
|
+
```
|
|
351
|
+
$ keryx review import --from ./ov --only 'review-acme-*' --dry-run
|
|
352
|
+
# skills import
|
|
353
|
+
|
|
354
|
+
from: ./ov
|
|
355
|
+
dry-run: yes
|
|
356
|
+
force: no
|
|
357
|
+
imported: 0 overwritten: 0 updated: 0 skipped: 0 would-import: 1 would-overwrite: 0 would-refuse: 1
|
|
358
|
+
…
|
|
359
|
+
## packages
|
|
360
|
+
|
|
361
|
+
- review/review-acme-inj: would-refuse — [security] pass: 2 finding(s) (prompt-injection:2)
|
|
362
|
+
- review/review-acme-key: would-import — would be redacted by the security gate ([security] fail: 1 finding(s) (secret:1)) — review-orchestrator will dispatch this after `keryx review reviewers` lists it
|
|
363
|
+
|
|
364
|
+
A refused file was not written. A prompt-injection refusal can be overridden after you read the file: run the command again with --allow-flagged.
|
|
365
|
+
…
|
|
366
|
+
|
|
367
|
+
$ keryx review import --from ./ov --only 'review-acme-*'
|
|
368
|
+
…
|
|
369
|
+
imported: 1 overwritten: 0 updated: 0 skipped: 0 would-import: 0 would-overwrite: 0 refused: 1
|
|
370
|
+
|
|
371
|
+
- review/review-acme-inj: refused — [security] pass: 2 finding(s) (prompt-injection:2)
|
|
372
|
+
- review/review-acme-key: imported — redacted by the security gate ([security] fail: 1 finding(s) (secret:1)) — review-orchestrator will dispatch this after `keryx review reviewers` lists it
|
|
373
|
+
…
|
|
374
|
+
|
|
375
|
+
$ keryx review import --from ./ov --only 'review-acme-inj' --dry-run --allow-flagged
|
|
376
|
+
…
|
|
377
|
+
- review/review-acme-inj: would-import — flagged by the security gate ([security] pass: 2 finding(s) (prompt-injection:2)), would be written because --allow-flagged — review-orchestrator will dispatch this after `keryx review reviewers` lists it
|
|
378
|
+
…
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Row statuses, with the gate in them: `imported`, `overwritten`, `updated`,
|
|
382
|
+
`skipped`, `would-import`, `would-overwrite`, `refused`, `would-refuse`.
|
|
383
|
+
|
|
384
|
+
In `--json` a row carries a `security` object when the gate had something to say
|
|
385
|
+
about it, and none otherwise. The refused row above, trimmed:
|
|
386
|
+
|
|
387
|
+
```json
|
|
388
|
+
{
|
|
389
|
+
"name": "review-acme-inj",
|
|
390
|
+
"module": "review",
|
|
391
|
+
"status": "would-refuse",
|
|
392
|
+
"reason": "[security] pass: 2 finding(s) (prompt-injection:2)",
|
|
393
|
+
"security": {
|
|
394
|
+
"action": "warn",
|
|
395
|
+
"findings": [
|
|
396
|
+
{ "policyId": "prompt-injection.ignore-instructions", "category": "prompt-injection", "action": "warn" },
|
|
397
|
+
…
|
|
398
|
+
],
|
|
399
|
+
"redacted": false,
|
|
400
|
+
"refused": true
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
`security.redacted` is true when the gate changed the bytes that are, or would
|
|
406
|
+
be, written; `security.refused` is true when the file was not (or would not be)
|
|
407
|
+
written. A caller that used to read `status` alone must now treat `refused` and
|
|
408
|
+
`would-refuse` as failures to import that exit 0.
|
|
409
|
+
|
|
308
410
|
### 4. Reading `keryx review reviewers`
|
|
309
411
|
|
|
310
412
|
```
|