-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathregistry.ts
More file actions
146 lines (130 loc) · 4.7 KB
/
Copy pathregistry.ts
File metadata and controls
146 lines (130 loc) · 4.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
/**
* Agent registry + kill-switch (PDLSS) (master plan §B5.1 Phase G, ADR-020).
*
* Holds every agent's PDLSS registration (Purpose / Duration / Limit / Scope /
* Self-instantiation) and decides whether an agent is currently usable. The
* {@link AgentRegistry.revoke} method is the **kill-switch**: an instant,
* non-bypassable deactivation that a {@link GateValidator} surfaces as a
* `terminate` verdict.
*
* Exposed as a {@link GateValidator} via {@link AgentRegistry.asValidator} so the
* Action Gate consults it like any other validator: active ⇒ `allow`, revoked ⇒
* `terminate`, every other inactive reason ⇒ `block`.
*/
import type {
AgentRegistration,
GateContext,
GateOperation,
GateValidator,
GateVerdict,
RegistryStatus,
} from "./types";
const VALIDATOR_NAME = "kill-switch" as const;
const REVOKED_PREFIX = "revoked: " as const;
/** Internal mutable state tracked per registered agent. */
interface AgentRecord {
registration: AgentRegistration;
invocations: number;
revokedReason?: string;
}
/** Default clock for {@link AgentRegistry.asValidator}: the current ISO instant. */
function defaultNow(): string {
return new Date().toISOString();
}
/** In-memory agent registry implementing the PDLSS kill-switch contract. */
export class AgentRegistry {
private readonly records = new Map<string, AgentRecord>();
/** Upsert by agentId — clears any prior revocation and resets the count to 0. */
public register(reg: AgentRegistration): void {
this.records.set(reg.agentId, { registration: reg, invocations: 0 });
}
/** The kill-switch: instantly deactivate an agent (default reason `"revoked"`). */
public revoke(agentId: string, reason = "revoked"): void {
const record = this.records.get(agentId);
if (record !== undefined) {
record.revokedReason = reason;
}
}
/** Increment an agent's invocation counter (no-op for unknown agents). */
public recordInvocation(agentId: string): void {
const record = this.records.get(agentId);
if (record !== undefined) {
record.invocations += 1;
}
}
/** Whether the agent is currently usable, with a specific inactivity reason. */
public status(
agentId: string,
opts?: { now?: string; operation?: GateOperation },
): RegistryStatus {
const record = this.records.get(agentId);
if (record === undefined) {
return inactive("not registered");
}
if (record.revokedReason !== undefined) {
return inactive(`${REVOKED_PREFIX}${record.revokedReason}`);
}
if (this.isExpired(record, opts?.now)) {
return inactive("expired");
}
if (this.overLimit(record)) {
return inactive("invocation limit reached");
}
if (this.outOfScope(record, opts?.operation)) {
return inactive("operation out of scope");
}
return { active: true, reason: "active" };
}
/** True only when registered, not revoked, and `selfInstantiation` is set. */
public mayInstantiate(agentId: string): boolean {
const record = this.records.get(agentId);
return (
record !== undefined &&
record.revokedReason === undefined &&
record.registration.selfInstantiation
);
}
/** Expose the registry as a {@link GateValidator} named `"kill-switch"`. */
public asValidator(opts?: { now?: () => string }): GateValidator {
const now = opts?.now ?? defaultNow;
const check = (ctx: GateContext): GateVerdict => {
const result = this.status(ctx.agentId, {
now: now(),
operation: ctx.operation,
});
return {
action: toAction(result),
reason: result.reason,
validator: VALIDATOR_NAME,
};
};
return { name: VALIDATOR_NAME, check };
}
private isExpired(record: AgentRecord, now?: string): boolean {
const { expiresAt } = record.registration;
return expiresAt !== undefined && now !== undefined && expiresAt < now;
}
private overLimit(record: AgentRecord): boolean {
const limit = record.registration.invocationLimit ?? 0;
return limit > 0 && record.invocations >= limit;
}
private outOfScope(record: AgentRecord, operation?: GateOperation): boolean {
const { scope } = record.registration;
return (
operation !== undefined &&
scope !== undefined &&
!scope.includes(operation)
);
}
}
/** Build an inactive {@link RegistryStatus} with the given reason. */
function inactive(reason: string): RegistryStatus {
return { active: false, reason };
}
/** Map a {@link RegistryStatus} to a gate action (active→allow, revoked→terminate, else→block). */
function toAction(status: RegistryStatus): GateVerdict["action"] {
if (status.active) {
return "allow";
}
return status.reason.startsWith(REVOKED_PREFIX) ? "terminate" : "block";
}