@intentius/chant-lexicon-fountain 0.124.31 → 0.124.32
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/README.md +18 -22
- package/dist/acp/command-line.js +134 -0
- package/dist/acp/host.js +78 -0
- package/dist/acp/index.js +58 -0
- package/dist/acp/jsonrpc.js +198 -0
- package/dist/acp/output.js +50 -0
- package/dist/acp/protocol.js +24 -0
- package/dist/acp/serve.js +61 -0
- package/dist/acp/server.js +267 -0
- package/dist/acp/turn.js +262 -0
- package/dist/allowlist-refs.js +121 -0
- package/dist/audit-entities.js +43 -0
- package/dist/codegen/docs-cli.js +6 -0
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/docs.js +130 -0
- package/dist/codegen/generate-cli.js +7 -0
- package/dist/codegen/generate-lexicon.d.ts +4 -5
- package/dist/codegen/generate-lexicon.d.ts.map +1 -1
- package/dist/codegen/generate-lexicon.js +41 -0
- package/dist/codegen/generate-typescript.js +45 -0
- package/dist/codegen/generate.js +85 -0
- package/dist/codegen/naming.d.ts.map +1 -1
- package/dist/codegen/naming.js +33 -0
- package/dist/codegen/package.js +33 -0
- package/dist/composites/box.d.ts +35 -25
- package/dist/composites/box.d.ts.map +1 -1
- package/dist/composites/box.js +216 -0
- package/dist/composites/concierge-stack.d.ts +1 -1
- package/dist/composites/concierge-stack.d.ts.map +1 -1
- package/dist/composites/concierge-stack.js +38 -0
- package/dist/composites/steward.d.ts +45 -51
- package/dist/composites/steward.d.ts.map +1 -1
- package/dist/composites/steward.js +203 -0
- package/dist/config.d.ts +27 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +113 -0
- package/dist/coverage.d.ts.map +1 -1
- package/dist/coverage.js +191 -0
- package/dist/deep-observe-hooks.d.ts +3 -20
- package/dist/deep-observe-hooks.d.ts.map +1 -1
- package/dist/deep-observe-hooks.js +214 -0
- package/dist/deep-observe.d.ts +3 -15
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/deep-observe.js +248 -0
- package/dist/describe-resources.d.ts +7 -9
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/describe-resources.js +129 -0
- package/dist/detect.js +44 -0
- package/dist/entity-props.js +24 -0
- package/dist/export-resources.d.ts +6 -15
- package/dist/export-resources.d.ts.map +1 -1
- package/dist/export-resources.js +124 -0
- package/dist/generated/index.d.ts +0 -3
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/generated/index.js +7 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/import/generator.js +114 -0
- package/dist/import/local-agents.js +351 -0
- package/dist/import/parser.d.ts +5 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/import/parser.js +87 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -0
- package/dist/init-templates.d.ts +3 -1
- package/dist/init-templates.d.ts.map +1 -1
- package/dist/init-templates.js +214 -0
- package/dist/integrity.json +9 -11
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-catalog.js +52 -0
- package/dist/lint/audit-lineage.js +6 -0
- package/dist/lint/post-synth/ftn010-networking-explicit.js +32 -0
- package/dist/lint/post-synth/ftn011-no-unrestricted-networking.js +33 -0
- package/dist/lint/post-synth/ftn012-no-cloud-credential-env.js +50 -0
- package/dist/lint/post-synth/ftn013-substitution-resolvable.js +88 -0
- package/dist/lint/post-synth/ftn014-vault-shadowing.js +57 -0
- package/dist/lint/post-synth/ftn015-mcp-secret-env-substitution.js +45 -0
- package/dist/lint/post-synth/ftn016-runtime-model-valid.d.ts.map +1 -1
- package/dist/lint/post-synth/ftn016-runtime-model-valid.js +78 -0
- package/dist/lint/post-synth/ftn017-unique-resource-names.js +39 -0
- package/dist/lint/post-synth/ftn023-acp-runtime-command.js +50 -0
- package/dist/lint/post-synth/ftn024-setup-timeout-range.js +41 -0
- package/dist/lint/post-synth/ftn025-removed-upstream.d.ts +3 -0
- package/dist/lint/post-synth/ftn025-removed-upstream.d.ts.map +1 -0
- package/dist/lint/post-synth/ftn025-removed-upstream.js +51 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/index.js +24 -0
- package/dist/lint/rules/ftn001-no-secret-literals.js +95 -0
- package/dist/lint/rules/index.js +4 -0
- package/dist/live-identity.d.ts +11 -55
- package/dist/live-identity.d.ts.map +1 -1
- package/dist/live-identity.js +129 -0
- package/dist/lsp/completions.js +9 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +57 -0
- package/dist/lsp/lexicon-index.d.ts.map +1 -1
- package/dist/lsp/lexicon-index.js +40 -0
- package/dist/manifest.json +2 -2
- package/dist/meta.json +2 -68
- package/dist/okf/index.md +1 -6
- package/dist/okf/rules/FTN025.md +15 -0
- package/dist/okf/types/Agent.md +6 -7
- package/dist/op/activities/fountain-apply.d.ts +9 -14
- package/dist/op/activities/fountain-apply.d.ts.map +1 -1
- package/dist/op/activities/fountain-apply.js +341 -0
- package/dist/op/activities/fountain-prompt.d.ts +5 -6
- package/dist/op/activities/fountain-prompt.d.ts.map +1 -1
- package/dist/op/activities/fountain-prompt.js +127 -0
- package/dist/op/activities/fountain-run.d.ts +25 -62
- package/dist/op/activities/fountain-run.d.ts.map +1 -1
- package/dist/op/activities/fountain-run.js +188 -0
- package/dist/op/activities/index.d.ts +1 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +10 -0
- package/dist/op/run-prompt.js +97 -0
- package/dist/op/runtime.d.ts +21 -10
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtime.js +1205 -0
- package/dist/package-cli.js +20 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +175 -0
- package/dist/reference-catalog.js +25 -0
- package/dist/retired.d.ts +36 -0
- package/dist/retired.d.ts.map +1 -0
- package/dist/retired.js +48 -0
- package/dist/rules/ftn016-runtime-model-valid.ts +6 -5
- package/dist/rules/ftn025-removed-upstream.ts +65 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +178 -0
- package/dist/skills/chant-fountain-locked-sandboxes.md +1 -1
- package/dist/skills/chant-fountain-ops.md +42 -41
- package/dist/skills/chant-fountain.md +4 -4
- package/dist/spec/fetch.d.ts +8 -3
- package/dist/spec/fetch.d.ts.map +1 -1
- package/dist/spec/fetch.js +98 -0
- package/dist/spec/parse.d.ts +9 -8
- package/dist/spec/parse.d.ts.map +1 -1
- package/dist/spec/parse.js +363 -0
- package/dist/types/index.d.ts +7 -67
- package/dist/validate-cli.js +13 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +33 -0
- package/package.json +7 -7
- package/src/codegen/docs.ts +22 -15
- package/src/codegen/generate-lexicon.ts +4 -5
- package/src/codegen/naming.ts +0 -3
- package/src/codegen/package.ts +2 -2
- package/src/composites/box.ts +37 -28
- package/src/composites/composites.test.ts +39 -75
- package/src/composites/concierge-stack.ts +1 -1
- package/src/composites/steward.ts +59 -127
- package/src/config.test.ts +36 -2
- package/src/config.ts +38 -3
- package/src/coverage.test.ts +3 -9
- package/src/coverage.ts +7 -30
- package/src/deep-observe-hooks.ts +5 -48
- package/src/deep-observe.test.ts +0 -243
- package/src/deep-observe.ts +8 -103
- package/src/describe-resources.test.ts +10 -180
- package/src/describe-resources.ts +15 -81
- package/src/export-resources.ts +16 -110
- package/src/generated/index.d.ts +7 -67
- package/src/generated/index.ts +1 -4
- package/src/generated/lexicon-fountain.json +2 -68
- package/src/import/parser.ts +5 -3
- package/src/import/roundtrip.test.ts +25 -127
- package/src/index.ts +6 -1
- package/src/init-templates.e2e.test.ts +5 -16
- package/src/init-templates.ts +12 -6
- package/src/lint/audit-catalog.ts +10 -27
- package/src/lint/post-synth/ftn016-runtime-model-valid.ts +6 -5
- package/src/lint/post-synth/ftn025-removed-upstream.ts +65 -0
- package/src/lint/post-synth/index.ts +2 -6
- package/src/lint/post-synth/post-synth.test.ts +57 -112
- package/src/live-identity.ts +13 -148
- package/src/lsp/completions.test.ts +13 -16
- package/src/lsp/hover.test.ts +6 -6
- package/src/lsp/hover.ts +4 -16
- package/src/lsp/lexicon-index.ts +6 -5
- package/src/op/activities/fountain-apply.test.ts +12 -251
- package/src/op/activities/fountain-apply.ts +29 -375
- package/src/op/activities/fountain-prompt.ts +5 -6
- package/src/op/activities/fountain-run.test.ts +49 -208
- package/src/op/activities/fountain-run.ts +37 -139
- package/src/op/activities/index.ts +0 -1
- package/src/op/activities/op-runtime.test.ts +11 -8
- package/src/op/runtime.test.ts +115 -41
- package/src/op/runtime.ts +77 -49
- package/src/plugin.test.ts +2 -9
- package/src/plugin.ts +1 -14
- package/src/retired.ts +55 -0
- package/src/serializer.test.ts +19 -43
- package/src/serializer.ts +10 -5
- package/src/skills/chant-fountain-locked-sandboxes.md +1 -1
- package/src/skills/chant-fountain-ops.md +42 -41
- package/src/skills/chant-fountain.md +4 -4
- package/src/spec/fetch.ts +14 -8
- package/src/spec/fountain-openapi.snapshot.json +4890 -13968
- package/src/spec/parse.ts +11 -56
- package/src/spec/spec.e2e.test.ts +22 -14
- package/src/validate.ts +0 -3
- package/dist/disruption.d.ts +0 -49
- package/dist/disruption.d.ts.map +0 -1
- package/dist/lint/post-synth/ftn020-schedule-cron-syntax.d.ts +0 -26
- package/dist/lint/post-synth/ftn020-schedule-cron-syntax.d.ts.map +0 -1
- package/dist/lint/post-synth/ftn021-typed-references-resolve.d.ts +0 -3
- package/dist/lint/post-synth/ftn021-typed-references-resolve.d.ts.map +0 -1
- package/dist/lint/post-synth/ftn022-webhook-url-public-https.d.ts +0 -5
- package/dist/lint/post-synth/ftn022-webhook-url-public-https.d.ts.map +0 -1
- package/dist/okf/rules/FTN020.md +0 -15
- package/dist/okf/rules/FTN021.md +0 -17
- package/dist/okf/rules/FTN022.md +0 -15
- package/dist/okf/types/Schedule.md +0 -23
- package/dist/okf/types/Teammate.md +0 -20
- package/dist/okf/types/Webhook.md +0 -19
- package/dist/rules/ftn020-schedule-cron-syntax.ts +0 -57
- package/dist/rules/ftn021-typed-references-resolve.ts +0 -87
- package/dist/rules/ftn022-webhook-url-public-https.ts +0 -89
- package/src/disruption.test.ts +0 -163
- package/src/disruption.ts +0 -141
- package/src/lint/post-synth/ftn020-schedule-cron-syntax.ts +0 -57
- package/src/lint/post-synth/ftn021-typed-references-resolve.ts +0 -87
- package/src/lint/post-synth/ftn022-webhook-url-public-https.ts +0 -89
package/README.md
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
fountain lexicon plugin for [chant](https://github.com/intentius/chant).
|
|
4
4
|
|
|
5
|
-
[
|
|
6
|
-
sandboxed VMs. This lexicon declares its workload layer as
|
|
7
|
-
resources: `Environment` (sandbox baseline), `Vault` (env-var
|
|
8
|
-
`Agent` (a runnable agent config)
|
|
9
|
-
with a thread of its own), `Schedule` (a cron prompt into that thread), and
|
|
10
|
-
`Webhook` (where the estate's events leave it). Conversations are runs, not
|
|
5
|
+
[Managoat](https://github.com/managoat/managoat) (formerly fountain) runs
|
|
6
|
+
coding agents in sandboxed VMs. This lexicon declares its workload layer as
|
|
7
|
+
typed chant resources: `Environment` (sandbox baseline), `Vault` (env-var
|
|
8
|
+
overrides) and `Agent` (a runnable agent config). Conversations are runs, not
|
|
11
9
|
resources — start them with the `fountainRun` op.
|
|
12
10
|
|
|
13
11
|
`chant build` serializes to fountain's own manifest YAML (`fountain apply -f`
|
|
@@ -15,7 +13,7 @@ accepts it verbatim) — the same YAML `fountainApply` sends to fountain's bulk
|
|
|
15
13
|
`POST /api/apply` endpoint in one request.
|
|
16
14
|
|
|
17
15
|
```ts
|
|
18
|
-
import { Environment, Agent
|
|
16
|
+
import { Environment, Agent } from "@intentius/chant-lexicon-fountain";
|
|
19
17
|
|
|
20
18
|
export const env = new Environment({
|
|
21
19
|
name: "team-env",
|
|
@@ -28,24 +26,22 @@ export const helper = new Agent({
|
|
|
28
26
|
name: "helper",
|
|
29
27
|
model: "anthropic/claude-sonnet-4-6",
|
|
30
28
|
runtime: "claude",
|
|
31
|
-
environment: env, // typed ref
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
export const helperSeat = new Teammate({ name: "helper", agent: helper });
|
|
35
|
-
|
|
36
|
-
export const nightly = new Schedule({
|
|
37
|
-
name: "nightly-converge",
|
|
38
|
-
teammate: helperSeat,
|
|
39
|
-
cron: "0 3 * * *", // five fields, UTC (FTN020)
|
|
40
|
-
prompt: "chant run converge",
|
|
29
|
+
environment: env, // typed ref, serialized by name
|
|
41
30
|
});
|
|
42
31
|
```
|
|
43
32
|
|
|
44
|
-
Types are generated from
|
|
45
|
-
`
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
33
|
+
Types are generated from Managoat v0.29.0. `secrets` on `Environment` and
|
|
34
|
+
`Vault` is the one field the lexicon adds to the request schemas, taken from
|
|
35
|
+
fountain's manifest format.
|
|
36
|
+
|
|
37
|
+
Managoat removed the team, its scheduler, webhooks and persistent sandboxes
|
|
38
|
+
([managoat/managoat#2793](https://github.com/managoat/managoat/issues/2793)).
|
|
39
|
+
`Teammate`, `Schedule` and `Webhook` are still exported so an older project
|
|
40
|
+
builds far enough to be told what replaced them: declaring one, or an Agent
|
|
41
|
+
with `sandbox_mode: "persistent"`, fails the build with FTN025. A `Steward`'s
|
|
42
|
+
runs go to one conversation resumed by `channel_id`, and a cadence is a clock
|
|
43
|
+
outside Managoat running `chant run <op> --on fountain`. The lexicon docs'
|
|
44
|
+
overview has the full mapping.
|
|
49
45
|
|
|
50
46
|
For agents handling anything sensitive, `ConciergeStack` bundles the
|
|
51
47
|
locked-down defaults (deny-all egress, no vault overrides, ownership marker
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A prompt is a chant command line (#2125).
|
|
3
|
+
*
|
|
4
|
+
* That is the whole convention, and the reason a fountain thread reads as an
|
|
5
|
+
* environment's shell history: each turn is `chant run prod-apply --env prod`
|
|
6
|
+
* or `chant lifecycle diff --live`, so scrolling the thread is scrolling what
|
|
7
|
+
* was done to the environment.
|
|
8
|
+
*
|
|
9
|
+
* It is a *command line*, not a shell line. This module splits on whitespace
|
|
10
|
+
* with quote awareness and stops there: no expansion, no substitution, no
|
|
11
|
+
* pipes, no `&&`, no globbing, no `$VAR`. A prompt that does not parse to a
|
|
12
|
+
* chant verb is refused before anything runs, which is what keeps an agent
|
|
13
|
+
* that can be prompted by anyone from being an agent that can run anything.
|
|
14
|
+
* The verb list is core's own registry, so the set of things this accepts is
|
|
15
|
+
* exactly the set `chant --help` prints.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Split a command line into argv the way a shell would *quote* it, and in no
|
|
19
|
+
* other way.
|
|
20
|
+
*
|
|
21
|
+
* Single and double quotes group, a backslash escapes the next character
|
|
22
|
+
* inside double quotes and outside them, and nothing else is special. An
|
|
23
|
+
* unterminated quote is an error rather than a silent join, because the
|
|
24
|
+
* silent reading changes which arguments a verb receives.
|
|
25
|
+
*/
|
|
26
|
+
export function tokenize(text) {
|
|
27
|
+
const tokens = [];
|
|
28
|
+
let current = "";
|
|
29
|
+
let has = false;
|
|
30
|
+
let quote;
|
|
31
|
+
for (let i = 0; i < text.length; i++) {
|
|
32
|
+
const ch = text[i];
|
|
33
|
+
if (ch === "\\" && quote !== "'" && i + 1 < text.length) {
|
|
34
|
+
current += text[++i];
|
|
35
|
+
has = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (quote) {
|
|
39
|
+
if (ch === quote)
|
|
40
|
+
quote = undefined;
|
|
41
|
+
else
|
|
42
|
+
current += ch;
|
|
43
|
+
has = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ch === '"' || ch === "'") {
|
|
47
|
+
quote = ch;
|
|
48
|
+
has = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (/\s/.test(ch)) {
|
|
52
|
+
if (has)
|
|
53
|
+
tokens.push(current);
|
|
54
|
+
current = "";
|
|
55
|
+
has = false;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
current += ch;
|
|
59
|
+
has = true;
|
|
60
|
+
}
|
|
61
|
+
if (quote)
|
|
62
|
+
throw new Error(`unterminated ${quote === '"' ? "double" : "single"} quote`);
|
|
63
|
+
if (has)
|
|
64
|
+
tokens.push(current);
|
|
65
|
+
return tokens;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Parse one prompt into a runnable chant command, or refuse it.
|
|
69
|
+
*
|
|
70
|
+
* Core's registry and argv parser are imported here rather than at module
|
|
71
|
+
* load: nothing about declaring the fountain lexicon should pull in every CLI
|
|
72
|
+
* handler, and only a live `chant acp` turn ever reaches this.
|
|
73
|
+
*/
|
|
74
|
+
export async function parseChantCommandLine(text) {
|
|
75
|
+
let tokens;
|
|
76
|
+
try {
|
|
77
|
+
tokens = tokenize(text);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
message: err instanceof Error ? err.message : String(err),
|
|
83
|
+
hint: "A prompt is a chant command line — quote arguments that contain spaces.",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (tokens.length === 0) {
|
|
87
|
+
return { ok: false, message: "the prompt was empty", hint: usageHint() };
|
|
88
|
+
}
|
|
89
|
+
// `chant` may lead, and usually does — it is what a person types. Stripping
|
|
90
|
+
// it here means the same string works whether it was copied from a terminal
|
|
91
|
+
// or typed as a bare verb.
|
|
92
|
+
const argv = tokens[0] === "chant" ? tokens.slice(1) : tokens;
|
|
93
|
+
if (argv.length === 0) {
|
|
94
|
+
return { ok: false, message: "the prompt named no chant command", hint: usageHint() };
|
|
95
|
+
}
|
|
96
|
+
const { parseArgs, commandRegistry } = await import("@intentius/chant/cli/main");
|
|
97
|
+
const { resolveCommand } = await import("@intentius/chant/cli/registry");
|
|
98
|
+
let args;
|
|
99
|
+
try {
|
|
100
|
+
args = parseArgs(argv);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
message: err instanceof Error ? err.message : String(err),
|
|
106
|
+
hint: usageHint(),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const match = resolveCommand(args, commandRegistry);
|
|
110
|
+
if (!match) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
message: `"${argv[0]}" is not a chant command`,
|
|
114
|
+
hint: usageHint(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// `chant run <op>` is the one form that takes the provider path with a
|
|
118
|
+
// progress sink. Every other `run` word — `run list`, `run status <op>` —
|
|
119
|
+
// is a registered compound verb, so `resolveCommand` has already told us
|
|
120
|
+
// which this is and no second list of subcommands has to be kept in step
|
|
121
|
+
// with core's.
|
|
122
|
+
if (args.command === "run" && !match.compound && args.path && args.path !== ".") {
|
|
123
|
+
return { ok: true, command: { kind: "op-run", op: args.path, args, argv } };
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
command: { kind: "verb", name: match.def.name, def: match.def, compound: match.compound, args, argv },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function usageHint() {
|
|
131
|
+
return ("A prompt is a chant command line, not a shell line — for example " +
|
|
132
|
+
'"chant run prod-apply --env prod" or "chant lifecycle diff --live". ' +
|
|
133
|
+
'Run "chant --help" for the verb list.');
|
|
134
|
+
}
|
package/dist/acp/host.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a turn needs from chant, stated once (#2125).
|
|
3
|
+
*
|
|
4
|
+
* The ACP server owns the protocol — sessions, updates, stop reasons — and
|
|
5
|
+
* nothing else. Everything it does *to a project* goes through this seam:
|
|
6
|
+
* finding an Op, starting it on a runtime provider, running any other chant
|
|
7
|
+
* verb, and recording a gate resolution. The conformance test drives the
|
|
8
|
+
* protocol through a stub host, so the tool-call ordering, the cancel path and
|
|
9
|
+
* the gated reply are tested without a checkout, a git branch or a real Op.
|
|
10
|
+
*
|
|
11
|
+
* The real implementation is `createChantHost`, and it is deliberately thin:
|
|
12
|
+
* `createLocalOpRuntime` from core is the provider (#2121), `discoverOps` is
|
|
13
|
+
* the lookup, and a verb runs through core's own registry handler rather than
|
|
14
|
+
* a reimplementation of it. Nothing here shells out — a prompt is parsed and
|
|
15
|
+
* dispatched in this process, so there is no line for a shell to interpret.
|
|
16
|
+
*/
|
|
17
|
+
/** How the real host identifies itself on a gate resolution it writes. */
|
|
18
|
+
export const ACP_APPROVER = "chant-acp";
|
|
19
|
+
/**
|
|
20
|
+
* The real host, bound to one session's working directory.
|
|
21
|
+
*
|
|
22
|
+
* Core's machinery reads the project from `process.cwd()`, so a turn changes
|
|
23
|
+
* into `cwd` for its duration (see ./turn.ts) rather than threading a path
|
|
24
|
+
* through every call — which is also why turns are serialized. Every import
|
|
25
|
+
* below is dynamic: declaring the fountain lexicon must not pull core's CLI
|
|
26
|
+
* handlers into a `chant build`.
|
|
27
|
+
*/
|
|
28
|
+
export function createChantHost(cwd) {
|
|
29
|
+
return {
|
|
30
|
+
cwd,
|
|
31
|
+
async findOp(name) {
|
|
32
|
+
const { discoverOps } = await import("@intentius/chant/op/discover");
|
|
33
|
+
const { ops, errors } = await discoverOps({ cwd });
|
|
34
|
+
return {
|
|
35
|
+
...(ops.get(name) ? { config: ops.get(name)?.config } : {}),
|
|
36
|
+
names: [...ops.keys()],
|
|
37
|
+
errors,
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
async startOp(op, opts) {
|
|
41
|
+
const { createLocalOpRuntime } = await import("@intentius/chant/op/runtimes/local");
|
|
42
|
+
return createLocalOpRuntime({ projectPath: cwd }).start(op, opts);
|
|
43
|
+
},
|
|
44
|
+
async runVerb(command) {
|
|
45
|
+
const { loadPlugins, resolveProjectLexicons } = await import("@intentius/chant/cli/plugins");
|
|
46
|
+
const plugins = command.def.requiresPlugins
|
|
47
|
+
? await loadPlugins(await resolveProjectLexicons(cwd).catch(() => [])).catch(() => [])
|
|
48
|
+
: [];
|
|
49
|
+
return command.def.handler({
|
|
50
|
+
args: command.args,
|
|
51
|
+
plugins,
|
|
52
|
+
serializers: plugins.map((p) => p.serializer),
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
async resolveGate(op, gate, resolvedBy) {
|
|
56
|
+
// chant#2400 — through `recordGateApproval`, the same function `chant
|
|
57
|
+
// approve` calls, rather than straight to `appendGateResolution`.
|
|
58
|
+
//
|
|
59
|
+
// Going direct wrote a line that looked like an approval and was missing
|
|
60
|
+
// every check one carries. It skipped #2300's plan binding, so the
|
|
61
|
+
// resolution named no plan and authorised whatever the next run produced
|
|
62
|
+
// rather than what anyone had read. It skipped the standing-pending-fact
|
|
63
|
+
// requirement, so it could answer a gate nothing had reached. And it
|
|
64
|
+
// skipped chant#2384's origin rule, which is the one that matters here:
|
|
65
|
+
// the pending fact and this resolution are both authored by the same ACP
|
|
66
|
+
// session, so the model that produced the gate was resolving it.
|
|
67
|
+
//
|
|
68
|
+
// With the rule applied, that case now refuses, which is the point. A
|
|
69
|
+
// Steward's gate reaches a person or it does not clear.
|
|
70
|
+
const { recordGateApproval } = await import("@intentius/chant/cli/handlers/operator");
|
|
71
|
+
const outcome = await recordGateApproval(op, gate, { actor: resolvedBy, origin: "acp" });
|
|
72
|
+
if (!outcome.ok) {
|
|
73
|
+
throw new Error(`the resolution for gate "${gate}" on "${op}" was refused. A gate reached over ACP cannot ` +
|
|
74
|
+
`also be resolved over ACP — approve it with \`chant approve ${op} ${gate}\` at a shell.`);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant acp` — the CLI mount for the ACP server (#2125).
|
|
3
|
+
*
|
|
4
|
+
* A command group with a `defaultVerb`, so the bare `chant acp` an editor or a
|
|
5
|
+
* fountain Agent spawns (`runtime_command: "chant acp"`) reaches `serve`
|
|
6
|
+
* without the client having to know chant's verb grammar. `chant acp serve`
|
|
7
|
+
* is the same thing spelled out.
|
|
8
|
+
*
|
|
9
|
+
* The server itself is behind a dynamic import: mounting the group happens
|
|
10
|
+
* every time a plugin loads, and nothing about `chant build` should pay for a
|
|
11
|
+
* JSON-RPC peer it will never open.
|
|
12
|
+
*/
|
|
13
|
+
import { splitJoinedFlags, unknownFlagError } from "@intentius/chant/cli/command-group";
|
|
14
|
+
const BOOLEAN_FLAGS = new Set(["--durable-requests"]);
|
|
15
|
+
/** The verb group `chant acp` mounts. */
|
|
16
|
+
export function acpCommandGroup() {
|
|
17
|
+
return {
|
|
18
|
+
name: "acp",
|
|
19
|
+
description: "Serve chant as an Agent Client Protocol agent over stdio",
|
|
20
|
+
defaultVerb: "serve",
|
|
21
|
+
commands: [
|
|
22
|
+
{
|
|
23
|
+
name: "serve",
|
|
24
|
+
description: "Speak ACP on stdin/stdout; each prompt is one chant command line",
|
|
25
|
+
handler: serveHandler,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function serveHandler(ctx) {
|
|
31
|
+
let durableRequests = false;
|
|
32
|
+
let steward;
|
|
33
|
+
const args = splitJoinedFlags(ctx.rawArgs, BOOLEAN_FLAGS);
|
|
34
|
+
for (let i = 0; i < args.length; i++) {
|
|
35
|
+
const arg = args[i];
|
|
36
|
+
if (arg === "--durable-requests")
|
|
37
|
+
durableRequests = true;
|
|
38
|
+
else if (arg === "--steward") {
|
|
39
|
+
// The Steward composite's Agent runs `chant acp --steward <name>`
|
|
40
|
+
// (chant#2749), so every turn in this session is that steward's.
|
|
41
|
+
const name = args[++i];
|
|
42
|
+
if (!name || name.startsWith("--")) {
|
|
43
|
+
console.error(`"chant acp --steward" takes the steward's name.`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
steward = name;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// stdout carries the protocol and nothing else, so a usage error goes
|
|
50
|
+
// to stderr — the one place a spawned agent can say anything.
|
|
51
|
+
console.error(unknownFlagError(arg, `"chant acp" accepts --durable-requests and --steward <name>.`).message);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const { serveAcpOverStdio } = await import("./serve.js");
|
|
56
|
+
await serveAcpOverStdio({ durableRequests, ...(steward ? { steward } : {}) });
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newline-delimited JSON-RPC 2.0, both directions (#2125).
|
|
3
|
+
*
|
|
4
|
+
* ACP frames one JSON value per line. That is the whole transport, which is
|
|
5
|
+
* why this is ~150 lines of local code instead of a dependency: an agent that
|
|
6
|
+
* speaks the protocol needs a line reader, a writer that serializes its two
|
|
7
|
+
* writers, and a table of outbound requests waiting on answers.
|
|
8
|
+
*
|
|
9
|
+
* The peer is stream-agnostic on purpose. `chant acp` hands it the process's
|
|
10
|
+
* stdin and stdout; the conformance test hands it a pair of in-memory pipes
|
|
11
|
+
* and drives the client side in the same process, so the test exercises the
|
|
12
|
+
* real framing rather than a mock of it.
|
|
13
|
+
*/
|
|
14
|
+
/** An error a handler wants reported with a specific JSON-RPC code. */
|
|
15
|
+
export class JsonRpcError extends Error {
|
|
16
|
+
constructor(code, message, data) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.data = data;
|
|
20
|
+
this.name = "JsonRpcError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A transport over a pair of Node streams — stdin and stdout, for `chant acp`.
|
|
25
|
+
*
|
|
26
|
+
* The writer is bound here, at construction, and that is load-bearing rather
|
|
27
|
+
* than tidy. A turn replaces `process.stdout.write` to stream a command's
|
|
28
|
+
* output (../acp/output.ts); if this looked the method up per call it would
|
|
29
|
+
* find the interceptor, every protocol line the turn emits would come back as
|
|
30
|
+
* another message chunk to emit, and the first notification sent inside a turn
|
|
31
|
+
* would recurse until V8 refused to grow the string. Binding once means the
|
|
32
|
+
* protocol always reaches the real stdout.
|
|
33
|
+
*/
|
|
34
|
+
export function streamTransport(input, output) {
|
|
35
|
+
const write = output.write.bind(output);
|
|
36
|
+
let buffer = "";
|
|
37
|
+
let lineCb;
|
|
38
|
+
let closeCb;
|
|
39
|
+
input.setEncoding("utf8");
|
|
40
|
+
input.on("data", (chunk) => {
|
|
41
|
+
buffer += chunk;
|
|
42
|
+
let nl = buffer.indexOf("\n");
|
|
43
|
+
while (nl >= 0) {
|
|
44
|
+
const line = buffer.slice(0, nl);
|
|
45
|
+
buffer = buffer.slice(nl + 1);
|
|
46
|
+
if (line.trim().length > 0)
|
|
47
|
+
lineCb?.(line);
|
|
48
|
+
nl = buffer.indexOf("\n");
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
input.on("end", () => closeCb?.());
|
|
52
|
+
input.on("close", () => closeCb?.());
|
|
53
|
+
return {
|
|
54
|
+
send(line) {
|
|
55
|
+
write(line + "\n");
|
|
56
|
+
},
|
|
57
|
+
onLine(cb) {
|
|
58
|
+
lineCb = cb;
|
|
59
|
+
},
|
|
60
|
+
onClose(cb) {
|
|
61
|
+
closeCb = cb;
|
|
62
|
+
},
|
|
63
|
+
close() {
|
|
64
|
+
input.destroy();
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** An in-memory transport pair — one end for each side of a connection. */
|
|
69
|
+
export function memoryTransportPair() {
|
|
70
|
+
const cbs = [undefined, undefined];
|
|
71
|
+
const closes = [undefined, undefined];
|
|
72
|
+
let closed = false;
|
|
73
|
+
const end = (self) => ({
|
|
74
|
+
send(line) {
|
|
75
|
+
if (closed)
|
|
76
|
+
return;
|
|
77
|
+
// Deliver asynchronously so a `send` inside a handler never re-enters
|
|
78
|
+
// that handler through the peer's own dispatch — the same ordering a
|
|
79
|
+
// real pipe gives, without the pipe.
|
|
80
|
+
queueMicrotask(() => cbs[self === 0 ? 1 : 0]?.(line));
|
|
81
|
+
},
|
|
82
|
+
onLine(cb) {
|
|
83
|
+
cbs[self] = cb;
|
|
84
|
+
},
|
|
85
|
+
onClose(cb) {
|
|
86
|
+
closes[self] = cb;
|
|
87
|
+
},
|
|
88
|
+
close() {
|
|
89
|
+
if (closed)
|
|
90
|
+
return;
|
|
91
|
+
closed = true;
|
|
92
|
+
queueMicrotask(() => {
|
|
93
|
+
closes[0]?.();
|
|
94
|
+
closes[1]?.();
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
return [end(0), end(1)];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* One connection, serving a handler and able to originate requests of its own.
|
|
102
|
+
*
|
|
103
|
+
* The agent has two writers — request responses, and the `session/update`
|
|
104
|
+
* notifications a turn emits while a response is still pending. `send` is the
|
|
105
|
+
* single funnel for both, so two interleaved writes can never produce one
|
|
106
|
+
* unparseable line.
|
|
107
|
+
*/
|
|
108
|
+
export class JsonRpcPeer {
|
|
109
|
+
constructor(transport, handler) {
|
|
110
|
+
this.transport = transport;
|
|
111
|
+
this.handler = handler;
|
|
112
|
+
this.nextId = 1;
|
|
113
|
+
this.pending = new Map();
|
|
114
|
+
this.closed = false;
|
|
115
|
+
transport.onLine((line) => void this.receive(line));
|
|
116
|
+
transport.onClose(() => this.shutdown());
|
|
117
|
+
}
|
|
118
|
+
/** Send a notification. Fire and forget, by definition. */
|
|
119
|
+
notify(method, params) {
|
|
120
|
+
this.write({ jsonrpc: "2.0", method, params });
|
|
121
|
+
}
|
|
122
|
+
/** Send a request and resolve with its `result`, or reject with its `error`. */
|
|
123
|
+
request(method, params) {
|
|
124
|
+
const id = this.nextId++;
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
this.pending.set(id, { resolve, reject });
|
|
127
|
+
this.write({ jsonrpc: "2.0", id, method, params });
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** Stop serving and fail every outbound request still waiting. */
|
|
131
|
+
close() {
|
|
132
|
+
this.transport.close();
|
|
133
|
+
this.shutdown();
|
|
134
|
+
}
|
|
135
|
+
shutdown() {
|
|
136
|
+
if (this.closed)
|
|
137
|
+
return;
|
|
138
|
+
this.closed = true;
|
|
139
|
+
for (const [, waiter] of this.pending) {
|
|
140
|
+
waiter.reject(new Error("the connection closed before the request was answered"));
|
|
141
|
+
}
|
|
142
|
+
this.pending.clear();
|
|
143
|
+
}
|
|
144
|
+
write(message) {
|
|
145
|
+
if (this.closed)
|
|
146
|
+
return;
|
|
147
|
+
this.transport.send(JSON.stringify(message));
|
|
148
|
+
}
|
|
149
|
+
async receive(line) {
|
|
150
|
+
let message;
|
|
151
|
+
try {
|
|
152
|
+
message = JSON.parse(line);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
this.write({
|
|
156
|
+
jsonrpc: "2.0",
|
|
157
|
+
id: null,
|
|
158
|
+
error: { code: -32700, message: "the line was not JSON" },
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// A response to something we asked.
|
|
163
|
+
if (message.method === undefined && message.id !== undefined && message.id !== null) {
|
|
164
|
+
const waiter = this.pending.get(Number(message.id));
|
|
165
|
+
if (!waiter)
|
|
166
|
+
return;
|
|
167
|
+
this.pending.delete(Number(message.id));
|
|
168
|
+
if (message.error)
|
|
169
|
+
waiter.reject(new JsonRpcError(message.error.code, message.error.message));
|
|
170
|
+
else
|
|
171
|
+
waiter.resolve(message.result);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (!message.method)
|
|
175
|
+
return;
|
|
176
|
+
// A notification: no id, no reply.
|
|
177
|
+
if (message.id === undefined || message.id === null) {
|
|
178
|
+
try {
|
|
179
|
+
this.handler.notify(message.method, message.params);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// JSON-RPC has nowhere to put this, and a stdio agent's stdout is
|
|
183
|
+
// protocol only.
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const id = message.id;
|
|
188
|
+
try {
|
|
189
|
+
const result = await this.handler.request(message.method, message.params);
|
|
190
|
+
this.write({ jsonrpc: "2.0", id, result: result ?? null });
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
const code = err instanceof JsonRpcError ? err.code : -32603;
|
|
194
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
195
|
+
this.write({ jsonrpc: "2.0", id, error: { code, message: msg } });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capturing what a chant command prints, so a turn can stream it (#2125).
|
|
3
|
+
*
|
|
4
|
+
* chant's own handlers and its activities print through `console.log` /
|
|
5
|
+
* `console.error`, which means the process's stdout and stderr are where a
|
|
6
|
+
* step's output actually appears — there is no per-step output channel on the
|
|
7
|
+
* executor to subscribe to instead. So a turn borrows both writers for its
|
|
8
|
+
* duration and forwards every chunk as an `agent_message_chunk`.
|
|
9
|
+
*
|
|
10
|
+
* Two consequences worth stating rather than discovering:
|
|
11
|
+
*
|
|
12
|
+
* - The `chant acp` process writes the protocol on stdout, so a turn MUST
|
|
13
|
+
* hold this capture over everything it runs. A stray `console.log` reaching
|
|
14
|
+
* the real stdout mid-turn is an unparseable line to the client.
|
|
15
|
+
* - The capture is process-wide, so turns are serialized (see
|
|
16
|
+
* ./server.ts). One stdio connection running one command at a time is the
|
|
17
|
+
* honest shape anyway.
|
|
18
|
+
*
|
|
19
|
+
* Redaction is deliberately absent. A step's output is the environment's
|
|
20
|
+
* output, and fountain redacts a thread's secrets on the way in — the
|
|
21
|
+
* documented division of labour in ../skills/chant-fountain.md. This server
|
|
22
|
+
* never reads or prints the environment it inherits.
|
|
23
|
+
*/
|
|
24
|
+
/** Run `fn` with both process writers forwarded to `onChunk`, and restore them after. */
|
|
25
|
+
export async function withCapturedOutput(onChunk, fn) {
|
|
26
|
+
const realOut = process.stdout.write.bind(process.stdout);
|
|
27
|
+
const realErr = process.stderr.write.bind(process.stderr);
|
|
28
|
+
const intercept = (stream) => (chunk, encoding, cb) => {
|
|
29
|
+
const text = typeof chunk === "string"
|
|
30
|
+
? chunk
|
|
31
|
+
: Buffer.isBuffer(chunk)
|
|
32
|
+
? chunk.toString("utf8")
|
|
33
|
+
: String(chunk);
|
|
34
|
+
if (text.length > 0)
|
|
35
|
+
onChunk(text, stream);
|
|
36
|
+
const done = typeof encoding === "function" ? encoding : cb;
|
|
37
|
+
if (typeof done === "function")
|
|
38
|
+
done();
|
|
39
|
+
return true;
|
|
40
|
+
};
|
|
41
|
+
process.stdout.write = intercept("stdout");
|
|
42
|
+
process.stderr.write = intercept("stderr");
|
|
43
|
+
try {
|
|
44
|
+
return await fn();
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
process.stdout.write = realOut;
|
|
48
|
+
process.stderr.write = realErr;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Agent Client Protocol wire shapes `chant acp` speaks (#2125).
|
|
3
|
+
*
|
|
4
|
+
* ACP (<https://agentclientprotocol.com>) is JSON-RPC 2.0 between a *client*
|
|
5
|
+
* — an editor, or fountain — and an *agent*. chant is the agent half here: it
|
|
6
|
+
* is spawned, it answers `initialize`, it opens sessions, and every prompt it
|
|
7
|
+
* receives is one chant command line.
|
|
8
|
+
*
|
|
9
|
+
* These are hand-written rather than pulled from a package on purpose. The
|
|
10
|
+
* subset an agent has to speak is small enough to state in one file, and a
|
|
11
|
+
* dependency for it would be a dependency in every project that declares the
|
|
12
|
+
* fountain lexicon. Field names match what fountain's own ACP client sends
|
|
13
|
+
* and expects (fountain ADR 0014, `cli/internal/acp`), which is the same
|
|
14
|
+
* vocabulary the specification defines — a client of ours that is not
|
|
15
|
+
* fountain sees nothing fountain-specific.
|
|
16
|
+
*/
|
|
17
|
+
/** The ACP major version this agent speaks. */
|
|
18
|
+
export const ACP_PROTOCOL_VERSION = 1;
|
|
19
|
+
/** JSON-RPC 2.0 error codes, plus the one ACP adds. */
|
|
20
|
+
export const JSONRPC_PARSE_ERROR = -32700;
|
|
21
|
+
export const JSONRPC_INVALID_REQUEST = -32600;
|
|
22
|
+
export const JSONRPC_METHOD_NOT_FOUND = -32601;
|
|
23
|
+
export const JSONRPC_INVALID_PARAMS = -32602;
|
|
24
|
+
export const JSONRPC_INTERNAL_ERROR = -32603;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant acp` on the process's own stdio (#2125).
|
|
3
|
+
*
|
|
4
|
+
* Split from ./index.ts so mounting the command group costs nothing: this
|
|
5
|
+
* module is imported only once a client has actually spawned the agent.
|
|
6
|
+
*
|
|
7
|
+
* stdout is the protocol. Nothing else may write to it for the life of the
|
|
8
|
+
* process — every turn holds the output capture (./output.ts) precisely so a
|
|
9
|
+
* command's `console.log` cannot land between two JSON-RPC lines.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { setGateOrigin } from "@intentius/chant/lifecycle/gate-origin";
|
|
13
|
+
import { setStewardTurn } from "@intentius/chant/op/steward-turn";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { AcpServer } from "./server.js";
|
|
17
|
+
import { streamTransport } from "./jsonrpc.js";
|
|
18
|
+
/** Serve until the client closes stdin, which is how it says the connection is over. */
|
|
19
|
+
export async function serveAcpOverStdio(opts = {}) {
|
|
20
|
+
// chant#2384 — every prompt this session carries is authored by the model,
|
|
21
|
+
// and `approve` is a verb in chant's registry like any other, so nothing in
|
|
22
|
+
// the transport stops `chant run <op>` being followed by
|
|
23
|
+
// `chant approve <op> <gate>` as the next prompt. Declaring the channel is
|
|
24
|
+
// what makes the gate ledger able to tell that apart from a person at a
|
|
25
|
+
// shell; the same-origin rule then refuses it.
|
|
26
|
+
setGateOrigin("acp");
|
|
27
|
+
// chant#2749 — the session is a steward's. A decision point an Op asks during
|
|
28
|
+
// a turn names the steward, makes its model call through the steward's
|
|
29
|
+
// brokered capability, and is left open for a person; `points answer` from
|
|
30
|
+
// this session is refused, as the gate's resolution is above.
|
|
31
|
+
if (opts.steward)
|
|
32
|
+
setStewardTurn({ steward: opts.steward });
|
|
33
|
+
// chant#3539 — fountain gives the command it spawns the conversation's id
|
|
34
|
+
// in `FOUNTAIN_CONVERSATION_ID` (process env only, never on disk), and
|
|
35
|
+
// `chant run <op> --on fountain` names the run by that same id. Naming the
|
|
36
|
+
// sandbox's runs by it joins the two: the run record written here carries
|
|
37
|
+
// the id the caller's `chant run status` reports.
|
|
38
|
+
const conversationId = process.env.FOUNTAIN_CONVERSATION_ID?.trim();
|
|
39
|
+
const server = new AcpServer({
|
|
40
|
+
durableRequests: opts.durableRequests ?? false,
|
|
41
|
+
version: lexiconVersion(),
|
|
42
|
+
...(conversationId ? { runId: conversationId } : {}),
|
|
43
|
+
});
|
|
44
|
+
const transport = streamTransport(process.stdin, process.stdout);
|
|
45
|
+
server.connect(transport);
|
|
46
|
+
await new Promise((resolve) => {
|
|
47
|
+
process.stdin.on("end", resolve);
|
|
48
|
+
process.stdin.on("close", resolve);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** The lexicon's own version, reported as `agentInfo.version`. */
|
|
52
|
+
function lexiconVersion() {
|
|
53
|
+
try {
|
|
54
|
+
const pkgPath = join(dirname(dirname(dirname(fileURLToPath(import.meta.url)))), "package.json");
|
|
55
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
56
|
+
return pkg.version ?? "dev";
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return "dev";
|
|
60
|
+
}
|
|
61
|
+
}
|