@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/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-MLBJCPX2.js";
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-T2UG23KI.js");
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;