@intentius/chant 0.124.1 → 0.124.2
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/cli/mcp/workspace-tools.d.ts +5 -1
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/lifecycle/gate-audit.d.ts +37 -13
- package/dist/lifecycle/gate-audit.d.ts.map +1 -1
- package/dist/lifecycle/gate-origin.d.ts +14 -2
- package/dist/lifecycle/gate-origin.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +11 -1
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +2 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/mcp/workspace-tools.ts +7 -3
- package/src/lifecycle/gate-audit.test.ts +42 -0
- package/src/lifecycle/gate-audit.ts +102 -34
- package/src/lifecycle/gate-origin.ts +20 -3
- package/src/op/gate-point-mcp.e2e.test.ts +161 -0
- package/src/workspace/decide.ts +85 -6
- package/src/workspace/points-write.schema.json +1 -0
- package/src/workspace/reason-codes.ts +2 -1
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
* `clientInfo`. Computed provenance, from git, is unchanged.
|
|
29
29
|
*
|
|
30
30
|
* Nothing here listens on a port or authenticates anyone (ws-052): the server
|
|
31
|
-
* speaks over stdio, and `by` is recorded as given, as `--by` is.
|
|
31
|
+
* speaks over stdio, and `by` is recorded as given, as `--by` is. A decision
|
|
32
|
+
* point's answer is the exception (#3817): `chant serve mcp` makes every call
|
|
33
|
+
* the model's (`lifecycle/gate-origin.ts`), so `points-answer` and
|
|
34
|
+
* `points-retract` count nobody `by` names toward a point's quorum, as
|
|
35
|
+
* `op-approve` records its approver as an agent.
|
|
32
36
|
*
|
|
33
37
|
* An agent session (#2548): a server started with `CHANT_AGENT` set writes in
|
|
34
38
|
* that session, bound to its one member, and its writes are refused outside
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-tools.d.ts","sourceRoot":"","sources":["../../../src/cli/mcp/workspace-tools.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"workspace-tools.d.ts","sourceRoot":"","sources":["../../../src/cli/mcp/workspace-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAGH,OAAO,KAAK,EAAe,cAAc,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAKxE,oEAAoE;AACpE,MAAM,MAAM,YAAY,GAAG,MAAM,EAAE,CAAC;AAEpC;;;GAGG;AACH,wBAAgB,eAAe,IAAI,YAAY,CAE9C;AAED,MAAM,WAAW,qBAAqB;IACpC,qFAAqF;IACrF,GAAG,EAAE,MAAM,CAAC;IACZ,oEAAoE;IACpE,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAmBD,eAAO,MAAM,kBAAkB,EAAE,cAAc,EAkI9C,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,cAAc,EA8G/C,CAAC;AAsCF,qHAAqH;AACrH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAyEhF;AA6CD,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,cAAc,CAAC;IAC3B,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,+EAA+E;AAC/E,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,aAAa,EAAE,CAyGpF"}
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import type { GateLedgerPort } from "../op/gate.js";
|
|
2
2
|
export declare const GATE_AUDIT_KINDS: readonly ["approval-requested", "approved", "override-requested", "override-recorded", "revoked", "applied", "override-spent"];
|
|
3
3
|
export type GateAuditKind = (typeof GATE_AUDIT_KINDS)[number];
|
|
4
|
-
/** How an approval was
|
|
4
|
+
/** How an approval was authorised: a forge review, an ssh seal, or neither. A review wins; a review that is also sealed has `sealed: true` beside it. */
|
|
5
5
|
export type GateAuditSeal = "sealed" | "review" | "unsigned";
|
|
6
|
+
/** A commit that added or removed a ledger line: its date and its author's name and email. */
|
|
7
|
+
export interface GateLedgerCommit {
|
|
8
|
+
at: string;
|
|
9
|
+
by?: string;
|
|
10
|
+
email?: string;
|
|
11
|
+
}
|
|
6
12
|
/**
|
|
7
13
|
* One line of a ledger file. Give the `text` as it is on disk, or the parsed
|
|
8
|
-
* `record` a {@link GateLedgerPort} returns.
|
|
9
|
-
*
|
|
14
|
+
* `record` a {@link GateLedgerPort} returns. `line` is its line number in the
|
|
15
|
+
* file, when known. A line read from history carries the commit that `added`
|
|
16
|
+
* it, or, if a commit removed it, `removed`, each with when and by whom.
|
|
10
17
|
*/
|
|
11
18
|
export interface GateLedgerLine {
|
|
12
19
|
text?: string;
|
|
13
20
|
record?: unknown;
|
|
21
|
+
line?: number;
|
|
14
22
|
commit?: string;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
by?: string;
|
|
18
|
-
};
|
|
23
|
+
added?: GateLedgerCommit;
|
|
24
|
+
removed?: GateLedgerCommit;
|
|
19
25
|
}
|
|
20
26
|
/** One op's ledger file. */
|
|
21
27
|
export interface GateAuditLedger {
|
|
@@ -50,7 +56,13 @@ export interface GateAuditSource {
|
|
|
50
56
|
/** The ledger file, or the apply's `ref` for an apply. */
|
|
51
57
|
path: string;
|
|
52
58
|
op?: string;
|
|
59
|
+
/** Ledger entries: the line's `line`, else its 1-based place in the ledger's `lines`. */
|
|
60
|
+
line?: number;
|
|
61
|
+
/** Ledger entries: the line's text exactly as passed in, when it was passed as text. */
|
|
62
|
+
text?: string;
|
|
53
63
|
commit?: string;
|
|
64
|
+
/** The commit that added the line, or for a revocation removed it, when the line came from history. */
|
|
65
|
+
author?: GateLedgerCommit;
|
|
54
66
|
change: "added" | "removed" | "apply";
|
|
55
67
|
}
|
|
56
68
|
export interface GateAuditEntry {
|
|
@@ -65,10 +77,14 @@ export interface GateAuditEntry {
|
|
|
65
77
|
digest?: string;
|
|
66
78
|
/** Who: the approver or overrider, whoever removed the line, or for an apply, whoever ran it. */
|
|
67
79
|
by?: string;
|
|
68
|
-
/** Resolutions and revocations:
|
|
80
|
+
/** Resolutions and revocations: reviewed, sealed, or unsigned. */
|
|
69
81
|
seal?: GateAuditSeal;
|
|
82
|
+
/** The line holds an ssh seal, whatever `seal` says (a sealed review is `seal: "review"`). */
|
|
83
|
+
sealed?: true;
|
|
70
84
|
/** The ssh signer, when sealed. */
|
|
71
85
|
signer?: string;
|
|
86
|
+
/** Resolutions read from history: the author of the commit that added the line, who may not be `by`. */
|
|
87
|
+
committedBy?: string;
|
|
72
88
|
/** Who carried the approval to chant (`chant approve --relayed-by`). */
|
|
73
89
|
relayedBy?: string;
|
|
74
90
|
/** A person or an agent, and the roles claimed. */
|
|
@@ -111,6 +127,12 @@ export interface GateAuditOptions {
|
|
|
111
127
|
isOverride?: (op: string, gate: string) => boolean;
|
|
112
128
|
/** Whether approvals of a gate need a seal or a review. Default none. */
|
|
113
129
|
sealed?: (op: string, gate: string) => boolean;
|
|
130
|
+
/**
|
|
131
|
+
* The id to give an entry, from the entry with its default id. Default
|
|
132
|
+
* that id. A product with ids of its own keys by `entry.source` (its
|
|
133
|
+
* `path`, `line`, `text`). `under` refers to the ids this returns.
|
|
134
|
+
*/
|
|
135
|
+
id?: (entry: GateAuditEntry) => string;
|
|
114
136
|
}
|
|
115
137
|
export interface GateAuditInput extends GateAuditOptions {
|
|
116
138
|
ledgers: readonly GateAuditLedger[];
|
|
@@ -124,15 +146,17 @@ export declare function deriveGateAudit(input: GateAuditInput): {
|
|
|
124
146
|
entries: GateAuditEntry[];
|
|
125
147
|
gaps: GateAuditGap[];
|
|
126
148
|
};
|
|
127
|
-
/** The lines of one ledger file's text, all added. Blank lines are skipped. */
|
|
149
|
+
/** The lines of one ledger file's text, all added, each with its line number. Blank lines are skipped. */
|
|
128
150
|
export declare function gateLedgerLines(content: string): GateLedgerLine[];
|
|
129
151
|
/**
|
|
130
152
|
* The lines each commit added to and removed from a ledger file, oldest first,
|
|
131
153
|
* from `git log -p --unified=0 --no-renames` written with
|
|
132
|
-
* `--format=%x1e%H%x1f%an%x1f%aI` over the file
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
154
|
+
* `--format=%x1e%H%x1f%an%x1f%aI` over the file (append `%x1f%ae` for the
|
|
155
|
+
* author's email). The caller runs git; this reads the text. Each line keeps
|
|
156
|
+
* its commit and that commit's date and author, as `added` or `removed`. A
|
|
157
|
+
* line removed and added again in one commit (a newline fixed at the end of
|
|
158
|
+
* the file) is neither. A removed line is what {@link deriveGateAudit}
|
|
159
|
+
* reports as a revocation.
|
|
136
160
|
*/
|
|
137
161
|
export declare function gateLedgerLinesFromLog(log: string): GateLedgerLine[];
|
|
138
162
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate-audit.d.ts","sourceRoot":"","sources":["../../src/lifecycle/gate-audit.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gate-audit.d.ts","sourceRoot":"","sources":["../../src/lifecycle/gate-audit.ts"],"names":[],"mappings":"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"}
|
|
@@ -48,13 +48,25 @@ export declare function isModelAuthored(origin: GateOrigin | undefined): boolean
|
|
|
48
48
|
* the ledger exactly like a name a person gave. This is the honest value.
|
|
49
49
|
*/
|
|
50
50
|
export declare const UNATTESTED_APPROVER = "unattested";
|
|
51
|
+
/**
|
|
52
|
+
* The environment variable a model-authored channel carries into child
|
|
53
|
+
* processes (chant#3817), as a steward's turn carries `CHANT_STEWARD`: a
|
|
54
|
+
* `chant workspace points answer` or `chant approve` a process on MCP or ACP
|
|
55
|
+
* starts is that channel's too. Only `mcp` and `acp` are read from it, so it
|
|
56
|
+
* can make a process model-authored and never make one a person's.
|
|
57
|
+
*/
|
|
58
|
+
export declare const GATE_ORIGIN_ENV = "CHANT_GATE_ORIGIN";
|
|
51
59
|
/**
|
|
52
60
|
* Declare the channel this process serves. Called once at an entry point, not
|
|
53
61
|
* per call: `chant mcp` sets `"mcp"` before it serves anything, `chant acp`
|
|
54
|
-
* sets `"acp"`, and the CLI leaves the default.
|
|
62
|
+
* sets `"acp"`, and the CLI leaves the default. A model-authored channel is
|
|
63
|
+
* also exported as {@link GATE_ORIGIN_ENV}, so a child process knows it too.
|
|
55
64
|
*/
|
|
56
65
|
export declare function setGateOrigin(origin: GateOrigin): void;
|
|
57
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* The channel this process serves. `"cli"` unless an entry point said
|
|
68
|
+
* otherwise, or this process was started by one on MCP or ACP.
|
|
69
|
+
*/
|
|
58
70
|
export declare function currentGateOrigin(): GateOrigin;
|
|
59
71
|
/** Restore the default. For tests, which must not leak a channel into each other. */
|
|
60
72
|
export declare function resetGateOrigin(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate-origin.d.ts","sourceRoot":"","sources":["../../src/lifecycle/gate-origin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,gDAAgD;AAChD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAY/C,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,OAAO,CAEvE;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,eAAe,CAAC;AAIhD
|
|
1
|
+
{"version":3,"file":"gate-origin.d.ts","sourceRoot":"","sources":["../../src/lifecycle/gate-origin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,gDAAgD;AAChD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAY/C,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,OAAO,CAEvE;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,eAAe,CAAC;AAIhD;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,sBAAsB,CAAC;AAEnD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAGtD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,UAAU,CAI9C;AAED,qFAAqF;AACrF,wBAAgB,eAAe,IAAI,IAAI,CAGtC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAC/B,aAAa,EAAE,UAAU,GAAG,SAAS,EACrC,gBAAgB,EAAE,UAAU,GAC3B,MAAM,GAAG,SAAS,CAOpB"}
|
|
@@ -46,11 +46,12 @@ import { type KindView } from "./records-write.js";
|
|
|
46
46
|
import { type Asked, type Escalation, type ModelAsk, type Point } from "./points.js";
|
|
47
47
|
import { type TrustPolicy } from "./trust/policy.js";
|
|
48
48
|
import type { SourceVia } from "./source-block.js";
|
|
49
|
+
import { type GateOrigin } from "../lifecycle/gate-origin.js";
|
|
49
50
|
/** The version of the write documents `points ask`, `points answer` and `points retract` print. */
|
|
50
51
|
export declare const POINTS_WRITE_CONTRACT_VERSION = 1;
|
|
51
52
|
export declare const POINTS_WRITE_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/points-write/v1/points-write.schema.json";
|
|
52
53
|
/** Why `points ask`, `points answer` or `points retract` wrote nothing. Closed: a reader may switch on it. */
|
|
53
|
-
export declare const POINTS_WRITE_ERROR_CODES: readonly ["kind-unreadable", "kind-invalid", "schema-unreadable", "schema-id-mismatch", "schema-invalid", "location-missing", "write-usage-invalid", "write-input-invalid", "points-undeclared", "points-invalid", "point-unknown", "point-inputs-invalid", "point-decider-failed", "answer-not-candidate", "quorum-not-met", "record-not-found", "record-closed", "record-id-taken", "answer-in-steward-turn", "principal-unidentified", "answer-not-answered", "answer-field-unsupported", "point-candidates-invalid", "write-lock-timeout", "write-lock-not-held", "record-unparseable", "record-schema-invalid", "record-id-duplicate", "record-supersedes-unknown", "record-supersedes-conflict", "record-remediates-unknown", "record-remediates-not-closed", "record-seal-mismatch", "session-seal-mismatch", "session-verdict-unknown-record"];
|
|
54
|
+
export declare const POINTS_WRITE_ERROR_CODES: readonly ["kind-unreadable", "kind-invalid", "schema-unreadable", "schema-id-mismatch", "schema-invalid", "location-missing", "write-usage-invalid", "write-input-invalid", "points-undeclared", "points-invalid", "point-unknown", "point-inputs-invalid", "point-decider-failed", "answer-not-candidate", "quorum-not-met", "record-not-found", "record-closed", "record-id-taken", "answer-in-steward-turn", "answer-never-over-mcp", "principal-unidentified", "answer-not-answered", "answer-field-unsupported", "point-candidates-invalid", "write-lock-timeout", "write-lock-not-held", "record-unparseable", "record-schema-invalid", "record-id-duplicate", "record-supersedes-unknown", "record-supersedes-conflict", "record-remediates-unknown", "record-remediates-not-closed", "record-seal-mismatch", "session-seal-mismatch", "session-verdict-unknown-record"];
|
|
54
55
|
export type PointsWriteErrorCode = (typeof POINTS_WRITE_ERROR_CODES)[number];
|
|
55
56
|
export declare class PointsWriteError extends Error {
|
|
56
57
|
readonly code: PointsWriteErrorCode;
|
|
@@ -277,6 +278,13 @@ export interface AnswerPointOptions {
|
|
|
277
278
|
kind?: string;
|
|
278
279
|
on?: string;
|
|
279
280
|
dryRun?: boolean;
|
|
281
|
+
/**
|
|
282
|
+
* The channel the answer came through (#3817). Defaults to this process's
|
|
283
|
+
* (`currentGateOrigin`). Over MCP or ACP nobody `by` names counts toward
|
|
284
|
+
* the quorum, and a question that constrains a `neverOverMcp` gate is
|
|
285
|
+
* refused outright.
|
|
286
|
+
*/
|
|
287
|
+
origin?: GateOrigin;
|
|
280
288
|
}
|
|
281
289
|
/**
|
|
282
290
|
* Whether an answer kind's schema copy takes the fields of #3351 (`note` on an
|
|
@@ -328,6 +336,8 @@ export interface RetractAnswerOptions {
|
|
|
328
336
|
kind?: string;
|
|
329
337
|
on?: string;
|
|
330
338
|
dryRun?: boolean;
|
|
339
|
+
/** The channel the retraction came through, counted as for an answer (#3817). */
|
|
340
|
+
origin?: GateOrigin;
|
|
331
341
|
}
|
|
332
342
|
/**
|
|
333
343
|
* People take an answer back: `points retract` (#3351, ws-084). The answer
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decide.d.ts","sourceRoot":"","sources":["../../src/workspace/decide.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAMH,OAAO,EAA4E,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAE3I,OAAO,EAWL,KAAK,QAAQ,EAEd,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAcL,KAAK,KAAK,EAEV,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,KAAK,EACX,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"decide.d.ts","sourceRoot":"","sources":["../../src/workspace/decide.ts"],"names":[],"mappings":"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"}
|
|
@@ -132,8 +132,9 @@ export declare const REASONS: {
|
|
|
132
132
|
readonly "point-inputs-invalid": "The inputs given to an ask are not a JSON object of the point's declared inputs.";
|
|
133
133
|
readonly "point-decider-failed": "A model decider that fails closed (unreachable: fail) could not answer, so the ask wrote nothing.";
|
|
134
134
|
readonly "answer-not-candidate": "The people's answer is not one of the question's candidates.";
|
|
135
|
-
readonly "quorum-not-met": "Too few of the people who answered count toward the point's quorum: distinct, not holding the agent role, not the steward that asked, and holding one of its roles when it names any.";
|
|
135
|
+
readonly "quorum-not-met": "Too few of the people who answered count toward the point's quorum: distinct, not holding the agent role, not the steward that asked, and holding one of its roles when it names any. Nobody named over MCP or ACP counts (#3817).";
|
|
136
136
|
readonly "answer-in-steward-turn": "The answer was given during a steward's turn, or by a process it started: a steward never answers a decision point, and a person answers it through hud or at a shell.";
|
|
137
|
+
readonly "answer-never-over-mcp": "The answer or retraction came over MCP or ACP, or from a process a session on one started, and the question constrains a gate that is never resolved there: a person answers it at a shell or through hud (#3817).";
|
|
137
138
|
readonly "answer-not-answered": "points retract names a question that has no answer to retract: it is escalated or proposed, and people answer it instead (#3351).";
|
|
138
139
|
readonly "answer-field-unsupported": "The answer kind's copy of point-answer.schema.json predates a field the write needs, a note, a relay, a retraction or an ad-hoc question's asked: copy the schema anew to turn it on (#3351, #3402, #3403).";
|
|
139
140
|
readonly "point-candidates-invalid": "points ask names an ad-hoc point without --candidates, a declared point with them, or candidates that are not a question and criteria of the point's question type (#3403).";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reason-codes.d.ts","sourceRoot":"","sources":["../../src/workspace/reason-codes.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"reason-codes.d.ts","sourceRoot":"","sources":["../../src/workspace/reason-codes.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
* `clientInfo`. Computed provenance, from git, is unchanged.
|
|
29
29
|
*
|
|
30
30
|
* Nothing here listens on a port or authenticates anyone (ws-052): the server
|
|
31
|
-
* speaks over stdio, and `by` is recorded as given, as `--by` is.
|
|
31
|
+
* speaks over stdio, and `by` is recorded as given, as `--by` is. A decision
|
|
32
|
+
* point's answer is the exception (#3817): `chant serve mcp` makes every call
|
|
33
|
+
* the model's (`lifecycle/gate-origin.ts`), so `points-answer` and
|
|
34
|
+
* `points-retract` count nobody `by` names toward a point's quorum, as
|
|
35
|
+
* `op-approve` records its approver as an agent.
|
|
32
36
|
*
|
|
33
37
|
* An agent session (#2548): a server started with `CHANT_AGENT` set writes in
|
|
34
38
|
* that session, bound to its one member, and its writes are refused outside
|
|
@@ -289,7 +293,7 @@ export const workspaceWriteTools: ToolDefinition[] = [
|
|
|
289
293
|
{
|
|
290
294
|
name: "points-answer",
|
|
291
295
|
description:
|
|
292
|
-
"Record people's answer to an open decision point question, or confirm a model's proposal: chant workspace points answer. Refused unless the point's quorum is met: distinct people, none holding the agent role, each holding one of the quorum's roles when it names any. An answered question never changes in place: points-retract takes it back. " +
|
|
296
|
+
"Record people's answer to an open decision point question, or confirm a model's proposal: chant workspace points answer. Refused unless the point's quorum is met: distinct people, none holding the agent role, each holding one of the quorum's roles when it names any. On a server started with `chant serve mcp`, every call is the model's, so nobody named in by counts toward the quorum (quorum-not-met), and a question whose gate is neverOverMcp is refused outright (answer-never-over-mcp): people answer at a shell or through hud. An answered question never changes in place: points-retract takes it back. " +
|
|
293
297
|
PROTOCOL,
|
|
294
298
|
inputSchema: {
|
|
295
299
|
type: "object",
|
|
@@ -308,7 +312,7 @@ export const workspaceWriteTools: ToolDefinition[] = [
|
|
|
308
312
|
{
|
|
309
313
|
name: "points-retract",
|
|
310
314
|
description:
|
|
311
|
-
"Take back people's answer to a decision point question: chant workspace points retract. The question is escalated to people again, and the answer, who gave it and its note stay in the record's retractions with who retracted it, when and why. Refused unless the point's quorum is met, counted as for an answer. " +
|
|
315
|
+
"Take back people's answer to a decision point question: chant workspace points retract. The question is escalated to people again, and the answer, who gave it and its note stay in the record's retractions with who retracted it, when and why. Refused unless the point's quorum is met, counted as for an answer, so over MCP it is refused as an answer is. " +
|
|
312
316
|
PROTOCOL,
|
|
313
317
|
inputSchema: {
|
|
314
318
|
type: "object",
|
|
@@ -84,6 +84,48 @@ describe("lifecycle/gate-audit", () => {
|
|
|
84
84
|
expect(entries.filter((e) => e.kind === "approved")).toHaveLength(1);
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
test("each ledger entry names its line: path, number and text as passed in", () => {
|
|
88
|
+
const text = `${pending()}\n\n${resolution()}\n`;
|
|
89
|
+
const { entries } = deriveGateAudit({ ledgers: [{ op: "tf-apply", path: "gates/tf.jsonl", lines: gateLedgerLines(text) }] });
|
|
90
|
+
expect(entries.map((e) => [e.source.path, e.source.line, e.source.text])).toEqual([
|
|
91
|
+
["gates/tf.jsonl", 1, pending()],
|
|
92
|
+
["gates/tf.jsonl", 3, resolution()],
|
|
93
|
+
]);
|
|
94
|
+
const records = deriveGateAudit({ ledgers: [{ op: "tf-apply", lines: [{ record: JSON.parse(pending()) }, { record: JSON.parse(resolution()) }] }] });
|
|
95
|
+
expect(records.entries.map((e) => [e.source.line, e.source.text])).toEqual([[1, undefined], [2, undefined]]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("a caller's id replaces the default, and applies run under it", () => {
|
|
99
|
+
const ledgers = [ledger("tf-apply", pending(), resolution())];
|
|
100
|
+
const id = (e: { source: { path: string; text?: string }; kind: string; id: string }) => (e.source.text ? `mine:${e.source.path}:${e.source.text.length}` : e.id);
|
|
101
|
+
const plain = deriveGateAudit({ ledgers });
|
|
102
|
+
const { entries } = deriveGateAudit({ ledgers, id, applies: [{ ref: "reports/1", at: "2026-10-01T12:00:00.000Z", gate: "wave-1", digest: D1 }] });
|
|
103
|
+
const approved = entries.find((e) => e.kind === "approved")!;
|
|
104
|
+
expect(approved.id).toBe(`mine:_gates/tf-apply.jsonl:${resolution().length}`);
|
|
105
|
+
expect(entries.find((e) => e.kind === "applied")?.under).toBe(approved.id);
|
|
106
|
+
expect(plain.entries.map((e) => e.id)).not.toContain(approved.id);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("an approval read from history names who committed it", () => {
|
|
110
|
+
const log = `\x1ec1\x1fmallory\x1f2026-10-01T11:05:00+00:00\x1fm@x.io\n\ndiff --git a/x b/x\n+${pending()}\n+${resolution()}\n`;
|
|
111
|
+
const lines = gateLedgerLinesFromLog(log);
|
|
112
|
+
expect(lines[1]).toEqual({ text: resolution(), commit: "c1", added: { at: "2026-10-01T11:05:00.000Z", by: "mallory", email: "m@x.io" } });
|
|
113
|
+
const { entries } = deriveGateAudit({ ledgers: [{ op: "tf-apply", lines }] });
|
|
114
|
+
const approved = entries.find((e) => e.kind === "approved")!;
|
|
115
|
+
expect(approved).toMatchObject({ by: "alex", committedBy: "mallory", source: { commit: "c1", author: { by: "mallory", email: "m@x.io", at: "2026-10-01T11:05:00.000Z" } } });
|
|
116
|
+
expect(entries.find((e) => e.kind === "approval-requested")?.source.author?.by).toBe("mallory");
|
|
117
|
+
const noEmail = gateLedgerLinesFromLog(`\x1ec2\x1fbo\x1f2026-10-01T11:05:00+00:00\n\n+${resolution()}\n`);
|
|
118
|
+
expect(noEmail[0]?.added).toEqual({ at: "2026-10-01T11:05:00.000Z", by: "bo" });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("a sealed pull request approval is a review, with the seal beside it", () => {
|
|
122
|
+
const seal = { signer: "bot", signature: "-----BEGIN" };
|
|
123
|
+
const { entries } = deriveGateAudit({ ledgers: [ledger("tf-apply", pending(), resolution({ seal, via: "pr-review", pr: 4 }), resolution({ seal, timestamp: "2026-10-01T12:00:00.000Z" }))] });
|
|
124
|
+
const approved = entries.filter((e) => e.kind === "approved");
|
|
125
|
+
expect(approved.map((e) => [e.seal, e.sealed, e.signer])).toEqual([["review", true, "bot"], ["sealed", true, "bot"]]);
|
|
126
|
+
expect(approved[0]?.review).toEqual({ pr: 4 });
|
|
127
|
+
});
|
|
128
|
+
|
|
87
129
|
test("applies: under an approval, without one, waived, and spending an override", () => {
|
|
88
130
|
const ledgers = [ledger("tf-apply", pending(), resolution()), ledger("policy-override", resolution({ op: "policy-override", gate: "root/a", planDigest: "sha256:ov" }))];
|
|
89
131
|
const ok = { ref: "reports/1", at: "2026-10-01T12:00:00.000Z", gate: "wave-1", digest: D1 };
|
|
@@ -47,6 +47,33 @@
|
|
|
47
47
|
* - `override`: an apply cites an override that no line holds.
|
|
48
48
|
* - `malformed-line`: a line that is not a gate record. Its content is lost
|
|
49
49
|
* to the trail, so it is said.
|
|
50
|
+
*
|
|
51
|
+
* ## Sources and ids
|
|
52
|
+
*
|
|
53
|
+
* Every ledger entry's `source` names the line it came from: `path`, `line`
|
|
54
|
+
* (the line's own `line` when given, which {@link gateLedgerLines} fills
|
|
55
|
+
* with the file's line number, else its 1-based place in `ledger.lines`),
|
|
56
|
+
* and `text`, exactly as passed in, when the line was given as text. When the
|
|
57
|
+
* line came from {@link gateLedgerLinesFromLog}, `source.author` is the
|
|
58
|
+
* author of the commit that added it (or, for a revocation, removed it), and
|
|
59
|
+
* resolutions also carry it as `committedBy`. Under `approval: ledger` that
|
|
60
|
+
* is the only check on who pushed an approval: anyone who can push may write
|
|
61
|
+
* any `resolvedBy`.
|
|
62
|
+
*
|
|
63
|
+
* The default `id` is a digest over the source and the record. A product
|
|
64
|
+
* that already holds ids of its own (an append-only record whose ids
|
|
65
|
+
* customers keep) passes {@link GateAuditOptions.id}, which gets each entry
|
|
66
|
+
* with its default id and returns the id to use. `under` links follow it.
|
|
67
|
+
*
|
|
68
|
+
* ## Seal and review
|
|
69
|
+
*
|
|
70
|
+
* `seal` names how the approval came to be: `review` when the line came
|
|
71
|
+
* through a forge review (`via: "pr-review"`, or a `review` field), else
|
|
72
|
+
* `sealed` when it carries an ssh seal, else `unsigned`. A pull request
|
|
73
|
+
* approval that `chant approve` also sealed is still `review`: the review is
|
|
74
|
+
* what authorised it, and the seal attests who wrote the line. That fact is
|
|
75
|
+
* kept apart as `sealed: true` with its `signer`, on any entry whose line
|
|
76
|
+
* holds a seal. terragucci classifies the same way.
|
|
50
77
|
*/
|
|
51
78
|
import { canonicalJson } from "../effect-receipt";
|
|
52
79
|
import { contentDigest } from "../content-digest";
|
|
@@ -64,19 +91,29 @@ export const GATE_AUDIT_KINDS = [
|
|
|
64
91
|
] as const;
|
|
65
92
|
export type GateAuditKind = (typeof GATE_AUDIT_KINDS)[number];
|
|
66
93
|
|
|
67
|
-
/** How an approval was
|
|
94
|
+
/** How an approval was authorised: a forge review, an ssh seal, or neither. A review wins; a review that is also sealed has `sealed: true` beside it. */
|
|
68
95
|
export type GateAuditSeal = "sealed" | "review" | "unsigned";
|
|
69
96
|
|
|
97
|
+
/** A commit that added or removed a ledger line: its date and its author's name and email. */
|
|
98
|
+
export interface GateLedgerCommit {
|
|
99
|
+
at: string;
|
|
100
|
+
by?: string;
|
|
101
|
+
email?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
70
104
|
/**
|
|
71
105
|
* One line of a ledger file. Give the `text` as it is on disk, or the parsed
|
|
72
|
-
* `record` a {@link GateLedgerPort} returns.
|
|
73
|
-
*
|
|
106
|
+
* `record` a {@link GateLedgerPort} returns. `line` is its line number in the
|
|
107
|
+
* file, when known. A line read from history carries the commit that `added`
|
|
108
|
+
* it, or, if a commit removed it, `removed`, each with when and by whom.
|
|
74
109
|
*/
|
|
75
110
|
export interface GateLedgerLine {
|
|
76
111
|
text?: string;
|
|
77
112
|
record?: unknown;
|
|
113
|
+
line?: number;
|
|
78
114
|
commit?: string;
|
|
79
|
-
|
|
115
|
+
added?: GateLedgerCommit;
|
|
116
|
+
removed?: GateLedgerCommit;
|
|
80
117
|
}
|
|
81
118
|
|
|
82
119
|
/** One op's ledger file. */
|
|
@@ -111,7 +148,13 @@ export interface GateAuditSource {
|
|
|
111
148
|
/** The ledger file, or the apply's `ref` for an apply. */
|
|
112
149
|
path: string;
|
|
113
150
|
op?: string;
|
|
151
|
+
/** Ledger entries: the line's `line`, else its 1-based place in the ledger's `lines`. */
|
|
152
|
+
line?: number;
|
|
153
|
+
/** Ledger entries: the line's text exactly as passed in, when it was passed as text. */
|
|
154
|
+
text?: string;
|
|
114
155
|
commit?: string;
|
|
156
|
+
/** The commit that added the line, or for a revocation removed it, when the line came from history. */
|
|
157
|
+
author?: GateLedgerCommit;
|
|
115
158
|
change: "added" | "removed" | "apply";
|
|
116
159
|
}
|
|
117
160
|
|
|
@@ -127,10 +170,14 @@ export interface GateAuditEntry {
|
|
|
127
170
|
digest?: string;
|
|
128
171
|
/** Who: the approver or overrider, whoever removed the line, or for an apply, whoever ran it. */
|
|
129
172
|
by?: string;
|
|
130
|
-
/** Resolutions and revocations:
|
|
173
|
+
/** Resolutions and revocations: reviewed, sealed, or unsigned. */
|
|
131
174
|
seal?: GateAuditSeal;
|
|
175
|
+
/** The line holds an ssh seal, whatever `seal` says (a sealed review is `seal: "review"`). */
|
|
176
|
+
sealed?: true;
|
|
132
177
|
/** The ssh signer, when sealed. */
|
|
133
178
|
signer?: string;
|
|
179
|
+
/** Resolutions read from history: the author of the commit that added the line, who may not be `by`. */
|
|
180
|
+
committedBy?: string;
|
|
134
181
|
/** Who carried the approval to chant (`chant approve --relayed-by`). */
|
|
135
182
|
relayedBy?: string;
|
|
136
183
|
/** A person or an agent, and the roles claimed. */
|
|
@@ -168,6 +215,12 @@ export interface GateAuditOptions {
|
|
|
168
215
|
isOverride?: (op: string, gate: string) => boolean;
|
|
169
216
|
/** Whether approvals of a gate need a seal or a review. Default none. */
|
|
170
217
|
sealed?: (op: string, gate: string) => boolean;
|
|
218
|
+
/**
|
|
219
|
+
* The id to give an entry, from the entry with its default id. Default
|
|
220
|
+
* that id. A product with ids of its own keys by `entry.source` (its
|
|
221
|
+
* `path`, `line`, `text`). `under` refers to the ids this returns.
|
|
222
|
+
*/
|
|
223
|
+
id?: (entry: GateAuditEntry) => string;
|
|
171
224
|
}
|
|
172
225
|
|
|
173
226
|
export interface GateAuditInput extends GateAuditOptions {
|
|
@@ -205,11 +258,11 @@ function rowOf(line: GateLedgerLine): Row | undefined {
|
|
|
205
258
|
return v && typeof v === "object" && !Array.isArray(v) ? (v as Row) : undefined;
|
|
206
259
|
}
|
|
207
260
|
|
|
208
|
-
function sealOf(r: Row): { seal: GateAuditSeal; signer?: string } {
|
|
261
|
+
function sealOf(r: Row): { seal: GateAuditSeal; sealed?: true; signer?: string } {
|
|
209
262
|
const s = r.seal as { signature?: unknown; signer?: unknown } | null | undefined;
|
|
210
|
-
|
|
211
|
-
if (r.review || r.via === "pr-review") return { seal: "review" };
|
|
212
|
-
return { seal: "unsigned" };
|
|
263
|
+
const signed = s && s.signature ? { sealed: true as const, ...(str(s.signer) ? { signer: str(s.signer) } : {}) } : undefined;
|
|
264
|
+
if (r.review || r.via === "pr-review") return { seal: "review", ...signed };
|
|
265
|
+
return signed ? { seal: "sealed", ...signed } : { seal: "unsigned" };
|
|
213
266
|
}
|
|
214
267
|
|
|
215
268
|
function reviewOf(r: Row): GateAuditEntry["review"] {
|
|
@@ -246,13 +299,15 @@ const compatible = (a: string | undefined, b: string | undefined): boolean => a
|
|
|
246
299
|
/** The entries `input` adds up to, oldest first, and the gaps they show. */
|
|
247
300
|
export function deriveGateAudit(input: GateAuditInput): { entries: GateAuditEntry[]; gaps: GateAuditGap[] } {
|
|
248
301
|
const isOverride = input.isOverride ?? isOverrideGate;
|
|
302
|
+
const rekey = input.id;
|
|
303
|
+
const keyed = (e: GateAuditEntry): GateAuditEntry => (rekey ? { ...e, id: rekey(e) } : e);
|
|
249
304
|
const entries: GateAuditEntry[] = [];
|
|
250
305
|
const gaps: GateAuditGap[] = [];
|
|
251
306
|
|
|
252
307
|
for (const ledger of input.ledgers) {
|
|
253
308
|
const path = ledger.path ?? `_gates/${ledger.op}.jsonl`;
|
|
254
|
-
const parsed: { line: GateLedgerLine; r: Row; pending: boolean }[] = [];
|
|
255
|
-
for (const line of ledger.lines) {
|
|
309
|
+
const parsed: { line: GateLedgerLine; n: number; r: Row; pending: boolean }[] = [];
|
|
310
|
+
for (const [i, line] of ledger.lines.entries()) {
|
|
256
311
|
const r = rowOf(line);
|
|
257
312
|
const pending = r?.kind === "pending";
|
|
258
313
|
const ok = r && r.version === 1 && typeof r.gate === "string" && typeof r.timestamp === "string" && (pending || typeof r.resolvedBy === "string");
|
|
@@ -260,7 +315,7 @@ export function deriveGateAudit(input: GateAuditInput): { entries: GateAuditEntr
|
|
|
260
315
|
gaps.push({ kind: "malformed-line", op: ledger.op, detail: `${path} holds a line that is not a gate record${line.commit ? ` (commit ${line.commit})` : ""}: ${(line.text ?? JSON.stringify(line.record) ?? "").slice(0, 80)}` });
|
|
261
316
|
continue;
|
|
262
317
|
}
|
|
263
|
-
parsed.push({ line, r, pending });
|
|
318
|
+
parsed.push({ line, n: line.line ?? i + 1, r, pending });
|
|
264
319
|
}
|
|
265
320
|
|
|
266
321
|
// Each denial an override answers: the newest pending fact of its gate and digest.
|
|
@@ -273,53 +328,63 @@ export function deriveGateAudit(input: GateAuditInput): { entries: GateAuditEntr
|
|
|
273
328
|
requests.push({ op: ledger.op, gate: String(p.r.gate), at: time(String(p.r.timestamp)), ...(digest ? { digest } : {}), ...(str(p.r.environment) ? { environment: str(p.r.environment) } : {}) });
|
|
274
329
|
}
|
|
275
330
|
|
|
276
|
-
for (const { line, r, pending } of parsed) {
|
|
331
|
+
for (const { line, n, r, pending } of parsed) {
|
|
277
332
|
const gate = String(r.gate);
|
|
278
333
|
const override = isOverride(ledger.op, gate);
|
|
279
334
|
const removed = line.removed;
|
|
280
335
|
if (pending && removed) continue;
|
|
281
336
|
const digest = str(r.planDigest);
|
|
282
|
-
const
|
|
337
|
+
const author = removed ?? line.added;
|
|
338
|
+
const source: GateAuditSource = {
|
|
339
|
+
path,
|
|
340
|
+
op: ledger.op,
|
|
341
|
+
line: n,
|
|
342
|
+
...(line.text !== undefined ? { text: line.text } : {}),
|
|
343
|
+
...(line.commit ? { commit: line.commit } : {}),
|
|
344
|
+
...(author ? { author } : {}),
|
|
345
|
+
change: removed ? "removed" : "added",
|
|
346
|
+
};
|
|
283
347
|
const id = idOf(removed ? "removed" : "ledger", path, line.commit ?? null, r);
|
|
284
348
|
const extra = Object.fromEntries(Object.entries(r).filter(([k]) => !KNOWN.has(k)));
|
|
285
349
|
const base = { id, gate, op: ledger.op, ...(digest ? { digest } : {}), source } as const;
|
|
286
350
|
|
|
287
351
|
if (pending) {
|
|
288
|
-
entries.push({
|
|
352
|
+
entries.push(keyed({
|
|
289
353
|
...base,
|
|
290
354
|
kind: override ? "override-requested" : "approval-requested",
|
|
291
355
|
at: String(r.timestamp),
|
|
292
356
|
...drop({ runId: str(r.runId), expiresAt: str(r.expiresAt), description: str(r.description), url: str(r.url), rules: strings(r.rules) }),
|
|
293
357
|
detail: extra,
|
|
294
|
-
});
|
|
358
|
+
}));
|
|
295
359
|
continue;
|
|
296
360
|
}
|
|
297
361
|
|
|
298
|
-
const { seal, signer } = sealOf(r);
|
|
362
|
+
const { seal, sealed, signer } = sealOf(r);
|
|
299
363
|
if (removed) {
|
|
300
|
-
entries.push({
|
|
364
|
+
entries.push(keyed({
|
|
301
365
|
...base,
|
|
302
366
|
kind: "revoked",
|
|
303
367
|
at: removed.at,
|
|
304
368
|
...(removed.by ? { by: removed.by } : {}),
|
|
305
369
|
subject: override ? "override" : "approval",
|
|
306
370
|
seal,
|
|
371
|
+
...(sealed ? { sealed } : {}),
|
|
307
372
|
detail: { ...extra, approvedBy: r.resolvedBy, approvedAt: r.timestamp, ...(signer ? { signer } : {}) },
|
|
308
|
-
});
|
|
373
|
+
}));
|
|
309
374
|
continue;
|
|
310
375
|
}
|
|
311
376
|
|
|
312
377
|
const denial = override ? denials.get(`${gate}\n${digest ?? ""}`) : undefined;
|
|
313
378
|
const rules = strings(denial?.rules) ?? (override && gate.startsWith("override-") ? [gate.slice("override-".length)] : undefined);
|
|
314
|
-
const entry: GateAuditEntry = {
|
|
379
|
+
const entry: GateAuditEntry = keyed({
|
|
315
380
|
...base,
|
|
316
381
|
kind: override ? "override-recorded" : "approved",
|
|
317
382
|
at: String(r.timestamp),
|
|
318
383
|
by: String(r.resolvedBy),
|
|
319
384
|
seal,
|
|
320
|
-
...drop({ signer, relayedBy: str(r.relayedBy), approver: approverOf(r), note: str(r.note), url: str(r.url), review: reviewOf(r), rules }),
|
|
385
|
+
...drop({ sealed, signer, committedBy: line.added?.by, relayedBy: str(r.relayedBy), approver: approverOf(r), note: str(r.note), url: str(r.url), review: reviewOf(r), rules }),
|
|
321
386
|
detail: extra,
|
|
322
|
-
};
|
|
387
|
+
});
|
|
323
388
|
entries.push(entry);
|
|
324
389
|
|
|
325
390
|
if (!override) {
|
|
@@ -349,7 +414,7 @@ export function deriveGateAudit(input: GateAuditInput): { entries: GateAuditEntr
|
|
|
349
414
|
if (a.digest !== undefined && a.approval !== "not-required" && !approval) {
|
|
350
415
|
gaps.push({ kind: "approval", ...(a.op ? { op: a.op } : {}), gate: a.gate, digest: a.digest, detail: `${a.ref} applied under ${a.digest}, but the ledger holds no approval of ${a.gate} for that digest before ${a.at}` });
|
|
351
416
|
}
|
|
352
|
-
entries.push({
|
|
417
|
+
entries.push(keyed({
|
|
353
418
|
...base,
|
|
354
419
|
id: idOf("apply", a.ref, a.at, a.gate, a.digest ?? null),
|
|
355
420
|
kind: "applied",
|
|
@@ -357,7 +422,7 @@ export function deriveGateAudit(input: GateAuditInput): { entries: GateAuditEntr
|
|
|
357
422
|
...(a.by ? { by: a.by } : {}),
|
|
358
423
|
...(approval ? { under: approval.id } : {}),
|
|
359
424
|
detail: { ...(a.detail ?? {}), approval: a.approval ?? "required" },
|
|
360
|
-
});
|
|
425
|
+
}));
|
|
361
426
|
|
|
362
427
|
for (const o of a.overrides ?? []) {
|
|
363
428
|
const recorded = find("override-recorded", o.gate, o.digest, false);
|
|
@@ -365,7 +430,7 @@ export function deriveGateAudit(input: GateAuditInput): { entries: GateAuditEntr
|
|
|
365
430
|
gaps.push({ kind: "override", ...(a.op ? { op: a.op } : {}), gate: o.gate, ...(o.digest ? { digest: o.digest } : {}), detail: `${a.ref} cites the override ${o.gate}${o.digest ? ` for ${o.digest}` : ""}, but the ledger holds none` });
|
|
366
431
|
continue;
|
|
367
432
|
}
|
|
368
|
-
entries.push({
|
|
433
|
+
entries.push(keyed({
|
|
369
434
|
...base,
|
|
370
435
|
id: idOf("spent", a.ref, a.at, recorded.id),
|
|
371
436
|
kind: "override-spent",
|
|
@@ -375,7 +440,7 @@ export function deriveGateAudit(input: GateAuditInput): { entries: GateAuditEntr
|
|
|
375
440
|
...(recorded.digest ? { digest: recorded.digest } : {}),
|
|
376
441
|
under: recorded.id,
|
|
377
442
|
detail: {},
|
|
378
|
-
});
|
|
443
|
+
}));
|
|
379
444
|
}
|
|
380
445
|
}
|
|
381
446
|
|
|
@@ -383,24 +448,26 @@ export function deriveGateAudit(input: GateAuditInput): { entries: GateAuditEntr
|
|
|
383
448
|
return { entries, gaps };
|
|
384
449
|
}
|
|
385
450
|
|
|
386
|
-
/** The lines of one ledger file's text, all added. Blank lines are skipped. */
|
|
451
|
+
/** The lines of one ledger file's text, all added, each with its line number. Blank lines are skipped. */
|
|
387
452
|
export function gateLedgerLines(content: string): GateLedgerLine[] {
|
|
388
|
-
return content.split("\n").
|
|
453
|
+
return content.split("\n").flatMap((l, i) => (l.trim() ? [{ text: l.trim(), line: i + 1 }] : []));
|
|
389
454
|
}
|
|
390
455
|
|
|
391
456
|
/**
|
|
392
457
|
* The lines each commit added to and removed from a ledger file, oldest first,
|
|
393
458
|
* from `git log -p --unified=0 --no-renames` written with
|
|
394
|
-
* `--format=%x1e%H%x1f%an%x1f%aI` over the file
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
459
|
+
* `--format=%x1e%H%x1f%an%x1f%aI` over the file (append `%x1f%ae` for the
|
|
460
|
+
* author's email). The caller runs git; this reads the text. Each line keeps
|
|
461
|
+
* its commit and that commit's date and author, as `added` or `removed`. A
|
|
462
|
+
* line removed and added again in one commit (a newline fixed at the end of
|
|
463
|
+
* the file) is neither. A removed line is what {@link deriveGateAudit}
|
|
464
|
+
* reports as a revocation.
|
|
398
465
|
*/
|
|
399
466
|
export function gateLedgerLinesFromLog(log: string): GateLedgerLine[] {
|
|
400
467
|
const out: GateLedgerLine[] = [];
|
|
401
468
|
for (const block of log.split("\x1e").slice(1)) {
|
|
402
469
|
const [head = "", ...rest] = block.split("\n");
|
|
403
|
-
const [commit = "", author = "", date = ""] = head.split("\x1f");
|
|
470
|
+
const [commit = "", author = "", date = "", email = ""] = head.split("\x1f");
|
|
404
471
|
const net = new Map<string, number>();
|
|
405
472
|
const order: string[] = [];
|
|
406
473
|
for (const l of rest) {
|
|
@@ -412,10 +479,11 @@ export function gateLedgerLinesFromLog(log: string): GateLedgerLine[] {
|
|
|
412
479
|
}
|
|
413
480
|
const when = Date.parse(date.trim());
|
|
414
481
|
const at = Number.isNaN(when) ? date.trim() : new Date(when).toISOString();
|
|
482
|
+
const by: GateLedgerCommit = { at, ...(author ? { by: author } : {}), ...(email.trim() ? { email: email.trim() } : {}) };
|
|
415
483
|
for (const text of order) {
|
|
416
484
|
const n = net.get(text)!;
|
|
417
485
|
for (let i = 0; i < Math.abs(n); i++) {
|
|
418
|
-
out.push(
|
|
486
|
+
out.push({ text, commit: commit.trim(), ...(n > 0 ? { added: { ...by } } : { removed: { ...by } }) });
|
|
419
487
|
}
|
|
420
488
|
}
|
|
421
489
|
}
|
|
@@ -66,23 +66,40 @@ export const UNATTESTED_APPROVER = "unattested";
|
|
|
66
66
|
|
|
67
67
|
let ambient: GateOrigin = "cli";
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The environment variable a model-authored channel carries into child
|
|
71
|
+
* processes (chant#3817), as a steward's turn carries `CHANT_STEWARD`: a
|
|
72
|
+
* `chant workspace points answer` or `chant approve` a process on MCP or ACP
|
|
73
|
+
* starts is that channel's too. Only `mcp` and `acp` are read from it, so it
|
|
74
|
+
* can make a process model-authored and never make one a person's.
|
|
75
|
+
*/
|
|
76
|
+
export const GATE_ORIGIN_ENV = "CHANT_GATE_ORIGIN";
|
|
77
|
+
|
|
69
78
|
/**
|
|
70
79
|
* Declare the channel this process serves. Called once at an entry point, not
|
|
71
80
|
* per call: `chant mcp` sets `"mcp"` before it serves anything, `chant acp`
|
|
72
|
-
* sets `"acp"`, and the CLI leaves the default.
|
|
81
|
+
* sets `"acp"`, and the CLI leaves the default. A model-authored channel is
|
|
82
|
+
* also exported as {@link GATE_ORIGIN_ENV}, so a child process knows it too.
|
|
73
83
|
*/
|
|
74
84
|
export function setGateOrigin(origin: GateOrigin): void {
|
|
75
85
|
ambient = origin;
|
|
86
|
+
if (isModelAuthored(origin)) process.env[GATE_ORIGIN_ENV] = origin;
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* The channel this process serves. `"cli"` unless an entry point said
|
|
91
|
+
* otherwise, or this process was started by one on MCP or ACP.
|
|
92
|
+
*/
|
|
79
93
|
export function currentGateOrigin(): GateOrigin {
|
|
80
|
-
return ambient;
|
|
94
|
+
if (ambient !== "cli") return ambient;
|
|
95
|
+
const inherited = process.env[GATE_ORIGIN_ENV];
|
|
96
|
+
return inherited === "mcp" || inherited === "acp" ? inherited : ambient;
|
|
81
97
|
}
|
|
82
98
|
|
|
83
99
|
/** Restore the default. For tests, which must not leak a channel into each other. */
|
|
84
100
|
export function resetGateOrigin(): void {
|
|
85
101
|
ambient = "cli";
|
|
102
|
+
delete process.env[GATE_ORIGIN_ENV];
|
|
86
103
|
}
|
|
87
104
|
|
|
88
105
|
/**
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3817: a gate that asks a decision point is not passed by an answer sent
|
|
3
|
+
* over MCP or ACP. The answer there is an agent's, so nobody it names counts
|
|
4
|
+
* toward a human quorum and the gate keeps waiting; a question whose gate is
|
|
5
|
+
* `neverOverMcp` is refused outright. The same answer from a shell answers
|
|
6
|
+
* it, and the next run passes.
|
|
7
|
+
*
|
|
8
|
+
* The workspace is real (a git repository with the reference workspace's
|
|
9
|
+
* answer kind, a points file and a trust policy at base). The MCP answer goes
|
|
10
|
+
* through the `points-answer` tool's own handler with this process serving
|
|
11
|
+
* MCP, the shell answer through `runWorkspacePoints`. The gate ledger the run
|
|
12
|
+
* writes is the in-memory port; the `neverOverMcp` pending fact is on the
|
|
13
|
+
* workspace's own `chant/lifecycle`, where `points answer` reads it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from "vitest";
|
|
19
|
+
import { cleanScratch, git, workspace } from "../workspace/__fixtures__/decide-workspace";
|
|
20
|
+
import { runWorkspacePoints } from "../workspace/points-cli";
|
|
21
|
+
import type { CommandContext } from "../cli/registry";
|
|
22
|
+
import { createWorkspaceTools } from "../cli/mcp/workspace-tools";
|
|
23
|
+
import { GATE_ORIGIN_ENV, resetGateOrigin, setGateOrigin } from "../lifecycle/gate-origin";
|
|
24
|
+
import { appendPendingGate } from "../lifecycle/gate-ledger";
|
|
25
|
+
import { ApplyOp } from "./composites/apply-op";
|
|
26
|
+
import { memoryGateLedgerPort } from "./gate";
|
|
27
|
+
import { runOpLocally } from "./local-executor";
|
|
28
|
+
import type { ActivityFn } from "./activity-registry";
|
|
29
|
+
import type { OpConfig } from "./types";
|
|
30
|
+
|
|
31
|
+
const PLAN = `jcs1-sha256:${"3".repeat(64)}`;
|
|
32
|
+
|
|
33
|
+
const POINTS = {
|
|
34
|
+
points: {
|
|
35
|
+
"prod-apply": {
|
|
36
|
+
title: "Apply this plan to prod",
|
|
37
|
+
question: { type: "noul", instructions: "Should this plan be applied to prod?", criteria: { true: "Apply it.", false: "Do not apply it." } },
|
|
38
|
+
inputs: { "gate.component": "the Op the gate belongs to", "gate.planDigest": "the plan the gate binds" },
|
|
39
|
+
deciders: [{ kind: "quorum", count: 2, roles: ["lead"] }],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let root: string;
|
|
45
|
+
const applied: string[] = [];
|
|
46
|
+
const activities = new Map<string, ActivityFn>([
|
|
47
|
+
["chantBuild", async () => ({ ok: true })],
|
|
48
|
+
["lifecycleDiff", async () => ({ planDigest: PLAN, drifted: true })],
|
|
49
|
+
["nativeApply", async () => {
|
|
50
|
+
applied.push(PLAN);
|
|
51
|
+
return { ok: true };
|
|
52
|
+
}],
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const config = (name: string): OpConfig =>
|
|
56
|
+
(ApplyOp({ name, env: "prod", target: "kubectl", gate: { gate: "approve-apply", point: "prod-apply" } }).op as unknown as { props: OpConfig }).props;
|
|
57
|
+
|
|
58
|
+
type Doc = Record<string, unknown> & { error?: { code: string; message: string } };
|
|
59
|
+
|
|
60
|
+
/** The `points-answer` and `points-retract` tools, as `chant serve mcp` serves them in the workspace. */
|
|
61
|
+
function mcpTool(name: "points-answer" | "points-retract"): (params: Record<string, unknown>) => Promise<Doc> {
|
|
62
|
+
const tool = createWorkspaceTools({ cwd: root }).find((t) => t.definition.name === name)!;
|
|
63
|
+
return async (params) => (await tool.handler(params, {} as never)) as Doc;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** `chant workspace points <verb> <id> ...` through the command's handler, in the workspace. Returns the document it prints. */
|
|
67
|
+
async function shell(verb: "answer" | "retract", id: string, by: string[], answer?: string): Promise<Doc> {
|
|
68
|
+
const printed: string[] = [];
|
|
69
|
+
const log = vi.spyOn(console, "log").mockImplementation((line: unknown) => {
|
|
70
|
+
printed.push(String(line));
|
|
71
|
+
});
|
|
72
|
+
const err = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
73
|
+
const was = process.cwd();
|
|
74
|
+
process.chdir(root);
|
|
75
|
+
try {
|
|
76
|
+
const ctx = { args: { extraPositional: verb, extraPositional2: id, ...(answer !== undefined ? { answer } : {}), bys: by, json: true }, plugins: [], serializers: [] } as unknown as CommandContext;
|
|
77
|
+
await runWorkspacePoints(ctx);
|
|
78
|
+
} finally {
|
|
79
|
+
process.chdir(was);
|
|
80
|
+
log.mockRestore();
|
|
81
|
+
err.mockRestore();
|
|
82
|
+
}
|
|
83
|
+
return JSON.parse(printed.join("\n")) as Doc;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
beforeAll(() => {
|
|
87
|
+
root = workspace(POINTS);
|
|
88
|
+
mkdirSync(join(root, ".chant"), { recursive: true });
|
|
89
|
+
writeFileSync(join(root, ".chant", "trust.json"), JSON.stringify({ schema: 1, roles: { agent: ["bot"], lead: ["alice", "bob"] } }));
|
|
90
|
+
git(root, "add", "-A");
|
|
91
|
+
git(root, "commit", "-q", "-m", "trust");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
afterEach(() => resetGateOrigin());
|
|
95
|
+
afterAll(cleanScratch);
|
|
96
|
+
|
|
97
|
+
describe("an answer over MCP or ACP never passes a gate that needs a human quorum (#3817)", () => {
|
|
98
|
+
test("points-answer over MCP naming the quorum's people does not answer; the gate keeps waiting; the same answer from a shell passes it", async () => {
|
|
99
|
+
const port = memoryGateLedgerPort();
|
|
100
|
+
const op = config("prod-apply");
|
|
101
|
+
|
|
102
|
+
const first = await runOpLocally(op, activities, {}, undefined, { gates: port, cwd: root, now: "2026-10-11T10:00:00.000Z" });
|
|
103
|
+
expect(first.status).toBe("waiting");
|
|
104
|
+
const id = first.point!.id;
|
|
105
|
+
|
|
106
|
+
// This process now serves MCP: alice and bob both hold lead, and neither counts.
|
|
107
|
+
setGateOrigin("mcp");
|
|
108
|
+
const overMcp = await mcpTool("points-answer")({ id, answer: true, by: ["alice", "bob"] });
|
|
109
|
+
expect(overMcp.error?.code).toBe("quorum-not-met");
|
|
110
|
+
expect(overMcp.error?.message).toContain("alice, who was named over MCP");
|
|
111
|
+
expect(overMcp.error?.message).toContain("bob, who was named over MCP");
|
|
112
|
+
|
|
113
|
+
// ACP is the same, and so is `chant workspace points answer` in a process a session on either started.
|
|
114
|
+
setGateOrigin("acp");
|
|
115
|
+
expect((await mcpTool("points-answer")({ id, answer: true, by: ["alice", "bob"] })).error?.code).toBe("quorum-not-met");
|
|
116
|
+
resetGateOrigin();
|
|
117
|
+
process.env[GATE_ORIGIN_ENV] = "mcp";
|
|
118
|
+
expect((await shell("answer", id, ["alice", "bob"], "yes")).error?.code).toBe("quorum-not-met");
|
|
119
|
+
resetGateOrigin();
|
|
120
|
+
|
|
121
|
+
const still = await runOpLocally(op, activities, {}, undefined, { gates: port, cwd: root, now: "2026-10-11T10:05:00.000Z" });
|
|
122
|
+
expect(still.status).toBe("waiting");
|
|
123
|
+
expect(still.point?.id).toBe(id);
|
|
124
|
+
expect(applied).toEqual([]);
|
|
125
|
+
expect(port.resolved).toHaveLength(0);
|
|
126
|
+
|
|
127
|
+
// From a shell the same people answer it, and the next run passes on the answer.
|
|
128
|
+
const answered = await shell("answer", id, ["alice", "bob"], "yes");
|
|
129
|
+
expect(answered).toMatchObject({ verb: "answer", id, written: true, question: { state: "answered", answer: true, answeredBy: ["alice", "bob"] } });
|
|
130
|
+
const passed = await runOpLocally(op, activities, {}, undefined, { gates: port, cwd: root, now: "2026-10-11T10:10:00.000Z" });
|
|
131
|
+
expect(passed.status).toBe("ok");
|
|
132
|
+
expect(applied).toEqual([PLAN]);
|
|
133
|
+
|
|
134
|
+
// Taking the answer back follows the same rule.
|
|
135
|
+
setGateOrigin("mcp");
|
|
136
|
+
expect((await mcpTool("points-retract")({ id, by: ["alice", "bob"] })).error?.code).toBe("quorum-not-met");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("a question whose gate is neverOverMcp is refused over MCP outright, and answered from a shell", async () => {
|
|
140
|
+
const port = memoryGateLedgerPort();
|
|
141
|
+
const op = config("tf-apply");
|
|
142
|
+
|
|
143
|
+
const first = await runOpLocally(op, activities, {}, undefined, { gates: port, cwd: root, now: "2026-10-11T11:00:00.000Z" });
|
|
144
|
+
expect(first.status).toBe("waiting");
|
|
145
|
+
const id = first.point!.id;
|
|
146
|
+
// The gate's pending fact on chant/lifecycle says it is never resolved over MCP, as terragucci's tf-apply records it.
|
|
147
|
+
await appendPendingGate({ op: "tf-apply", gate: "approve-apply", timestamp: "2026-10-11T11:00:00.000Z", expiresAt: "2026-10-13T11:00:00.000Z", planDigest: PLAN, neverOverMcp: true, answer: { point: "prod-apply", id, path: first.point!.path } }, { cwd: root });
|
|
148
|
+
|
|
149
|
+
setGateOrigin("mcp");
|
|
150
|
+
const overMcp = await mcpTool("points-answer")({ id, answer: true, by: ["alice", "bob"] });
|
|
151
|
+
expect(overMcp.error?.code).toBe("answer-never-over-mcp");
|
|
152
|
+
expect(overMcp.error?.message).toContain('gate "approve-apply" on "tf-apply"');
|
|
153
|
+
resetGateOrigin();
|
|
154
|
+
|
|
155
|
+
const answered = await shell("answer", id, ["alice", "bob"], "yes");
|
|
156
|
+
expect(answered).toMatchObject({ verb: "answer", id, written: true, question: { state: "answered" } });
|
|
157
|
+
|
|
158
|
+
setGateOrigin("acp");
|
|
159
|
+
expect((await mcpTool("points-retract")({ id, by: ["alice", "bob"] })).error?.code).toBe("answer-never-over-mcp");
|
|
160
|
+
});
|
|
161
|
+
});
|
package/src/workspace/decide.ts
CHANGED
|
@@ -93,6 +93,7 @@ import { IdentityError, refuseUnidentified } from "./identity";
|
|
|
93
93
|
import { scopeSource } from "./write-scope";
|
|
94
94
|
import { RefCASConflictError } from "../lifecycle/git";
|
|
95
95
|
import { readLedgerAnswers, refreshLedger, withLedgerAnswers, writeLedgerAnswer, type LedgerAnswers } from "./answers-ledger";
|
|
96
|
+
import { currentGateOrigin, isModelAuthored, type GateOrigin } from "../lifecycle/gate-origin";
|
|
96
97
|
|
|
97
98
|
/** The version of the write documents `points ask`, `points answer` and `points retract` print. */
|
|
98
99
|
export const POINTS_WRITE_CONTRACT_VERSION = 1;
|
|
@@ -122,6 +123,8 @@ export const POINTS_WRITE_ERROR_CODES = [
|
|
|
122
123
|
"record-id-taken",
|
|
123
124
|
/** The answer was given during a steward's turn (#2749): a steward never answers a question, its own or another's. */
|
|
124
125
|
"answer-in-steward-turn",
|
|
126
|
+
/** The answer or retraction came over MCP or ACP, and the question constrains a gate that is never resolved there (#3817). */
|
|
127
|
+
"answer-never-over-mcp",
|
|
125
128
|
/** An answerer is named by a bare name, and identity.attribution at base is identified (#3163). */
|
|
126
129
|
"principal-unidentified",
|
|
127
130
|
/** `points retract` names a question that has no answer: escalated or proposed (#3351). */
|
|
@@ -715,6 +718,13 @@ export interface AnswerPointOptions {
|
|
|
715
718
|
kind?: string;
|
|
716
719
|
on?: string;
|
|
717
720
|
dryRun?: boolean;
|
|
721
|
+
/**
|
|
722
|
+
* The channel the answer came through (#3817). Defaults to this process's
|
|
723
|
+
* (`currentGateOrigin`). Over MCP or ACP nobody `by` names counts toward
|
|
724
|
+
* the quorum, and a question that constrains a `neverOverMcp` gate is
|
|
725
|
+
* refused outright.
|
|
726
|
+
*/
|
|
727
|
+
origin?: GateOrigin;
|
|
718
728
|
}
|
|
719
729
|
|
|
720
730
|
/**
|
|
@@ -821,20 +831,83 @@ async function findQuestion(cwd: string, kind: string | undefined, id: string):
|
|
|
821
831
|
throw new PointsWriteError("record-not-found", `no answer record has id ${id}`);
|
|
822
832
|
}
|
|
823
833
|
|
|
824
|
-
/**
|
|
825
|
-
|
|
826
|
-
|
|
834
|
+
/** A model-authored channel's name, as a person reads it. */
|
|
835
|
+
const channelName = (origin: GateOrigin): string => (origin === "acp" ? "ACP" : "MCP");
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Count `by` toward the point's quorum, or refuse with `quorum-not-met`.
|
|
839
|
+
*
|
|
840
|
+
* Over MCP or ACP (#3817) the names are the model's to choose, as a gate's
|
|
841
|
+
* approver is (`lifecycle/gate-origin.ts`), so the answer is an agent's and
|
|
842
|
+
* nobody it names counts: a person answers at a shell or through hud.
|
|
843
|
+
*/
|
|
844
|
+
function meetQuorum(name: string, by: string[], quorum: { count: number; roles?: string[] }, root: string, steward: string | undefined, verb: "answer" | "retract", origin: GateOrigin): { counted: string[] } {
|
|
845
|
+
const overModel = isModelAuthored(origin);
|
|
846
|
+
const tally = overModel
|
|
847
|
+
? { counted: [] as string[], left: by.map((n) => ({ name: n, why: `was named over ${channelName(origin)}, where an answer is an agent's` })), met: false }
|
|
848
|
+
: tallyQuorum(by, quorum, policyFor(root), steward);
|
|
827
849
|
if (!tally.met) {
|
|
828
850
|
const got = tally.counted.length;
|
|
829
851
|
const left = tally.left.length ? `; not counted: ${tally.left.map((l) => `${l.name}, who ${l.why}`).join("; ")}` : "";
|
|
852
|
+
const remedy = overModel ? `: a person ${verb === "answer" ? "answers" : "retracts it"} at a shell with \`chant workspace points ${verb}\`, or through hud` : "";
|
|
830
853
|
throw new PointsWriteError(
|
|
831
854
|
"quorum-not-met",
|
|
832
|
-
`${name} needs ${quorum.count} ${quorum.count === 1 ? "person" : "people"}${quorum.roles ? ` holding ${quorum.roles.join(" or ")}` : ""} to ${verb === "answer" ? "answer" : "retract an answer"}, and ${got} ${got === 1 ? "counts" : "count"}${got ? ` (${tally.counted.join(", ")})` : ""}${left}`,
|
|
855
|
+
`${name} needs ${quorum.count} ${quorum.count === 1 ? "person" : "people"}${quorum.roles ? ` holding ${quorum.roles.join(" or ")}` : ""} to ${verb === "answer" ? "answer" : "retract an answer"}, and ${got} ${got === 1 ? "counts" : "count"}${got ? ` (${tally.counted.join(", ")})` : ""}${left}${remedy}`,
|
|
833
856
|
);
|
|
834
857
|
}
|
|
835
858
|
return tally;
|
|
836
859
|
}
|
|
837
860
|
|
|
861
|
+
/** The gates a question's `constrains` names, as `gate:<op>/<gate>` with an optional `@<env>` (`op/gate-point.ts`'s `gateSubject`). */
|
|
862
|
+
function constrainedGates(d: Record<string, unknown>): { op: string; gate: string }[] {
|
|
863
|
+
const out: { op: string; gate: string }[] = [];
|
|
864
|
+
for (const c of Array.isArray(d.constrains) ? d.constrains : []) {
|
|
865
|
+
if (typeof c !== "string" || !c.startsWith("gate:")) continue;
|
|
866
|
+
const rest = c.slice("gate:".length);
|
|
867
|
+
const at = rest.lastIndexOf("@");
|
|
868
|
+
const named = at > 0 ? rest.slice(0, at) : rest;
|
|
869
|
+
const slash = named.indexOf("/");
|
|
870
|
+
if (slash > 0 && slash < named.length - 1) out.push({ op: named.slice(0, slash), gate: named.slice(slash + 1) });
|
|
871
|
+
}
|
|
872
|
+
return out;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Refuse an answer or retraction over MCP or ACP to a question that
|
|
877
|
+
* constrains a gate never resolved there (#3817, #3485): the gate-only op
|
|
878
|
+
* names chant knows, a pending fact on the gate ledger that says so (as
|
|
879
|
+
* terragucci's records do), or the Op's own `neverOverMcp`. `op-approve`
|
|
880
|
+
* refuses the same gates.
|
|
881
|
+
*/
|
|
882
|
+
async function refuseNeverOverMcp(id: string, d: Record<string, unknown>, root: string, origin: GateOrigin, does: "answers" | "retracts"): Promise<void> {
|
|
883
|
+
if (!isModelAuthored(origin)) return;
|
|
884
|
+
const gates = constrainedGates(d);
|
|
885
|
+
if (gates.length === 0) return;
|
|
886
|
+
const { isNeverOverMcpGateOp } = await import("../op/gate-name");
|
|
887
|
+
const { readGateLedger } = await import("../lifecycle/gate-ledger");
|
|
888
|
+
let configs: Map<string, { config: { neverOverMcp?: boolean } }> | undefined;
|
|
889
|
+
for (const { op, gate } of gates) {
|
|
890
|
+
let never = isNeverOverMcpGateOp(op);
|
|
891
|
+
if (!never) {
|
|
892
|
+
const { pending } = await readGateLedger(op, { cwd: root }).catch(() => ({ pending: [] as { gate: string; neverOverMcp?: true }[] }));
|
|
893
|
+
never = pending.some((p) => p.gate === gate && p.neverOverMcp === true);
|
|
894
|
+
}
|
|
895
|
+
if (!never) {
|
|
896
|
+
if (configs === undefined) {
|
|
897
|
+
const { discoverOps } = await import("../op/discover");
|
|
898
|
+
configs = await discoverOps({ cwd: root }).then((r) => r.ops as Map<string, { config: { neverOverMcp?: boolean } }>).catch(() => new Map());
|
|
899
|
+
}
|
|
900
|
+
never = Boolean(configs.get(op)?.config.neverOverMcp);
|
|
901
|
+
}
|
|
902
|
+
if (never) {
|
|
903
|
+
throw new PointsWriteError(
|
|
904
|
+
"answer-never-over-mcp",
|
|
905
|
+
`${id} constrains gate "${gate}" on "${op}", which is never resolved over ${channelName(origin)}, so its question is not ${does === "answers" ? "answered" : "retracted"} there either: a person ${does === "answers" ? "answers" : "retracts the answer to"} it at a shell with \`chant workspace points ${does === "answers" ? "answer" : "retract"} ${id}\`, or through hud`,
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
838
911
|
/**
|
|
839
912
|
* People answer an open question: `points answer`. A proposed question the
|
|
840
913
|
* people answer as the model did is confirmed, and keeps the model as its
|
|
@@ -884,8 +957,10 @@ async function answerPointLocked(opts: AnswerPointOptions): Promise<PointsWriteD
|
|
|
884
957
|
if (value === undefined || !allowed.includes(value)) {
|
|
885
958
|
throw new PointsWriteError("answer-not-candidate", `${opts.id} takes one of ${allowed.map((c) => JSON.stringify(c)).join(", ")}, not ${JSON.stringify(opts.answer)}`);
|
|
886
959
|
}
|
|
960
|
+
const origin = opts.origin ?? currentGateOrigin();
|
|
961
|
+
await refuseNeverOverMcp(opts.id, d, o.root, origin, "answers");
|
|
887
962
|
const quorum = quorumOf(point);
|
|
888
|
-
const tally = meetQuorum(name, opts.by, quorum, o.root, askedByOf(d)?.steward, "answer");
|
|
963
|
+
const tally = meetQuorum(name, opts.by, quorum, o.root, askedByOf(d)?.steward, "answer", origin);
|
|
889
964
|
const on = opts.on ?? today();
|
|
890
965
|
const decider = d.decider as Record<string, unknown>;
|
|
891
966
|
const confirmed = target.state === "proposed" && decider.kind === "model" && d.answer === value;
|
|
@@ -960,6 +1035,8 @@ export interface RetractAnswerOptions {
|
|
|
960
1035
|
kind?: string;
|
|
961
1036
|
on?: string;
|
|
962
1037
|
dryRun?: boolean;
|
|
1038
|
+
/** The channel the retraction came through, counted as for an answer (#3817). */
|
|
1039
|
+
origin?: GateOrigin;
|
|
963
1040
|
}
|
|
964
1041
|
|
|
965
1042
|
/**
|
|
@@ -998,8 +1075,10 @@ async function retractAnswerLocked(opts: RetractAnswerOptions): Promise<PointsWr
|
|
|
998
1075
|
const name = String(d.point);
|
|
999
1076
|
const point = pointOf(opened.points, name);
|
|
1000
1077
|
if (!point) throw new PointsWriteError("point-unknown", `${opts.id} answers ${name}, which ${opened.pointsFile} no longer declares, so no quorum can take the answer back: ask the point as it is declared now`);
|
|
1078
|
+
const origin = opts.origin ?? currentGateOrigin();
|
|
1079
|
+
await refuseNeverOverMcp(opts.id, d, o.root, origin, "retracts");
|
|
1001
1080
|
const quorum = quorumOf(point);
|
|
1002
|
-
const tally = meetQuorum(name, opts.by, quorum, o.root, askedByOf(d)?.steward, "retract");
|
|
1081
|
+
const tally = meetQuorum(name, opts.by, quorum, o.root, askedByOf(d)?.steward, "retract", origin);
|
|
1003
1082
|
const on = opts.on ?? today();
|
|
1004
1083
|
const type = String(d.question_type);
|
|
1005
1084
|
const decider = d.decider !== null && typeof d.decider === "object" && !Array.isArray(d.decider) ? (d.decider as Record<string, unknown>) : { kind: "quorum" };
|
|
@@ -157,8 +157,9 @@ export const REASONS = {
|
|
|
157
157
|
"point-inputs-invalid": "The inputs given to an ask are not a JSON object of the point's declared inputs.",
|
|
158
158
|
"point-decider-failed": "A model decider that fails closed (unreachable: fail) could not answer, so the ask wrote nothing.",
|
|
159
159
|
"answer-not-candidate": "The people's answer is not one of the question's candidates.",
|
|
160
|
-
"quorum-not-met": "Too few of the people who answered count toward the point's quorum: distinct, not holding the agent role, not the steward that asked, and holding one of its roles when it names any.",
|
|
160
|
+
"quorum-not-met": "Too few of the people who answered count toward the point's quorum: distinct, not holding the agent role, not the steward that asked, and holding one of its roles when it names any. Nobody named over MCP or ACP counts (#3817).",
|
|
161
161
|
"answer-in-steward-turn": "The answer was given during a steward's turn, or by a process it started: a steward never answers a decision point, and a person answers it through hud or at a shell.",
|
|
162
|
+
"answer-never-over-mcp": "The answer or retraction came over MCP or ACP, or from a process a session on one started, and the question constrains a gate that is never resolved there: a person answers it at a shell or through hud (#3817).",
|
|
162
163
|
"answer-not-answered": "points retract names a question that has no answer to retract: it is escalated or proposed, and people answer it instead (#3351).",
|
|
163
164
|
"answer-field-unsupported": "The answer kind's copy of point-answer.schema.json predates a field the write needs, a note, a relay, a retraction or an ad-hoc question's asked: copy the schema anew to turn it on (#3351, #3402, #3403).",
|
|
164
165
|
"point-candidates-invalid": "points ask names an ad-hoc point without --candidates, a declared point with them, or candidates that are not a question and criteria of the point's question type (#3403).",
|