agent-trellis 0.9.1 → 0.10.0
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/adapters/mcpPlan.d.ts +5 -0
- package/dist/adapters/mcpPlan.js +7 -1
- package/dist/cli.js +11 -5
- package/dist/commands/mcp.d.ts +41 -2
- package/dist/commands/mcp.js +111 -14
- package/dist/commands/mcpAuth.js +28 -0
- package/dist/core/canonical.d.ts +15 -12
- package/dist/core/canonical.js +86 -18
- package/dist/core/types.d.ts +28 -2
- package/dist/core/types.js +19 -0
- package/dist/lib/oauth/flow.d.ts +13 -0
- package/dist/lib/oauth/flow.js +48 -3
- package/dist/lib/secretEnv.d.ts +5 -0
- package/dist/lib/secretEnv.js +8 -0
- package/dist/pi-bridge/bundle.js +738 -680
- package/dist/pi-bridge/index.js +2 -2
- package/docs/architecture.md +19 -0
- package/docs/mcp-oauth-matrix.md +140 -0
- package/docs/research.md +11 -0
- package/package.json +1 -1
- package/schema/servers.example.yaml +18 -0
package/dist/lib/oauth/flow.d.ts
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* The interactive half: dynamic client registration (RFC 7591) plus the
|
|
3
3
|
* authorization-code grant with PKCE (RFC 6749 §4.1, RFC 7636).
|
|
4
4
|
*
|
|
5
|
+
* Registration is a fallback, not a requirement: a caller that already
|
|
6
|
+
* holds a pre-registered `client_id` passes it in and the RFC 7591 step
|
|
7
|
+
* is skipped entirely (trellis-mcp-oauth-static-client design.md D3) —
|
|
8
|
+
* some providers refuse registration outright, and those are exactly the
|
|
9
|
+
* ones that publish a client for third-party use instead.
|
|
10
|
+
*
|
|
5
11
|
* Only ever reached from `trellis mcp auth`, run by a human. The gateway
|
|
6
12
|
* must never call into this file: it is spawned silently by an agent,
|
|
7
13
|
* typically with no terminal and no display, so it can neither show a
|
|
@@ -31,6 +37,13 @@ export interface AuthorizeOptions {
|
|
|
31
37
|
* endpoint — that is a legitimate configuration (a pre-registered
|
|
32
38
|
* client), not an error, so the caller falls back to whatever client id
|
|
33
39
|
* it was given.
|
|
40
|
+
*
|
|
41
|
+
* A refused registration is the one failure a human can actually fix, so
|
|
42
|
+
* the AS's own OAuth error code is read out of the response body rather
|
|
43
|
+
* than collapsed into a bare status: `unauthorized_client` is the
|
|
44
|
+
* provider saying "this server is not open to registration", which is
|
|
45
|
+
* precisely when a pre-registered `client_id` is the answer
|
|
46
|
+
* (trellis-mcp-oauth-static-client design.md D3).
|
|
34
47
|
*/
|
|
35
48
|
export declare function registerClient(metadata: AuthorizationServerMetadata, redirectUri: string, opts?: {
|
|
36
49
|
fetchImpl?: FetchLike;
|
package/dist/lib/oauth/flow.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* The interactive half: dynamic client registration (RFC 7591) plus the
|
|
3
3
|
* authorization-code grant with PKCE (RFC 6749 §4.1, RFC 7636).
|
|
4
4
|
*
|
|
5
|
+
* Registration is a fallback, not a requirement: a caller that already
|
|
6
|
+
* holds a pre-registered `client_id` passes it in and the RFC 7591 step
|
|
7
|
+
* is skipped entirely (trellis-mcp-oauth-static-client design.md D3) —
|
|
8
|
+
* some providers refuse registration outright, and those are exactly the
|
|
9
|
+
* ones that publish a client for third-party use instead.
|
|
10
|
+
*
|
|
5
11
|
* Only ever reached from `trellis mcp auth`, run by a human. The gateway
|
|
6
12
|
* must never call into this file: it is spawned silently by an agent,
|
|
7
13
|
* typically with no terminal and no display, so it can neither show a
|
|
@@ -17,6 +23,13 @@ export class AuthorizationError extends Error {
|
|
|
17
23
|
* endpoint — that is a legitimate configuration (a pre-registered
|
|
18
24
|
* client), not an error, so the caller falls back to whatever client id
|
|
19
25
|
* it was given.
|
|
26
|
+
*
|
|
27
|
+
* A refused registration is the one failure a human can actually fix, so
|
|
28
|
+
* the AS's own OAuth error code is read out of the response body rather
|
|
29
|
+
* than collapsed into a bare status: `unauthorized_client` is the
|
|
30
|
+
* provider saying "this server is not open to registration", which is
|
|
31
|
+
* precisely when a pre-registered `client_id` is the answer
|
|
32
|
+
* (trellis-mcp-oauth-static-client design.md D3).
|
|
20
33
|
*/
|
|
21
34
|
export async function registerClient(metadata, redirectUri, opts = {}) {
|
|
22
35
|
if (!metadata.registrationEndpoint)
|
|
@@ -35,7 +48,10 @@ export async function registerClient(metadata, redirectUri, opts = {}) {
|
|
|
35
48
|
}),
|
|
36
49
|
});
|
|
37
50
|
if (!response.ok) {
|
|
38
|
-
|
|
51
|
+
const code = await readOAuthErrorCode(response);
|
|
52
|
+
throw new AuthorizationError(code
|
|
53
|
+
? `dynamic client registration was refused (${code})`
|
|
54
|
+
: `dynamic client registration failed with HTTP ${response.status}`);
|
|
39
55
|
}
|
|
40
56
|
const payload = (await response.json());
|
|
41
57
|
if (!payload.client_id) {
|
|
@@ -46,6 +62,22 @@ export async function registerClient(metadata, redirectUri, opts = {}) {
|
|
|
46
62
|
out.clientSecret = payload.client_secret;
|
|
47
63
|
return out;
|
|
48
64
|
}
|
|
65
|
+
/** RFC 6749 §5.2 error body. Best-effort by design: a registration
|
|
66
|
+
* endpoint that answers with plain text or an empty body still produces
|
|
67
|
+
* a usable error, just a less specific one. */
|
|
68
|
+
async function readOAuthErrorCode(response) {
|
|
69
|
+
try {
|
|
70
|
+
const payload = (await response.json());
|
|
71
|
+
return typeof payload.error === "string" ? payload.error : undefined;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Provider-neutral remediation, appended wherever registration is the
|
|
78
|
+
* thing that failed. Names the exact file and key so the fix needs no
|
|
79
|
+
* further search. */
|
|
80
|
+
const STATIC_CLIENT_REMEDIATION = "register a client with the provider and set auth.client_id (plus auth.client_secret_env if the provider requires a secret) under this server in ~/.trellis/mcp/servers.yaml";
|
|
49
81
|
/** A one-shot loopback listener for the redirect. Bound to 127.0.0.1
|
|
50
82
|
* explicitly, never 0.0.0.0: the authorization code arrives in this URL,
|
|
51
83
|
* and a listener on all interfaces would accept it from the network. */
|
|
@@ -101,9 +133,22 @@ export async function authorize(serverName, metadata, opts = {}) {
|
|
|
101
133
|
let clientId = opts.clientId;
|
|
102
134
|
let clientSecret = opts.clientSecret;
|
|
103
135
|
if (!clientId) {
|
|
104
|
-
|
|
136
|
+
// The AS cannot register us AND canonical carries no client_id — the
|
|
137
|
+
// one dead end a person has to resolve by hand, so it is reported as
|
|
138
|
+
// such rather than as a generic registration failure
|
|
139
|
+
// (trellis-mcp-oauth-static-client design.md D3, task 2.3).
|
|
140
|
+
let registered;
|
|
141
|
+
try {
|
|
142
|
+
registered = await registerClient(metadata, redirectUri, { fetchImpl, scope: opts.scope });
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
if (err instanceof AuthorizationError) {
|
|
146
|
+
throw new AuthorizationError(`"${serverName}": ${err.message} — ${STATIC_CLIENT_REMEDIATION}`);
|
|
147
|
+
}
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
105
150
|
if (!registered) {
|
|
106
|
-
throw new AuthorizationError(`"${serverName}"'s authorization server offers no dynamic client registration
|
|
151
|
+
throw new AuthorizationError(`"${serverName}"'s authorization server offers no dynamic client registration — ${STATIC_CLIENT_REMEDIATION}`);
|
|
107
152
|
}
|
|
108
153
|
clientId = registered.clientId;
|
|
109
154
|
clientSecret = registered.clientSecret;
|
package/dist/lib/secretEnv.d.ts
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { SecretsPolicy } from "../core/types.js";
|
|
12
12
|
import type { BackupSession } from "./backup.js";
|
|
13
|
+
/** The name half of LINE_RE, standing alone: what counts as a variable
|
|
14
|
+
* NAME wherever secrets enter by name (`env`, `client_secret_env`). A
|
|
15
|
+
* value failing this in a name position is at best a typo, at worst a
|
|
16
|
+
* pasted secret — callers refuse it instead of trying to resolve it. */
|
|
17
|
+
export declare function isValidSecretVarName(name: string): boolean;
|
|
13
18
|
export declare function parseDotenv(content: string): Record<string, string>;
|
|
14
19
|
/**
|
|
15
20
|
* `policy.envFile` set: it is the SOLE source — a name absent from it
|
package/dist/lib/secretEnv.js
CHANGED
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
12
12
|
import { dirname } from "node:path";
|
|
13
13
|
const LINE_RE = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/;
|
|
14
|
+
const VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15
|
+
/** The name half of LINE_RE, standing alone: what counts as a variable
|
|
16
|
+
* NAME wherever secrets enter by name (`env`, `client_secret_env`). A
|
|
17
|
+
* value failing this in a name position is at best a typo, at worst a
|
|
18
|
+
* pasted secret — callers refuse it instead of trying to resolve it. */
|
|
19
|
+
export function isValidSecretVarName(name) {
|
|
20
|
+
return VAR_NAME_RE.test(name);
|
|
21
|
+
}
|
|
14
22
|
export function parseDotenv(content) {
|
|
15
23
|
const result = {};
|
|
16
24
|
for (const rawLine of content.split("\n")) {
|