@i4ctime/q-ring 0.15.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -1
- package/dist/{chunk-MLBJCPX2.js → chunk-5LFKBZ3Q.js} +62 -7
- package/dist/chunk-5LFKBZ3Q.js.map +1 -0
- package/dist/{chunk-TPPPTL4J.js → chunk-NCM5GHNW.js} +62 -7
- package/dist/chunk-NCM5GHNW.js.map +1 -0
- package/dist/{dashboard-T2UG23KI.js → dashboard-A3GJQCJX.js} +2 -2
- package/dist/{dashboard-TFWCG23R.js → dashboard-R3FWTFFW.js} +2 -2
- package/dist/index.js +285 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +10 -3
- package/dist/mcp.js.map +1 -1
- package/package.json +2 -1
- package/dist/chunk-MLBJCPX2.js.map +0 -1
- package/dist/chunk-TPPPTL4J.js.map +0 -1
- /package/dist/{dashboard-T2UG23KI.js.map → dashboard-A3GJQCJX.js.map} +0 -0
- /package/dist/{dashboard-TFWCG23R.js.map → dashboard-R3FWTFFW.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -38,13 +38,14 @@ import {
|
|
|
38
38
|
removeHook,
|
|
39
39
|
revokeApproval,
|
|
40
40
|
serviceForScope,
|
|
41
|
+
setAuditAgentLabel,
|
|
41
42
|
setSecret,
|
|
42
43
|
tunnelCreate,
|
|
43
44
|
tunnelDestroy,
|
|
44
45
|
tunnelList,
|
|
45
46
|
tunnelRead,
|
|
46
47
|
verifyAuditChain
|
|
47
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-5LFKBZ3Q.js";
|
|
48
49
|
|
|
49
50
|
// src/cli/commands.ts
|
|
50
51
|
import { Command, Help } from "commander";
|
|
@@ -2587,7 +2588,7 @@ function registerToolingCommands(program2) {
|
|
|
2587
2588
|
}
|
|
2588
2589
|
});
|
|
2589
2590
|
program2.command("status").description("Launch the quantum status dashboard in your browser").option("--port <port>", "Port to serve on", "9876").option("--no-open", "Don't auto-open the browser").action(async (cmd) => {
|
|
2590
|
-
const { startDashboardServer } = await import("./dashboard-
|
|
2591
|
+
const { startDashboardServer } = await import("./dashboard-A3GJQCJX.js");
|
|
2591
2592
|
const { exec } = await import("child_process");
|
|
2592
2593
|
const { platform } = await import("os");
|
|
2593
2594
|
const port = Number(cmd.port);
|
|
@@ -4030,6 +4031,285 @@ ${SYMBOLS.check} ${prefix}${c.bold(verb)}: ${c.cyan(result.configPath)} ${c.dim(
|
|
|
4030
4031
|
});
|
|
4031
4032
|
}
|
|
4032
4033
|
|
|
4034
|
+
// src/core/canary.ts
|
|
4035
|
+
var ALPHA_UPPER_NUM = () => generateSecret({ format: "alphanumeric", length: 16 }).toUpperCase();
|
|
4036
|
+
var CANARY_FORMATS = {
|
|
4037
|
+
aws: {
|
|
4038
|
+
name: "aws",
|
|
4039
|
+
description: "AWS access key id (AKIA\u2026)",
|
|
4040
|
+
generate: () => `AKIA${ALPHA_UPPER_NUM()}`
|
|
4041
|
+
},
|
|
4042
|
+
github: {
|
|
4043
|
+
name: "github",
|
|
4044
|
+
description: "GitHub personal access token (ghp_\u2026)",
|
|
4045
|
+
generate: () => generateSecret({ format: "api-key", prefix: "ghp_", length: 36 })
|
|
4046
|
+
},
|
|
4047
|
+
openai: {
|
|
4048
|
+
name: "openai",
|
|
4049
|
+
description: "OpenAI API key (sk-\u2026)",
|
|
4050
|
+
generate: () => generateSecret({ format: "api-key", prefix: "sk-", length: 48 })
|
|
4051
|
+
},
|
|
4052
|
+
anthropic: {
|
|
4053
|
+
name: "anthropic",
|
|
4054
|
+
description: "Anthropic API key (sk-ant-\u2026)",
|
|
4055
|
+
generate: () => generateSecret({ format: "api-key", prefix: "sk-ant-api03-", length: 80 })
|
|
4056
|
+
},
|
|
4057
|
+
stripe: {
|
|
4058
|
+
name: "stripe",
|
|
4059
|
+
description: "Stripe live secret key (sk_live_\u2026)",
|
|
4060
|
+
generate: () => generateSecret({ format: "api-key", prefix: "sk_live_", length: 24 })
|
|
4061
|
+
},
|
|
4062
|
+
generic: {
|
|
4063
|
+
name: "generic",
|
|
4064
|
+
description: "Generic high-entropy API key",
|
|
4065
|
+
generate: () => generateSecret({ format: "api-key", prefix: "qk_", length: 40 })
|
|
4066
|
+
}
|
|
4067
|
+
};
|
|
4068
|
+
var DEFAULT_CANARY_FORMAT = "generic";
|
|
4069
|
+
function plantCanary(key, opts = {}) {
|
|
4070
|
+
const formatName = opts.format ?? DEFAULT_CANARY_FORMAT;
|
|
4071
|
+
const format = CANARY_FORMATS[formatName];
|
|
4072
|
+
if (!format) {
|
|
4073
|
+
throw new Error(
|
|
4074
|
+
`Unknown canary format "${formatName}". Available: ${Object.keys(CANARY_FORMATS).join(", ")}`
|
|
4075
|
+
);
|
|
4076
|
+
}
|
|
4077
|
+
const value = opts.value ?? format.generate();
|
|
4078
|
+
const scope = opts.scope ?? "global";
|
|
4079
|
+
setSecret(key, value, {
|
|
4080
|
+
...opts,
|
|
4081
|
+
scope,
|
|
4082
|
+
canary: true,
|
|
4083
|
+
canaryFormat: formatName,
|
|
4084
|
+
description: opts.value ? "Canary honeytoken (custom value)" : `Canary honeytoken (${format.description})`
|
|
4085
|
+
});
|
|
4086
|
+
return { key, value, format: formatName, scope };
|
|
4087
|
+
}
|
|
4088
|
+
function listCanaries(opts = {}) {
|
|
4089
|
+
const out = [];
|
|
4090
|
+
for (const entry of listSecrets({ ...opts, silent: true })) {
|
|
4091
|
+
const meta = entry.envelope?.meta;
|
|
4092
|
+
if (!meta?.canary) continue;
|
|
4093
|
+
out.push({
|
|
4094
|
+
key: entry.key,
|
|
4095
|
+
scope: entry.scope,
|
|
4096
|
+
format: meta.canaryFormat,
|
|
4097
|
+
plantedAt: meta.createdAt,
|
|
4098
|
+
tripCount: meta.accessCount,
|
|
4099
|
+
lastTrippedAt: meta.lastAccessedAt
|
|
4100
|
+
});
|
|
4101
|
+
}
|
|
4102
|
+
return out;
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
// src/cli/commands/canary.ts
|
|
4106
|
+
function registerCanaryCommands(program2) {
|
|
4107
|
+
const canary = program2.command("canary").description("Honeytokens \u2014 fake credentials that raise the alarm on read");
|
|
4108
|
+
canary.command("plant <key>").description("Plant a fake credential; any read fires a loud alert").option(
|
|
4109
|
+
"-f, --format <format>",
|
|
4110
|
+
`Token shape to imitate (${Object.keys(CANARY_FORMATS).join(", ")})`,
|
|
4111
|
+
DEFAULT_CANARY_FORMAT
|
|
4112
|
+
).option("--value <value>", "Plant this exact value instead of generating one").option("--force", "Overwrite an existing non-canary secret at this key").option("-g, --global", "Global scope (default)").option("-p, --project", "Project scope").option("--team <id>", "Team scope").option("--org <id>", "Org scope").option("--project-path <path>", "Project path (defaults to cwd)").action((key, cmd) => {
|
|
4113
|
+
const opts = buildOpts(cmd);
|
|
4114
|
+
const existing = getEnvelope(key, opts);
|
|
4115
|
+
if (existing && !existing.envelope.meta.canary && !cmd.force) {
|
|
4116
|
+
console.error(
|
|
4117
|
+
c.red(
|
|
4118
|
+
`${SYMBOLS.cross} "${key}" already holds a real secret in ${existing.scope} scope. Use --force to replace it with a canary.`
|
|
4119
|
+
)
|
|
4120
|
+
);
|
|
4121
|
+
process.exit(1);
|
|
4122
|
+
}
|
|
4123
|
+
const result = plantCanary(key, { ...opts, format: cmd.format, value: cmd.value });
|
|
4124
|
+
console.log(
|
|
4125
|
+
`${SYMBOLS.sparkle} ${c.yellow("canary planted")} ${c.bold(result.key)} ${c.dim(`(${result.format}, ${result.scope} scope)`)}`
|
|
4126
|
+
);
|
|
4127
|
+
console.log(c.dim(` value: ${result.value}`));
|
|
4128
|
+
console.log(
|
|
4129
|
+
c.dim(
|
|
4130
|
+
" Reads return this fake value and fire a desktop alert + a 'canary' audit event."
|
|
4131
|
+
)
|
|
4132
|
+
);
|
|
4133
|
+
console.log(
|
|
4134
|
+
c.dim(" Watch trips with: qring canary list \xB7 qring audit --action canary")
|
|
4135
|
+
);
|
|
4136
|
+
});
|
|
4137
|
+
canary.command("list").alias("ls").description("List planted canaries and their trip counts").option("--json", "Output as JSON").option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--team <id>", "Team scope").option("--org <id>", "Org scope").option("--project-path <path>", "Project path (defaults to cwd)").action((cmd) => {
|
|
4138
|
+
const canaries = listCanaries(buildOpts(cmd));
|
|
4139
|
+
if (emitJson(program2, cmd, { canaries })) return;
|
|
4140
|
+
if (canaries.length === 0) {
|
|
4141
|
+
console.log(c.dim("No canaries planted. Plant one: qring canary plant AWS_SECRET_ACCESS_KEY --format aws"));
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
4144
|
+
console.log(c.bold(`
|
|
4145
|
+
${SYMBOLS.eye} Canaries (${canaries.length})
|
|
4146
|
+
`));
|
|
4147
|
+
for (const entry of canaries) {
|
|
4148
|
+
const parts = [c.bold(entry.key)];
|
|
4149
|
+
parts.push(c.dim(`${entry.format ?? "generic"} \xB7 ${entry.scope}`));
|
|
4150
|
+
if (entry.tripCount > 0) {
|
|
4151
|
+
parts.push(c.red(`trips: ${entry.tripCount}`));
|
|
4152
|
+
if (entry.lastTrippedAt) {
|
|
4153
|
+
parts.push(c.red(`last: ${entry.lastTrippedAt}`));
|
|
4154
|
+
}
|
|
4155
|
+
} else {
|
|
4156
|
+
parts.push(c.green("no trips"));
|
|
4157
|
+
}
|
|
4158
|
+
console.log(` ${SYMBOLS.eye} ${parts.join(" ")}`);
|
|
4159
|
+
}
|
|
4160
|
+
console.log();
|
|
4161
|
+
});
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
// src/core/wrap.ts
|
|
4165
|
+
import { randomUUID } from "crypto";
|
|
4166
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4167
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4168
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4169
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4170
|
+
import {
|
|
4171
|
+
ListToolsRequestSchema,
|
|
4172
|
+
CallToolRequestSchema
|
|
4173
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
4174
|
+
function createAirlockServer(downstream, session) {
|
|
4175
|
+
const downstreamInfo = downstream.getServerVersion();
|
|
4176
|
+
const name = downstreamInfo ? `${downstreamInfo.name} (q-ring airlock)` : "q-ring-airlock";
|
|
4177
|
+
const proxy = new Server(
|
|
4178
|
+
{ name, version: PACKAGE_VERSION },
|
|
4179
|
+
{ capabilities: { tools: {} } }
|
|
4180
|
+
);
|
|
4181
|
+
proxy.oninitialized = () => {
|
|
4182
|
+
const info = proxy.getClientVersion();
|
|
4183
|
+
if (info) setAuditAgentLabel(`${info.name}@${info.version}`);
|
|
4184
|
+
};
|
|
4185
|
+
proxy.setRequestHandler(ListToolsRequestSchema, async (request) => {
|
|
4186
|
+
return downstream.listTools(request.params);
|
|
4187
|
+
});
|
|
4188
|
+
proxy.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
4189
|
+
const toolName = request.params.name;
|
|
4190
|
+
logAudit({
|
|
4191
|
+
action: "wrap",
|
|
4192
|
+
source: "mcp",
|
|
4193
|
+
detail: `tool call "${toolName}" \u2192 ${session.label}`,
|
|
4194
|
+
correlationId: session.correlationId
|
|
4195
|
+
});
|
|
4196
|
+
try {
|
|
4197
|
+
return await downstream.callTool(request.params, void 0, {
|
|
4198
|
+
signal: extra.signal
|
|
4199
|
+
});
|
|
4200
|
+
} catch (err) {
|
|
4201
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4202
|
+
logAudit({
|
|
4203
|
+
action: "wrap",
|
|
4204
|
+
source: "mcp",
|
|
4205
|
+
detail: `tool call "${toolName}" failed: ${message}`,
|
|
4206
|
+
correlationId: session.correlationId
|
|
4207
|
+
});
|
|
4208
|
+
return {
|
|
4209
|
+
content: [
|
|
4210
|
+
{ type: "text", text: `airlock: downstream error: ${message}` }
|
|
4211
|
+
],
|
|
4212
|
+
isError: true
|
|
4213
|
+
};
|
|
4214
|
+
}
|
|
4215
|
+
});
|
|
4216
|
+
return proxy;
|
|
4217
|
+
}
|
|
4218
|
+
async function connectDownstream(opts) {
|
|
4219
|
+
const client = new Client({
|
|
4220
|
+
name: "q-ring-airlock",
|
|
4221
|
+
version: PACKAGE_VERSION
|
|
4222
|
+
});
|
|
4223
|
+
const env = opts.inheritEnv ? Object.fromEntries(
|
|
4224
|
+
Object.entries(process.env).filter(([, v]) => v !== void 0)
|
|
4225
|
+
) : void 0;
|
|
4226
|
+
const transport = new StdioClientTransport({
|
|
4227
|
+
command: opts.command,
|
|
4228
|
+
args: opts.args ?? [],
|
|
4229
|
+
env,
|
|
4230
|
+
stderr: "inherit"
|
|
4231
|
+
});
|
|
4232
|
+
await client.connect(transport);
|
|
4233
|
+
return client;
|
|
4234
|
+
}
|
|
4235
|
+
async function runWrap(opts) {
|
|
4236
|
+
const label = opts.label ?? [opts.command, ...opts.args ?? []].join(" ");
|
|
4237
|
+
const session = { label, correlationId: randomUUID() };
|
|
4238
|
+
const downstream = await connectDownstream(opts);
|
|
4239
|
+
const downstreamInfo = downstream.getServerVersion();
|
|
4240
|
+
logAudit({
|
|
4241
|
+
action: "wrap",
|
|
4242
|
+
source: "cli",
|
|
4243
|
+
detail: `airlock session started: ${label}${opts.inheritEnv ? " (env inherited)" : " (env stripped)"}`,
|
|
4244
|
+
correlationId: session.correlationId
|
|
4245
|
+
});
|
|
4246
|
+
console.error(
|
|
4247
|
+
`q-ring airlock: wrapping ${downstreamInfo?.name ?? label} \u2014 env ${opts.inheritEnv ? "inherited" : "stripped"}, tool calls audited (session ${session.correlationId.slice(0, 8)})`
|
|
4248
|
+
);
|
|
4249
|
+
const proxy = createAirlockServer(downstream, session);
|
|
4250
|
+
const transport = new StdioServerTransport();
|
|
4251
|
+
return new Promise((resolve) => {
|
|
4252
|
+
let settled = false;
|
|
4253
|
+
const finish = (code, reason) => {
|
|
4254
|
+
if (settled) return;
|
|
4255
|
+
settled = true;
|
|
4256
|
+
logAudit({
|
|
4257
|
+
action: "wrap",
|
|
4258
|
+
source: "cli",
|
|
4259
|
+
detail: `airlock session ended: ${reason}`,
|
|
4260
|
+
correlationId: session.correlationId
|
|
4261
|
+
});
|
|
4262
|
+
void proxy.close().catch(() => {
|
|
4263
|
+
});
|
|
4264
|
+
void downstream.close().catch(() => {
|
|
4265
|
+
});
|
|
4266
|
+
resolve(code);
|
|
4267
|
+
};
|
|
4268
|
+
downstream.onclose = () => finish(1, "wrapped server exited");
|
|
4269
|
+
transport.onclose = () => finish(0, "host disconnected");
|
|
4270
|
+
transport.onerror = (err) => finish(1, `transport error: ${err.message}`);
|
|
4271
|
+
process.stdin.on("end", () => finish(0, "host disconnected"));
|
|
4272
|
+
process.stdout.on("error", (err) => {
|
|
4273
|
+
if (err.code === "EPIPE") finish(0, "host disconnected");
|
|
4274
|
+
else finish(1, `stdout error: ${err.message}`);
|
|
4275
|
+
});
|
|
4276
|
+
proxy.connect(transport).catch((err) => {
|
|
4277
|
+
console.error(
|
|
4278
|
+
`q-ring airlock: failed to start: ${err instanceof Error ? err.message : String(err)}`
|
|
4279
|
+
);
|
|
4280
|
+
finish(1, "startup failure");
|
|
4281
|
+
});
|
|
4282
|
+
});
|
|
4283
|
+
}
|
|
4284
|
+
|
|
4285
|
+
// src/cli/commands/mcp.ts
|
|
4286
|
+
function registerMcpCommands(program2) {
|
|
4287
|
+
const mcp = program2.command("mcp").description("MCP airlock \u2014 run third-party MCP servers behind q-ring");
|
|
4288
|
+
mcp.command("wrap <command...>").description(
|
|
4289
|
+
"Proxy an MCP server through the airlock: spawned with a stripped env, every tool call audited. Use -- before the server command: qring mcp wrap -- npx some-server"
|
|
4290
|
+
).option(
|
|
4291
|
+
"--inherit-env",
|
|
4292
|
+
"Pass the full parent environment to the wrapped server (default: minimal safe env)"
|
|
4293
|
+
).option("--label <label>", "Session label for audit events (default: the command line)").action(async (commandArgs, cmd) => {
|
|
4294
|
+
try {
|
|
4295
|
+
const code = await runWrap({
|
|
4296
|
+
command: commandArgs[0],
|
|
4297
|
+
args: commandArgs.slice(1),
|
|
4298
|
+
inheritEnv: cmd.inheritEnv === true,
|
|
4299
|
+
label: cmd.label
|
|
4300
|
+
});
|
|
4301
|
+
process.exit(code);
|
|
4302
|
+
} catch (err) {
|
|
4303
|
+
console.error(
|
|
4304
|
+
c.red(
|
|
4305
|
+
`${SYMBOLS.cross} Airlock failed: ${err instanceof Error ? err.message : String(err)}`
|
|
4306
|
+
)
|
|
4307
|
+
);
|
|
4308
|
+
process.exit(1);
|
|
4309
|
+
}
|
|
4310
|
+
});
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4033
4313
|
// src/cli/commands/doctor.ts
|
|
4034
4314
|
import { existsSync as existsSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6, rmSync } from "fs";
|
|
4035
4315
|
import { join as join5, delimiter } from "path";
|
|
@@ -4400,7 +4680,7 @@ var COMMAND_GROUPS = [
|
|
|
4400
4680
|
{
|
|
4401
4681
|
name: "Security & Governance",
|
|
4402
4682
|
symbol: SYMBOLS.lock,
|
|
4403
|
-
commands: ["approve", "approvals", "policy"]
|
|
4683
|
+
commands: ["approve", "approvals", "policy", "canary", "mcp"]
|
|
4404
4684
|
}
|
|
4405
4685
|
];
|
|
4406
4686
|
var GroupedHelp = class extends Help {
|
|
@@ -4518,6 +4798,8 @@ function createProgram() {
|
|
|
4518
4798
|
registerAgentCommands(program2);
|
|
4519
4799
|
registerSecurityCommands(program2);
|
|
4520
4800
|
registerBridgeCommands(program2);
|
|
4801
|
+
registerCanaryCommands(program2);
|
|
4802
|
+
registerMcpCommands(program2);
|
|
4521
4803
|
registerDoctorCommand(program2);
|
|
4522
4804
|
registerCompletionCommand(program2);
|
|
4523
4805
|
return program2;
|