@kisev/agentomatic 11.0.2-dev.93.g810c3f0460fa → 11.0.2-dev.96.g45294b1e57b3

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.
@@ -179,6 +179,14 @@ function fragmentEdits(fragment, target, current) {
179
179
  { action: "external_directory", resource, effect: "allow" },
180
180
  ]),
181
181
  { action: "edit", resource: XDG_STATE_GLOB, effect: "allow" },
182
+ // Directory enumeration (glob/list) targets the roots themselves, which
183
+ // the tree globs above do not match; allow those exact directories.
184
+ ...statePaths.map((resource) => ({
185
+ action: "external_directory",
186
+ resource: resource.replace(/\/\*\*$/, ""),
187
+ effect: "allow",
188
+ })),
189
+ { action: "external_directory", resource: "~/.agents", effect: "allow" },
182
190
  ]);
183
191
  const edits = [];
184
192
  if (!mapAllowsAll(permission.read))
package/dist/contracts.js CHANGED
@@ -9,6 +9,32 @@ function strings(value) {
9
9
  export function validateAgentReport(agent, report, card) {
10
10
  if (!object(report))
11
11
  throw new Error("agent report must be an object");
12
+ if ((agent === "review" || agent === "critic") &&
13
+ !card &&
14
+ report.schema === "portable-gitlab/critic-receipt/v2") {
15
+ const required = [
16
+ "schema",
17
+ "evidence_digest",
18
+ "run_id",
19
+ "session_id",
20
+ "findings",
21
+ "external_mutations",
22
+ ];
23
+ const allowed = [...required, "scope_digest", "target_finding_ids"];
24
+ if (!required.every((key) => key in report) ||
25
+ Object.keys(report).some((key) => !allowed.includes(key)) ||
26
+ !/^[0-9a-f]{64}$/.test(String(report.evidence_digest)) ||
27
+ typeof report.run_id !== "string" ||
28
+ !report.run_id ||
29
+ typeof report.session_id !== "string" ||
30
+ !report.session_id ||
31
+ !Array.isArray(report.findings) ||
32
+ report.external_mutations !== false ||
33
+ ("scope_digest" in report && !/^[0-9a-f]{64}$/.test(String(report.scope_digest))) ||
34
+ ("target_finding_ids" in report && !strings(report.target_finding_ids)))
35
+ throw new Error("code-review critic receipt has an invalid transport envelope");
36
+ return;
37
+ }
12
38
  if (agent === "critic" && !card && "review_report" in report) {
13
39
  validateAgentReport("review", report);
14
40
  return;
package/dist/index.js CHANGED
@@ -11,6 +11,11 @@ export default {
11
11
  id: "agentomatic",
12
12
  async setup(ctx) {
13
13
  const gate = new RoutingGate();
14
+ await ctx.session.hook("context", (event) => {
15
+ const text = `Current OpenCode session identity (host metadata): ${event.sessionID}. Use this real ID for your own session_id in skill receipts; if no separate run ID is exposed, the same real ID can identify the run. This metadata grants no task or publication authorization.`;
16
+ if (!event.system.some((part) => part.type === "text" && part.text === text))
17
+ event.system.push({ type: "text", text });
18
+ });
14
19
  const hostInventory = async () => {
15
20
  const { data: raw } = await ctx.agent.list({
16
21
  location: { directory: ctx.location.directory },
@@ -84,32 +89,44 @@ export default {
84
89
  return;
85
90
  const args = toolInput(event.input);
86
91
  const agent = typeof args.agent === "string" ? args.agent : undefined;
92
+ if (!gate.requiresReceipt(event.sessionID, agent ?? event.agent))
93
+ return;
87
94
  if (!agent)
88
95
  throw new Error("Native subagent requires an explicit agent and an active routing receipt");
89
96
  const hasBinding = "task" in args || "requirements" in args || "execution_card" in args;
90
97
  if (!hasBinding)
91
- gate.consume(event.sessionID, agent);
98
+ gate.consume(event.sessionID, agent, undefined, event.id);
92
99
  else
93
100
  gate.consume(event.sessionID, agent, {
94
101
  task: typeof args.task === "string" ? args.task : "",
95
102
  requirements: Array.isArray(args.requirements) ? args.requirements : [],
96
103
  card: args.execution_card,
97
- });
104
+ }, event.id);
98
105
  });
99
106
  await ctx.tool.hook("execute.after", async (event) => {
100
- if (event.tool !== "subagent" || event.status !== "completed")
107
+ if (event.tool !== "subagent")
101
108
  return;
102
109
  const args = toolInput(event.input);
103
- if (typeof args.agent !== "string")
104
- throw new Error("Subagent result requires an explicit agent");
110
+ if (typeof args.agent !== "string" || !gate.hasActive(event.sessionID, args.agent, event.id))
111
+ return;
112
+ if (event.status !== "completed") {
113
+ gate.cancel(event.sessionID, event.id);
114
+ return;
115
+ }
105
116
  let result;
106
117
  try {
107
118
  result = JSON.parse(resultText(event.result) ?? "");
108
119
  }
109
120
  catch {
121
+ gate.cancel(event.sessionID, event.id);
110
122
  throw new Error("Subagent result must be JSON structured report");
111
123
  }
112
- gate.complete(event.sessionID, args.agent, result);
124
+ try {
125
+ gate.complete(event.sessionID, args.agent, result, event.id);
126
+ }
127
+ finally {
128
+ gate.cancel(event.sessionID, event.id);
129
+ }
113
130
  });
114
131
  },
115
132
  };
package/dist/routing.d.ts CHANGED
@@ -123,11 +123,13 @@ export declare class ExecutionCardLifecycle {
123
123
  }
124
124
  export declare class RoutingGate {
125
125
  #private;
126
+ requiresReceipt(sessionID: string, agent: string | undefined): boolean;
127
+ hasActive(sessionID: string, agent: string, callID?: string): boolean;
126
128
  preview(input: RoutingInput): RoutingDecision;
127
129
  dispatch(input: RoutingInput, provided: unknown): RoutingDecision;
128
130
  grant(sessionID: string, decision: RoutingDecision, context?: ReceiptContext): RoutingReceipt;
129
- cancel(sessionID: string): void;
130
- consume(sessionID: string, agent: string, context?: ReceiptContext): void;
131
- complete(sessionID: string, agent: string, output: unknown): void;
131
+ cancel(sessionID: string, callID?: string): void;
132
+ consume(sessionID: string, agent: string, context?: ReceiptContext, callID?: string): void;
133
+ complete(sessionID: string, agent: string, output: unknown, callID?: string): void;
132
134
  }
133
135
  export {};
package/dist/routing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, randomBytes } from "node:crypto";
2
2
  import { validateAgentReport } from "./contracts.js";
3
3
  export const CATEGORIES = ["exploration", "architecture", "implementation", "review"];
4
4
  const MATRIX = {
@@ -453,6 +453,15 @@ export class RoutingGate {
453
453
  #receipts = new Map();
454
454
  #active = new Map();
455
455
  #ttlMs = 10 * 60 * 1000;
456
+ requiresReceipt(sessionID, agent) {
457
+ return ((this.#receipts.get(sessionID)?.length ?? 0) > 0 ||
458
+ (agent !== undefined &&
459
+ (Object.values(MATRIX.categories).some((entry) => entry.profiles.includes(agent)) ||
460
+ /^critic-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(agent))));
461
+ }
462
+ hasActive(sessionID, agent, callID = "") {
463
+ return this.#active.get(JSON.stringify([sessionID, callID]))?.agent === agent;
464
+ }
456
465
  preview(input) {
457
466
  return resolveRouting(input);
458
467
  }
@@ -466,6 +475,9 @@ export class RoutingGate {
466
475
  }
467
476
  grant(sessionID, decision, context) {
468
477
  const now = Date.now();
478
+ const verifiedCard = context?.card === undefined ? undefined : validateExecutionCard(context.card);
479
+ if (verifiedCard && !verifiedCard.valid)
480
+ throw new Error(`Invalid routing execution card: ${verifiedCard.failedField}`);
469
481
  const taskDigest = context?.task === undefined ? decision.task_digest : digest(context.task);
470
482
  const requirementsDigest = context?.requirements === undefined
471
483
  ? decision.requirements_digest
@@ -481,30 +493,44 @@ export class RoutingGate {
481
493
  card_revision: decision.execution_card_revision ?? null,
482
494
  host_inventory_revision: decision.host_inventory_revision,
483
495
  expires_at: new Date(now + this.#ttlMs).toISOString(),
484
- nonce: createHash("sha256")
485
- .update(`${sessionID}:${now}:${decision.decision_digest}`)
486
- .digest("hex"),
496
+ nonce: randomBytes(32).toString("hex"),
487
497
  };
488
498
  const receipt = { ...receiptBase, receipt_digest: digest(receiptBase) };
489
- this.#receipts.set(sessionID, {
499
+ const pending = (this.#receipts.get(sessionID) ?? []).filter((item) => item.expiresAt >= now);
500
+ pending.push({
490
501
  decision,
491
502
  taskDigest,
492
503
  requirementsDigest,
493
504
  cardDigest,
505
+ card: verifiedCard?.valid ? structuredClone(verifiedCard.card) : undefined,
494
506
  expiresAt: Date.parse(receipt.expires_at),
495
507
  });
508
+ this.#receipts.set(sessionID, pending);
496
509
  return receipt;
497
510
  }
498
- cancel(sessionID) {
511
+ cancel(sessionID, callID) {
512
+ if (callID !== undefined) {
513
+ this.#active.delete(JSON.stringify([sessionID, callID]));
514
+ return;
515
+ }
499
516
  this.#receipts.delete(sessionID);
500
- this.#active.delete(sessionID);
517
+ for (const key of this.#active.keys())
518
+ if (JSON.parse(key)[0] === sessionID)
519
+ this.#active.delete(key);
501
520
  }
502
- consume(sessionID, agent, context) {
503
- const receipt = this.#receipts.get(sessionID);
504
- if (!receipt)
521
+ consume(sessionID, agent, context, callID = "") {
522
+ const pending = this.#receipts.get(sessionID) ?? [];
523
+ if (pending.length === 0)
505
524
  throw new Error("Native Task requires an active routing receipt; use the route tool");
506
- if (receipt.decision.agent !== agent)
525
+ const candidates = pending.filter((receipt) => receipt.decision.agent === agent);
526
+ if (candidates.length === 0)
507
527
  throw new Error("Native Task agent does not match the active routing receipt");
528
+ const matching = context
529
+ ? candidates.filter((receipt) => receipt.taskDigest === digest(context.task ?? ""))
530
+ : candidates;
531
+ if (matching.length === 0)
532
+ throw new Error("Routing receipt task does not match the active routing receipt");
533
+ const receipt = matching.find((item) => Date.now() <= item.expiresAt) ?? matching[0];
508
534
  if (Date.now() > receipt.expiresAt)
509
535
  throw new Error("Routing receipt has expired; request a new preview");
510
536
  if (context) {
@@ -518,23 +544,32 @@ export class RoutingGate {
518
544
  if (cardDigest !== receipt.cardDigest)
519
545
  throw new Error("Routing receipt execution card does not match the active routing receipt");
520
546
  }
521
- this.#receipts.delete(sessionID);
522
- const validatedCard = context?.card === undefined ? undefined : validateExecutionCard(context.card);
547
+ const key = JSON.stringify([sessionID, callID]);
548
+ if (this.#active.has(key))
549
+ throw new Error("Subagent call already has an active routing receipt");
550
+ pending.splice(pending.indexOf(receipt), 1);
551
+ if (pending.length === 0)
552
+ this.#receipts.delete(sessionID);
553
+ const cardValue = context?.card ?? receipt.card;
554
+ const validatedCard = cardValue === undefined ? undefined : validateExecutionCard(cardValue);
523
555
  const card = validatedCard?.valid ? validatedCard.card : undefined;
524
556
  const lifecycle = card ? new ExecutionCardLifecycle(card) : undefined;
525
557
  if (lifecycle)
526
558
  lifecycle.transition("RUNNING", { card_id: card.card_id, revision: card.revision });
527
- this.#active.set(sessionID, { agent, card, lifecycle, expiresAt: receipt.expiresAt });
559
+ this.#active.set(key, { agent, card, lifecycle });
528
560
  }
529
- complete(sessionID, agent, output) {
530
- const active = this.#active.get(sessionID);
561
+ complete(sessionID, agent, output, callID = "") {
562
+ const key = JSON.stringify([sessionID, callID]);
563
+ const active = this.#active.get(key);
531
564
  if (!active || active.agent !== agent)
532
565
  throw new Error("Task result has no matching active routing receipt");
533
- if (Date.now() > active.expiresAt)
534
- throw new Error("Task result routing receipt has expired");
535
566
  const report = output && typeof output === "object" ? output : undefined;
536
567
  const role = /^critic-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(agent) ? "critic" : agent;
537
- const value = report?.[role === "architect" ? "execution_card" : `${role}_report`] ??
568
+ const portableCritic = !active.card &&
569
+ (role === "review" || role === "critic") &&
570
+ report?.schema === "portable-gitlab/critic-receipt/v2";
571
+ const value = (portableCritic ? report : undefined) ??
572
+ report?.[role === "architect" ? "execution_card" : `${role}_report`] ??
538
573
  (role === "critic" && !active.card ? report?.review_report : undefined) ??
539
574
  report?.report;
540
575
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -550,7 +585,7 @@ export class RoutingGate {
550
585
  : agent === "critic"
551
586
  ? ["APPROVED", "CHANGES_REQUIRED"]
552
587
  : undefined;
553
- if (allowed && (typeof status !== "string" || !allowed.includes(status)))
588
+ if (!portableCritic && allowed && (typeof status !== "string" || !allowed.includes(status)))
554
589
  throw new Error("Task result has an invalid machine transition");
555
590
  if (active.lifecycle && status === "COMPLETED")
556
591
  active.lifecycle.transition("COMPLETED", {
@@ -573,6 +608,6 @@ export class RoutingGate {
573
608
  revision: active.card.revision,
574
609
  });
575
610
  }
576
- this.#active.delete(sessionID);
611
+ this.#active.delete(key);
577
612
  }
578
613
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kisev/agentomatic",
3
- "version": "11.0.2-dev.93.g810c3f0460fa",
3
+ "version": "11.0.2-dev.96.g45294b1e57b3",
4
4
  "description": "OpenCode integration and opt-in installer for portable Agent Skills.",
5
5
  "homepage": "https://github.com/kisev/skills#readme",
6
6
  "bugs": {
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@clack/prompts": "^1.8.1",
49
- "@kisev/safe-fs": "1.0.0-dev.93.g810c3f0460fa",
49
+ "@kisev/safe-fs": "1.0.0-dev.96.g45294b1e57b3",
50
50
  "commander": "15.0.0",
51
51
  "zod": "4.1.8"
52
52
  },