agent-trellis 0.9.0 → 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.
Files changed (180) hide show
  1. package/dist/adapters/mcpPlan.d.ts +5 -0
  2. package/dist/adapters/mcpPlan.js +7 -1
  3. package/dist/cli.js +11 -5
  4. package/dist/commands/mcp.d.ts +41 -2
  5. package/dist/commands/mcp.js +111 -14
  6. package/dist/commands/mcpAuth.js +28 -0
  7. package/dist/core/canonical.d.ts +15 -12
  8. package/dist/core/canonical.js +86 -18
  9. package/dist/core/types.d.ts +28 -2
  10. package/dist/core/types.js +19 -0
  11. package/dist/lib/oauth/flow.d.ts +13 -0
  12. package/dist/lib/oauth/flow.js +48 -3
  13. package/dist/lib/secretEnv.d.ts +5 -0
  14. package/dist/lib/secretEnv.js +8 -0
  15. package/dist/pi-bridge/bundle.js +738 -680
  16. package/dist/pi-bridge/index.js +2 -2
  17. package/docs/architecture.md +19 -0
  18. package/docs/mcp-oauth-matrix.md +140 -0
  19. package/docs/research.md +11 -0
  20. package/package.json +4 -2
  21. package/schema/servers.example.yaml +18 -0
  22. package/docs/video/README.md +0 -76
  23. package/docs/video/build/audio/01-opening.aiff +0 -0
  24. package/docs/video/build/audio/01-opening.mp3 +0 -0
  25. package/docs/video/build/audio/02-pain.aiff +0 -0
  26. package/docs/video/build/audio/02-pain.mp3 +0 -0
  27. package/docs/video/build/audio/03-solution.aiff +0 -0
  28. package/docs/video/build/audio/03-solution.mp3 +0 -0
  29. package/docs/video/build/audio/04-install.aiff +0 -0
  30. package/docs/video/build/audio/04-install.mp3 +0 -0
  31. package/docs/video/build/audio/04-setup.mp3 +0 -0
  32. package/docs/video/build/audio/05-dryrun.mp3 +0 -0
  33. package/docs/video/build/audio/05-init.aiff +0 -0
  34. package/docs/video/build/audio/05-init.mp3 +0 -0
  35. package/docs/video/build/audio/05-interactive.mp3 +0 -0
  36. package/docs/video/build/audio/06-dryrun.aiff +0 -0
  37. package/docs/video/build/audio/06-dryrun.mp3 +0 -0
  38. package/docs/video/build/audio/06-onboard.mp3 +0 -0
  39. package/docs/video/build/audio/06-run.mp3 +0 -0
  40. package/docs/video/build/audio/07-onboard.aiff +0 -0
  41. package/docs/video/build/audio/07-onboard.mp3 +0 -0
  42. package/docs/video/build/audio/07-verify.mp3 +0 -0
  43. package/docs/video/build/audio/08-closing.mp3 +0 -0
  44. package/docs/video/build/audio/08-doctor.aiff +0 -0
  45. package/docs/video/build/audio/08-doctor.mp3 +0 -0
  46. package/docs/video/build/audio/09-rollback.aiff +0 -0
  47. package/docs/video/build/audio/09-rollback.mp3 +0 -0
  48. package/docs/video/build/audio/10-principles.aiff +0 -0
  49. package/docs/video/build/audio/10-principles.mp3 +0 -0
  50. package/docs/video/build/audio/11-closing.aiff +0 -0
  51. package/docs/video/build/audio/11-closing.mp3 +0 -0
  52. package/docs/video/build/frames/01-opening-0.png +0 -0
  53. package/docs/video/build/frames/02-pain-1.png +0 -0
  54. package/docs/video/build/frames/02-pain-2.png +0 -0
  55. package/docs/video/build/frames/03-solution-3.png +0 -0
  56. package/docs/video/build/frames/04-install-4.png +0 -0
  57. package/docs/video/build/frames/04-setup-4.png +0 -0
  58. package/docs/video/build/frames/04-setup-5.png +0 -0
  59. package/docs/video/build/frames/05-dryrun-6.png +0 -0
  60. package/docs/video/build/frames/05-init-5.png +0 -0
  61. package/docs/video/build/frames/05-interactive-5.png +0 -0
  62. package/docs/video/build/frames/05-interactive-6.png +0 -0
  63. package/docs/video/build/frames/06-dryrun-6.png +0 -0
  64. package/docs/video/build/frames/06-dryrun-7.png +0 -0
  65. package/docs/video/build/frames/06-onboard-7.png +0 -0
  66. package/docs/video/build/frames/06-onboard-8.png +0 -0
  67. package/docs/video/build/frames/06-run-7.png +0 -0
  68. package/docs/video/build/frames/06-run-8.png +0 -0
  69. package/docs/video/build/frames/07-onboard-10.png +0 -0
  70. package/docs/video/build/frames/07-onboard-7.png +0 -0
  71. package/docs/video/build/frames/07-onboard-8.png +0 -0
  72. package/docs/video/build/frames/07-onboard-9.png +0 -0
  73. package/docs/video/build/frames/07-verify-10.png +0 -0
  74. package/docs/video/build/frames/07-verify-8.png +0 -0
  75. package/docs/video/build/frames/07-verify-9.png +0 -0
  76. package/docs/video/build/frames/08-closing-10.png +0 -0
  77. package/docs/video/build/frames/08-closing-11.png +0 -0
  78. package/docs/video/build/frames/08-doctor-11.png +0 -0
  79. package/docs/video/build/frames/08-doctor-9.png +0 -0
  80. package/docs/video/build/frames/09-rollback-10.png +0 -0
  81. package/docs/video/build/frames/09-rollback-12.png +0 -0
  82. package/docs/video/build/frames/10-principles-11.png +0 -0
  83. package/docs/video/build/frames/10-principles-13.png +0 -0
  84. package/docs/video/build/frames/11-closing-12.png +0 -0
  85. package/docs/video/build/frames/11-closing-14.png +0 -0
  86. package/docs/video/build/segments/01-opening.mp4 +0 -0
  87. package/docs/video/build/segments/01-opening.txt +0 -3
  88. package/docs/video/build/segments/02-pain.mp4 +0 -0
  89. package/docs/video/build/segments/02-pain.txt +0 -5
  90. package/docs/video/build/segments/03-solution.mp4 +0 -0
  91. package/docs/video/build/segments/03-solution.txt +0 -3
  92. package/docs/video/build/segments/04-install.mp4 +0 -0
  93. package/docs/video/build/segments/04-install.txt +0 -3
  94. package/docs/video/build/segments/04-setup.mp4 +0 -0
  95. package/docs/video/build/segments/04-setup.txt +0 -3
  96. package/docs/video/build/segments/05-dryrun.mp4 +0 -0
  97. package/docs/video/build/segments/05-dryrun.txt +0 -3
  98. package/docs/video/build/segments/05-init.mp4 +0 -0
  99. package/docs/video/build/segments/05-init.txt +0 -3
  100. package/docs/video/build/segments/05-interactive.mp4 +0 -0
  101. package/docs/video/build/segments/05-interactive.txt +0 -5
  102. package/docs/video/build/segments/06-dryrun.mp4 +0 -0
  103. package/docs/video/build/segments/06-dryrun.txt +0 -3
  104. package/docs/video/build/segments/06-onboard.mp4 +0 -0
  105. package/docs/video/build/segments/06-onboard.txt +0 -5
  106. package/docs/video/build/segments/06-run.mp4 +0 -0
  107. package/docs/video/build/segments/06-run.txt +0 -3
  108. package/docs/video/build/segments/07-onboard.mp4 +0 -0
  109. package/docs/video/build/segments/07-onboard.txt +0 -5
  110. package/docs/video/build/segments/07-verify.mp4 +0 -0
  111. package/docs/video/build/segments/07-verify.txt +0 -5
  112. package/docs/video/build/segments/08-closing.mp4 +0 -0
  113. package/docs/video/build/segments/08-closing.txt +0 -3
  114. package/docs/video/build/segments/08-doctor.mp4 +0 -0
  115. package/docs/video/build/segments/08-doctor.txt +0 -3
  116. package/docs/video/build/segments/09-rollback.mp4 +0 -0
  117. package/docs/video/build/segments/09-rollback.txt +0 -3
  118. package/docs/video/build/segments/10-principles.mp4 +0 -0
  119. package/docs/video/build/segments/10-principles.txt +0 -3
  120. package/docs/video/build/segments/11-closing.mp4 +0 -0
  121. package/docs/video/build/segments/11-closing.txt +0 -3
  122. package/docs/video/build/segments/final.txt +0 -8
  123. package/docs/video/build-ai/audio/01-hook.mp3 +0 -0
  124. package/docs/video/build-ai/audio/02-pain.mp3 +0 -0
  125. package/docs/video/build-ai/audio/03-solution.mp3 +0 -0
  126. package/docs/video/build-ai/audio/04-how.mp3 +0 -0
  127. package/docs/video/build-ai/audio/05-safe.mp3 +0 -0
  128. package/docs/video/build-ai/audio/06-closing.mp3 +0 -0
  129. package/docs/video/build-ai/clips/01-hook.mp4 +0 -0
  130. package/docs/video/build-ai/clips/02-pain.mp4 +0 -0
  131. package/docs/video/build-ai/clips/03-solution.mp4 +0 -0
  132. package/docs/video/build-ai/clips/04-how.mp4 +0 -0
  133. package/docs/video/build-ai/clips/05-safe.mp4 +0 -0
  134. package/docs/video/build-ai/clips/06-closing.mp4 +0 -0
  135. package/docs/video/build-ai/first-frame-02-pain.png +0 -0
  136. package/docs/video/build-ai/first-frame-03-solution.png +0 -0
  137. package/docs/video/build-ai/first-frame-04-how.png +0 -0
  138. package/docs/video/build-ai/first-frame-05-safe.png +0 -0
  139. package/docs/video/build-ai/segments/01-hook.caption.txt +0 -2
  140. package/docs/video/build-ai/segments/01-hook.mp4 +0 -0
  141. package/docs/video/build-ai/segments/02-pain.mp4 +0 -0
  142. package/docs/video/build-ai/segments/03-solution.mp4 +0 -0
  143. package/docs/video/build-ai/segments/04-how.mp4 +0 -0
  144. package/docs/video/build-ai/segments/05-safe.mp4 +0 -0
  145. package/docs/video/build-ai/segments/06-closing.mp4 +0 -0
  146. package/docs/video/build-ai/segments/cap-01-hook.png +0 -0
  147. package/docs/video/build-ai/segments/cap-02-pain.png +0 -0
  148. package/docs/video/build-ai/segments/cap-03-solution.png +0 -0
  149. package/docs/video/build-ai/segments/cap-04-how.png +0 -0
  150. package/docs/video/build-ai/segments/cap-05-safe.png +0 -0
  151. package/docs/video/build-ai/segments/cap-06-closing.png +0 -0
  152. package/docs/video/build-ai/segments/final.txt +0 -6
  153. package/docs/video/build-posters/audio/01-hook.mp3 +0 -0
  154. package/docs/video/build-posters/audio/02-pain.mp3 +0 -0
  155. package/docs/video/build-posters/audio/03-solution.mp3 +0 -0
  156. package/docs/video/build-posters/audio/04-how.mp3 +0 -0
  157. package/docs/video/build-posters/audio/05-safe.mp3 +0 -0
  158. package/docs/video/build-posters/audio/06-closing.mp3 +0 -0
  159. package/docs/video/build-posters/cap-01-hook.png +0 -0
  160. package/docs/video/build-posters/cap-02-pain.png +0 -0
  161. package/docs/video/build-posters/cap-03-solution.png +0 -0
  162. package/docs/video/build-posters/cap-04-how.png +0 -0
  163. package/docs/video/build-posters/cap-05-safe.png +0 -0
  164. package/docs/video/build-posters/cap-06-closing.png +0 -0
  165. package/docs/video/build-posters/card-01-hook.png +0 -0
  166. package/docs/video/build-posters/card-06-closing.png +0 -0
  167. package/docs/video/build-posters/segments/01-hook.mp4 +0 -0
  168. package/docs/video/build-posters/segments/02-pain.mp4 +0 -0
  169. package/docs/video/build-posters/segments/03-solution.mp4 +0 -0
  170. package/docs/video/build-posters/segments/04-how.mp4 +0 -0
  171. package/docs/video/build-posters/segments/05-safe.mp4 +0 -0
  172. package/docs/video/build-posters/segments/06-closing.mp4 +0 -0
  173. package/docs/video/build-posters/segments/final.txt +0 -6
  174. package/docs/video/generate-video-ai.mjs +0 -334
  175. package/docs/video/generate-video-posters.mjs +0 -204
  176. package/docs/video/generate-video.mjs +0 -426
  177. package/docs/video/outline.zh-CN.md +0 -102
  178. package/docs/video/trellis-demo-video-ai.mp4 +0 -0
  179. package/docs/video/trellis-demo-video.mp4 +0 -0
  180. package/docs/video/voiceover.zh-CN.md +0 -49
@@ -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;
@@ -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
- throw new AuthorizationError(`dynamic client registration failed with HTTP ${response.status}`);
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
- const registered = await registerClient(metadata, redirectUri, { fetchImpl, scope: opts.scope });
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, and no client_id is configured for it`);
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;
@@ -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
@@ -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")) {