mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 22:58:42 -10:00
feat(session-name): classifier virtual-domain 4th leg [M1 A2]
M1 gateway A2. Contract 2cf541c6 / arch verdict 8cd30094 (both verified on-box).
Adds a CLOSED virtual-domain leg (<local>@external) to the parity-pinned FR-8
session-name contract — a LEXICAL widening only; ADMISSION stays at the A1/A4 gateway.
- kind 'external' {local, domain}; VIRTUAL_DOMAIN_TOKENS = ["external"] as a CLOSED
NAMED CONSTANT beside a derived pattern (arch: not inline alternation; pure fn, no IO).
- isHumanSeatSessionRef WIDENED = HUMAN_SEAT || VIRTUAL_DOMAIN: @external refs are
human-CLASS (they JOIN the attention/human-route union — NEVER a silent agent-class
downgrade). parseSessionName precedence human -> external -> canonical -> legacy -> malformed.
- Edited daemon session-name.ts FIRST -> mirrored VERBATIM to cli + ui (cli≡ui byte-identical).
- Parity test: +7 vectors (mike@external, scheme slack:U...@external, dotted/hyphenated,
decided human@external POSITIVE; @external + mike@externalx NEG) + agreement-pin RHS
updated to PIN the intentional widening (not silent).
- 5-consumer consolidation onto the ONE predicate: switched routes/queue.ts isAttentionItem
from human-route-enforcer's isHumanSeatSession to isHumanSeatSessionRef (behavior-identical
today, gains @external). Sweep clean: queue.ts + queue-bridge + startup + attention-feed +
feed-classifier all on isHumanSeatSessionRef; no hidden re-impl.
DOCUMENTED DIVERGENCE (route as follow-on, per dev-planner): human-route-enforcer.ts stays
READ-ONLY this slice, so its other consumers (mission-control-write-contract, queue-repository
park-validation, review/gather) keep the narrow @(kernel|host) pattern — @external is not
human-class THERE yet. Reserved-token 'external' rail is A1's build (proof-7 verifies post-A1).
Parity 66/66; attention-route + queue-route targeted 54/54; tsc daemon/cli/ui 0. Based on
local main c4d5f4e3 (origin/main was 12 folds stale — never-push means local main is truth).
Never push.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
c4d5f4e3e3
commit
3e688f6347
6 files changed
+153
-22
No files matched your search
@@ -33,18 +33,42 @@
|
||||
export type ParsedSessionName =
|
||||
| { kind: "canonical"; member: string; rig: string }
|
||||
| { kind: "legacy"; name: string }
|
||||
// A2 4th leg — virtual-domain ref (<local>@external). `local` is verbatim up to
|
||||
// the domain "@" (the scheme leg, slack:U..., rides `local` LEXICALLY; the
|
||||
// registered-vs-scheme split is a GATEWAY parse, never the classifier's).
|
||||
| { kind: "external"; local: string; domain: string }
|
||||
| { kind: "malformed"; error: "malformed_session_name"; input: string };
|
||||
|
||||
const HUMAN_SEAT_SESSION_REF_PATTERN = /^human(?:-[A-Za-z0-9._-]+)?@(kernel|host)$/;
|
||||
const LEGACY_FLAT_SESSION_PATTERN = /^r\d{2}-.+$/;
|
||||
// A2 4th leg — CLOSED virtual-domain token set as a NAMED CONSTANT beside the
|
||||
// pattern (arch: not inline alternation; lexical-only pure fn, no state/IO).
|
||||
// Future domain words extend this array (e.g. @a2a) as explicit alternation.
|
||||
// Admission (is this @external ref registered?) is NOT here — it is the A1/A4
|
||||
// gateway's job; the classifier only recognizes the lexical shape.
|
||||
const VIRTUAL_DOMAIN_TOKENS = ["external"] as const;
|
||||
const VIRTUAL_DOMAIN_SESSION_REF_PATTERN = new RegExp(
|
||||
`^[A-Za-z0-9._:-]+@(${VIRTUAL_DOMAIN_TOKENS.join("|")})$`
|
||||
);
|
||||
|
||||
/** Arch tooth 1: human-seat classification runs BEFORE any parse at sites
|
||||
* where the human/agent distinction matters (the queue-destination gate). */
|
||||
/** Arch tooth 1: human-CLASS classification runs BEFORE any parse at sites where
|
||||
* the human/agent distinction matters (the queue-destination gate + attention
|
||||
* union). A2 widened it: virtual-domain refs (<local>@external) are human-CLASS
|
||||
* too (they join human-route enforcement; NEVER a silent agent-class downgrade).
|
||||
* Admission stays at the A1/A4 gateway. */
|
||||
export function isHumanSeatSessionRef(sessionRef: string): boolean {
|
||||
return HUMAN_SEAT_SESSION_REF_PATTERN.test(sessionRef);
|
||||
return HUMAN_SEAT_SESSION_REF_PATTERN.test(sessionRef) ||
|
||||
VIRTUAL_DOMAIN_SESSION_REF_PATTERN.test(sessionRef);
|
||||
}
|
||||
|
||||
export function parseSessionName(raw: string): ParsedSessionName {
|
||||
// Precedence (arch): human -> EXTERNAL -> canonical -> legacy -> malformed. The
|
||||
// virtual-domain leg MUST precede canonical, else "mike@external" parses as
|
||||
// member=mike rig=external; A1's rig-name reservation keeps that unambiguous.
|
||||
if (VIRTUAL_DOMAIN_SESSION_REF_PATTERN.test(raw)) {
|
||||
const domainAt = raw.lastIndexOf("@");
|
||||
return { kind: "external", local: raw.slice(0, domainAt), domain: raw.slice(domainAt + 1) };
|
||||
}
|
||||
const at = raw.indexOf("@");
|
||||
if (at > 0 && at < raw.length - 1) {
|
||||
return { kind: "canonical", member: raw.slice(0, at), rig: raw.slice(at + 1) };
|
||||
|
||||
@@ -132,18 +132,42 @@ export function validateSessionComponents(
|
||||
export type ParsedSessionName =
|
||||
| { kind: "canonical"; member: string; rig: string }
|
||||
| { kind: "legacy"; name: string }
|
||||
// A2 4th leg — virtual-domain ref (<local>@external). `local` is verbatim up to
|
||||
// the domain "@" (the scheme leg, slack:U..., rides `local` LEXICALLY; the
|
||||
// registered-vs-scheme split is a GATEWAY parse, never the classifier's).
|
||||
| { kind: "external"; local: string; domain: string }
|
||||
| { kind: "malformed"; error: "malformed_session_name"; input: string };
|
||||
|
||||
const HUMAN_SEAT_SESSION_REF_PATTERN = /^human(?:-[A-Za-z0-9._-]+)?@(kernel|host)$/;
|
||||
const LEGACY_FLAT_SESSION_PATTERN = /^r\d{2}-.+$/;
|
||||
// A2 4th leg — CLOSED virtual-domain token set as a NAMED CONSTANT beside the
|
||||
// pattern (arch: not inline alternation; lexical-only pure fn, no state/IO).
|
||||
// Future domain words extend this array (e.g. @a2a) as explicit alternation.
|
||||
// Admission (is this @external ref registered?) is NOT here — it is the A1/A4
|
||||
// gateway's job; the classifier only recognizes the lexical shape.
|
||||
const VIRTUAL_DOMAIN_TOKENS = ["external"] as const;
|
||||
const VIRTUAL_DOMAIN_SESSION_REF_PATTERN = new RegExp(
|
||||
`^[A-Za-z0-9._:-]+@(${VIRTUAL_DOMAIN_TOKENS.join("|")})$`
|
||||
);
|
||||
|
||||
/** Arch tooth 1: human-seat classification runs BEFORE any parse at sites
|
||||
* where the human/agent distinction matters (the queue-destination gate). */
|
||||
/** Arch tooth 1: human-CLASS classification runs BEFORE any parse at sites where
|
||||
* the human/agent distinction matters (the queue-destination gate + attention
|
||||
* union). A2 widened it: virtual-domain refs (<local>@external) are human-CLASS
|
||||
* too (they join human-route enforcement; NEVER a silent agent-class downgrade).
|
||||
* Admission stays at the A1/A4 gateway. */
|
||||
export function isHumanSeatSessionRef(sessionRef: string): boolean {
|
||||
return HUMAN_SEAT_SESSION_REF_PATTERN.test(sessionRef);
|
||||
return HUMAN_SEAT_SESSION_REF_PATTERN.test(sessionRef) ||
|
||||
VIRTUAL_DOMAIN_SESSION_REF_PATTERN.test(sessionRef);
|
||||
}
|
||||
|
||||
export function parseSessionName(raw: string): ParsedSessionName {
|
||||
// Precedence (arch): human -> EXTERNAL -> canonical -> legacy -> malformed. The
|
||||
// virtual-domain leg MUST precede canonical, else "mike@external" parses as
|
||||
// member=mike rig=external; A1's rig-name reservation keeps that unambiguous.
|
||||
if (VIRTUAL_DOMAIN_SESSION_REF_PATTERN.test(raw)) {
|
||||
const domainAt = raw.lastIndexOf("@");
|
||||
return { kind: "external", local: raw.slice(0, domainAt), domain: raw.slice(domainAt + 1) };
|
||||
}
|
||||
const at = raw.indexOf("@");
|
||||
if (at > 0 && at < raw.length - 1) {
|
||||
return { kind: "canonical", member: raw.slice(0, at), rig: raw.slice(at + 1) };
|
||||
|
||||
@@ -8,8 +8,7 @@ import type {
|
||||
} from "../domain/queue-repository.js";
|
||||
import { QueueRepositoryError, newQitemId, deriveCrossHostSuccessorId, stampSelfHostSuffix } from "../domain/queue-repository.js";
|
||||
import type { QueueItem } from "../domain/queue-repository.js";
|
||||
import { isHumanSeatSession } from "../domain/human-route-enforcer.js";
|
||||
import { parseSessionName } from "../domain/session-name.js";
|
||||
import { parseSessionName, isHumanSeatSessionRef } from "../domain/session-name.js";
|
||||
import { hostname as osHostname } from "node:os";
|
||||
import type { InboxHandler } from "../domain/inbox-handler.js";
|
||||
import { InboxHandlerError } from "../domain/inbox-handler.js";
|
||||
@@ -72,8 +71,13 @@ export function crossHostProvenanceTags(existing: string[] | undefined): string[
|
||||
*/
|
||||
export function isAttentionItem(q: { tier: string | null; destinationSession: string; state?: string; blockedOn?: string | null }): boolean {
|
||||
if (q.tier === "human-gate") return true;
|
||||
if (isHumanSeatSession(q.destinationSession)) return true;
|
||||
return q.state === "blocked" && isHumanSeatSession(q.blockedOn ?? null);
|
||||
// A2: consolidated onto the ONE contract predicate (isHumanSeatSessionRef) so
|
||||
// virtual-domain refs (<local>@external) are human-CLASS and JOIN the attention
|
||||
// union — behavior-identical for every non-@external ref (same pattern today).
|
||||
// The enforcer's other consumers keep the narrow pattern this slice (a DOCUMENTED
|
||||
// divergence, routed as a follow-on per dev-planner's ruling).
|
||||
if (isHumanSeatSessionRef(q.destinationSession)) return true;
|
||||
return q.state === "blocked" && isHumanSeatSessionRef(q.blockedOn ?? "");
|
||||
}
|
||||
|
||||
|
||||
@@ -717,8 +721,9 @@ export function queueRoutes(): Hono {
|
||||
// source of truth for the UI Action-required + Approval lenses) so
|
||||
// those surfaces don't depend on the lossy ephemeral client event
|
||||
// FIFO. Class membership matches the mission-control read layer
|
||||
// semantics: tier='human-gate' OR destination matches
|
||||
// /^human(?:-[A-Za-z0-9._-]+)?@(kernel|host)$/. Open state defaults
|
||||
// semantics: tier='human-gate' OR the ONE contract predicate
|
||||
// isHumanSeatSessionRef (human-seat /^human…@(kernel|host)$/ OR the A2
|
||||
// virtual-domain leg <local>@external). Open state defaults
|
||||
// to pending|in-progress|blocked (callers can still override via
|
||||
// `state=...`). Composable with destinationSession/sourceSession/
|
||||
// targetRepo/limit.
|
||||
|
||||
@@ -127,6 +127,52 @@ const SHARED_VECTORS = [
|
||||
parsed: malformed("r3-worker"),
|
||||
human: false, memberLabel: "r3-worker", rigOf: undefined,
|
||||
},
|
||||
// ── A2 4th leg: virtual-domain (@external). Contract 2cf541c6 / verdict 8cd30094.
|
||||
// kind 'external' {local, domain}; the human-CLASS predicate returns TRUE (no silent
|
||||
// agent-class downgrade); rigOf undefined (a virtual domain is NOT a routing rig).
|
||||
// Admission (registered vs scheme, refuse-if-unregistered) is the A1/A4 GATEWAY's job.
|
||||
{
|
||||
label: "4TH LEG: registered virtual-domain ref mike@external",
|
||||
input: "mike@external",
|
||||
parsed: { kind: "external", local: "mike", domain: "external" },
|
||||
human: true, memberLabel: "mike", rigOf: undefined,
|
||||
},
|
||||
{
|
||||
label: "4TH LEG: scheme-form local rides the leg LEXICALLY (slack:U012AB3CD@external)",
|
||||
input: "slack:U012AB3CD@external",
|
||||
parsed: { kind: "external", local: "slack:U012AB3CD", domain: "external" },
|
||||
human: true, memberLabel: "slack:U012AB3CD", rigOf: undefined,
|
||||
},
|
||||
{
|
||||
label: "4TH LEG: dotted local (my.name@external)",
|
||||
input: "my.name@external",
|
||||
parsed: { kind: "external", local: "my.name", domain: "external" },
|
||||
human: true, memberLabel: "my.name", rigOf: undefined,
|
||||
},
|
||||
{
|
||||
label: "4TH LEG: hyphenated local (my-name@external)",
|
||||
input: "my-name@external",
|
||||
parsed: { kind: "external", local: "my-name", domain: "external" },
|
||||
human: true, memberLabel: "my-name", rigOf: undefined,
|
||||
},
|
||||
{
|
||||
label: "4TH LEG decided human@external: local 'human' is valid -> external (human-class), refused-at-gateway if unregistered",
|
||||
input: "human@external",
|
||||
parsed: { kind: "external", local: "human", domain: "external" },
|
||||
human: true, memberLabel: "human", rigOf: undefined,
|
||||
},
|
||||
{
|
||||
label: "4TH LEG neg: empty local (@external) -> malformed (not agent-downgraded)",
|
||||
input: "@external",
|
||||
parsed: malformed("@external"),
|
||||
human: false, memberLabel: "", rigOf: undefined,
|
||||
},
|
||||
{
|
||||
label: "4TH LEG neg: domain not in the closed set (mike@externalx) -> canonical, NOT external",
|
||||
input: "mike@externalx",
|
||||
parsed: { kind: "canonical", member: "mike", rig: "externalx" },
|
||||
human: false, memberLabel: "mike", rigOf: "externalx",
|
||||
},
|
||||
] as const;
|
||||
|
||||
describe("session-name contract — three-copy parity (shared vectors)", () => {
|
||||
@@ -149,12 +195,18 @@ describe("session-name contract — three-copy parity (shared vectors)", () => {
|
||||
expect(cliSrc).toBe(uiSrc);
|
||||
});
|
||||
|
||||
it("the contract's human-seat predicate agrees with the read-only human-route-enforcer pattern on every vector", () => {
|
||||
// human-route-enforcer.ts is READ-ONLY this slice (PRD constraint) and
|
||||
// keeps its own pattern; this read-only import pins the contract copy
|
||||
// against it so neither can drift silently.
|
||||
it("the contract's human-CLASS predicate = the read-only enforcer human-seat pattern OR the A2 virtual-domain leg (intentional widening, pinned not silent)", () => {
|
||||
// human-route-enforcer.ts is READ-ONLY this slice (PRD constraint) and keeps
|
||||
// its narrow human-seat pattern. A2 widened the contract predicate to also
|
||||
// admit virtual-domain refs (<local>@external) as human-CLASS. That divergence
|
||||
// is INTENTIONAL, so we pin the exact union — enforcer-human-seat OR the
|
||||
// contract's own external kind — so neither side can drift SILENTLY. (The
|
||||
// enforcer's remaining consumers keep the narrow pattern this slice: a
|
||||
// DOCUMENTED divergence routed as a follow-on, per dev-planner's ruling.)
|
||||
for (const v of SHARED_VECTORS) {
|
||||
expect(daemonCopy.isHumanSeatSessionRef(v.input)).toBe(HUMAN_SEAT_SESSION_PATTERN.test(v.input));
|
||||
const enforcerHumanSeat = HUMAN_SEAT_SESSION_PATTERN.test(v.input);
|
||||
const virtualDomainLeg = daemonCopy.parseSessionName(v.input).kind === "external";
|
||||
expect(daemonCopy.isHumanSeatSessionRef(v.input)).toBe(enforcerHumanSeat || virtualDomainLeg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,8 +81,10 @@ async function fetchAggregatedAttention(limit?: number): Promise<AttentionData>
|
||||
}
|
||||
|
||||
/**
|
||||
* Open attention-class qitems (tier="human-gate" OR destination
|
||||
* matches /^human(?:-[A-Za-z0-9._-]+)?@(kernel|host)$/). State default
|
||||
* Open attention-class qitems (tier="human-gate" OR destination is
|
||||
* human-CLASS via isHumanSeatSessionRef — human-seat
|
||||
* /^human(?:-[A-Za-z0-9._-]+)?@(kernel|host)$/ OR the A2 virtual-domain
|
||||
* leg <local>@external). State default
|
||||
* is pending|in-progress|blocked — closed/done items are NOT surfaced.
|
||||
*
|
||||
* `limit` (optional, default 50) caps the rendered set so a
|
||||
|
||||
@@ -33,18 +33,42 @@
|
||||
export type ParsedSessionName =
|
||||
| { kind: "canonical"; member: string; rig: string }
|
||||
| { kind: "legacy"; name: string }
|
||||
// A2 4th leg — virtual-domain ref (<local>@external). `local` is verbatim up to
|
||||
// the domain "@" (the scheme leg, slack:U..., rides `local` LEXICALLY; the
|
||||
// registered-vs-scheme split is a GATEWAY parse, never the classifier's).
|
||||
| { kind: "external"; local: string; domain: string }
|
||||
| { kind: "malformed"; error: "malformed_session_name"; input: string };
|
||||
|
||||
const HUMAN_SEAT_SESSION_REF_PATTERN = /^human(?:-[A-Za-z0-9._-]+)?@(kernel|host)$/;
|
||||
const LEGACY_FLAT_SESSION_PATTERN = /^r\d{2}-.+$/;
|
||||
// A2 4th leg — CLOSED virtual-domain token set as a NAMED CONSTANT beside the
|
||||
// pattern (arch: not inline alternation; lexical-only pure fn, no state/IO).
|
||||
// Future domain words extend this array (e.g. @a2a) as explicit alternation.
|
||||
// Admission (is this @external ref registered?) is NOT here — it is the A1/A4
|
||||
// gateway's job; the classifier only recognizes the lexical shape.
|
||||
const VIRTUAL_DOMAIN_TOKENS = ["external"] as const;
|
||||
const VIRTUAL_DOMAIN_SESSION_REF_PATTERN = new RegExp(
|
||||
`^[A-Za-z0-9._:-]+@(${VIRTUAL_DOMAIN_TOKENS.join("|")})$`
|
||||
);
|
||||
|
||||
/** Arch tooth 1: human-seat classification runs BEFORE any parse at sites
|
||||
* where the human/agent distinction matters (the queue-destination gate). */
|
||||
/** Arch tooth 1: human-CLASS classification runs BEFORE any parse at sites where
|
||||
* the human/agent distinction matters (the queue-destination gate + attention
|
||||
* union). A2 widened it: virtual-domain refs (<local>@external) are human-CLASS
|
||||
* too (they join human-route enforcement; NEVER a silent agent-class downgrade).
|
||||
* Admission stays at the A1/A4 gateway. */
|
||||
export function isHumanSeatSessionRef(sessionRef: string): boolean {
|
||||
return HUMAN_SEAT_SESSION_REF_PATTERN.test(sessionRef);
|
||||
return HUMAN_SEAT_SESSION_REF_PATTERN.test(sessionRef) ||
|
||||
VIRTUAL_DOMAIN_SESSION_REF_PATTERN.test(sessionRef);
|
||||
}
|
||||
|
||||
export function parseSessionName(raw: string): ParsedSessionName {
|
||||
// Precedence (arch): human -> EXTERNAL -> canonical -> legacy -> malformed. The
|
||||
// virtual-domain leg MUST precede canonical, else "mike@external" parses as
|
||||
// member=mike rig=external; A1's rig-name reservation keeps that unambiguous.
|
||||
if (VIRTUAL_DOMAIN_SESSION_REF_PATTERN.test(raw)) {
|
||||
const domainAt = raw.lastIndexOf("@");
|
||||
return { kind: "external", local: raw.slice(0, domainAt), domain: raw.slice(domainAt + 1) };
|
||||
}
|
||||
const at = raw.indexOf("@");
|
||||
if (at > 0 && at < raw.length - 1) {
|
||||
return { kind: "canonical", member: raw.slice(0, at), rig: raw.slice(at + 1) };
|
||||
|
||||
Reference in new issue
Block a user