@monster-orange/den 0.2.7 → 0.3.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 (139) hide show
  1. package/CHANGELOG.md +1655 -0
  2. package/README.md +4 -2
  3. package/bin/den +2 -1
  4. package/docs/distributions.md +619 -3
  5. package/make/den.mk +19 -4
  6. package/make/journeys.mk +2 -1
  7. package/make/local_dev.mk +6 -13
  8. package/make/secrets-1password.mk +18 -0
  9. package/make/test.mk +1 -1
  10. package/package.json +9 -2
  11. package/schemas/den.plugin.v1.json +156 -0
  12. package/schemas/den.theme.v1.json +269 -0
  13. package/scripts/auth-configure.ts +16 -8
  14. package/scripts/heal-config.ts +0 -5
  15. package/scripts/setup-1password.ts +41 -19
  16. package/scripts/setup-secrets.ts +67 -101
  17. package/src/pillars/api/global-setup.ts +1 -1
  18. package/src/pillars/journeys/sandbox.ts +23 -18
  19. package/src/pillars/journeys/tools/browser-session.ts +2 -2
  20. package/src/scripts/setup-default-workspace.ts +2 -2
  21. package/src/shared/agent-actor.ts +38 -3
  22. package/src/shared/auth/headless-login.ts +1 -1
  23. package/src/shared/auth/sudo-vault.ts +93 -31
  24. package/src/shared/auth/token-providers/index.ts +7 -3
  25. package/src/shared/brand.ts +7 -0
  26. package/src/shared/config-error.ts +63 -0
  27. package/src/shared/constants.ts +3 -24
  28. package/src/shared/den-config.ts +185 -7
  29. package/src/shared/edge/config.ts +147 -21
  30. package/src/shared/edge/exec.ts +31 -0
  31. package/src/shared/edge/included.ts +169 -0
  32. package/src/shared/edge/render.ts +69 -14
  33. package/src/shared/edge/runtime.ts +128 -121
  34. package/src/shared/edge/status.ts +68 -8
  35. package/src/shared/edge/tls.ts +328 -0
  36. package/src/shared/edge/types.ts +25 -9
  37. package/src/shared/environment-manager/catalog.ts +52 -1
  38. package/src/shared/environment-manager/default-workspace.ts +26 -3
  39. package/src/shared/environment-manager/git-remote.ts +49 -0
  40. package/src/shared/environment-manager/health.ts +73 -38
  41. package/src/shared/environment-manager/scope.ts +168 -0
  42. package/src/shared/environment-manager/service-runner.ts +789 -43
  43. package/src/shared/environment-manager/up-targets.ts +132 -0
  44. package/src/shared/environment-manager/workspace-manager.ts +67 -25
  45. package/src/shared/environment-manager/workspace-org.ts +26 -11
  46. package/src/shared/health/rollup.ts +48 -0
  47. package/src/shared/manifest/service-links.ts +20 -8
  48. package/src/shared/open-url.ts +21 -2
  49. package/src/shared/org-sources.ts +328 -0
  50. package/src/shared/org.ts +53 -5
  51. package/src/shared/plugins/manifest.ts +406 -0
  52. package/src/shared/plugins/store.ts +358 -0
  53. package/src/shared/process/legacy-adopt.ts +285 -0
  54. package/src/shared/process/process-group.ts +202 -0
  55. package/src/shared/process/spawn-streaming.ts +7 -0
  56. package/src/shared/secrets/doppler.ts +1 -1
  57. package/src/shared/secrets/index.ts +279 -50
  58. package/src/shared/secrets/none.ts +56 -0
  59. package/src/shared/secrets/strategies.ts +202 -0
  60. package/src/shared/secrets/types.ts +1 -1
  61. package/src/shared/smoke/git-remote.ts +40 -0
  62. package/src/shared/stacks/manifest-commands.ts +1 -1
  63. package/src/shared/stacks/namespace.ts +178 -0
  64. package/src/shared/stacks/owners.ts +303 -0
  65. package/src/shared/stacks/render.ts +8 -3
  66. package/src/shared/stacks/runtime.ts +100 -16
  67. package/src/shared/stacks/status.ts +63 -12
  68. package/src/shared/strategies/registry.ts +89 -0
  69. package/src/shared/theme/contract.ts +538 -0
  70. package/src/shared/types/index.ts +24 -5
  71. package/src/ui/cli/clone-plan.ts +154 -23
  72. package/src/ui/cli/create.ts +24 -7
  73. package/src/ui/cli/doctor-command.ts +131 -0
  74. package/src/ui/cli/edge-command.ts +56 -10
  75. package/src/ui/cli/flag-parser.ts +62 -3
  76. package/src/ui/cli/health-display.ts +25 -0
  77. package/src/ui/cli/index.ts +649 -72
  78. package/src/ui/cli/notice.ts +20 -0
  79. package/src/ui/cli/org-command.ts +12 -2
  80. package/src/ui/cli/plugin-command.ts +849 -0
  81. package/src/ui/cli/stack-command.ts +63 -2
  82. package/src/ui/cli/test-runner.ts +11 -9
  83. package/src/ui/cli/theme-command.ts +136 -0
  84. package/src/ui/cli/up-args.ts +15 -0
  85. package/src/ui/cli/web-command.ts +47 -23
  86. package/src/ui/mcp/api.ts +28 -2
  87. package/src/ui/mcp/scope.ts +298 -0
  88. package/src/ui/mcp/server.ts +70 -8
  89. package/src/ui/mcp/tools.ts +151 -24
  90. package/src/ui/server/appearance-prefs.ts +95 -0
  91. package/src/ui/server/audit.ts +90 -3
  92. package/src/ui/server/daemon-reaper.ts +29 -1
  93. package/src/ui/server/engine-stamp.ts +83 -0
  94. package/src/ui/server/github-service.ts +29 -45
  95. package/src/ui/server/graph-page.ts +20 -0
  96. package/src/ui/server/graph-prefs.ts +44 -1
  97. package/src/ui/server/http-errors.ts +293 -0
  98. package/src/ui/server/http-helpers.ts +22 -3
  99. package/src/ui/server/index.ts +590 -45
  100. package/src/ui/server/public/appearance.js +531 -0
  101. package/src/ui/server/public/edge-page.js +248 -0
  102. package/src/ui/server/public/edge.html +251 -0
  103. package/src/ui/server/public/environments.html +65 -83
  104. package/src/ui/server/public/error-page.js +247 -0
  105. package/src/ui/server/public/error.html +260 -0
  106. package/src/ui/server/public/graph-model.js +332 -14
  107. package/src/ui/server/public/graph.css +141 -47
  108. package/src/ui/server/public/graph.js +223 -44
  109. package/src/ui/server/public/header.js +396 -600
  110. package/src/ui/server/public/health-fit.js +248 -0
  111. package/src/ui/server/public/health-rollup.js +128 -0
  112. package/src/ui/server/public/i18n/es.js +287 -0
  113. package/src/ui/server/public/i18n/ja.js +276 -0
  114. package/src/ui/server/public/index.html +139 -438
  115. package/src/ui/server/public/local-dev.html +3468 -4044
  116. package/src/ui/server/public/logs.html +71 -208
  117. package/src/ui/server/public/panel.js +15 -3
  118. package/src/ui/server/public/release.html +175 -389
  119. package/src/ui/server/public/services.html +596 -770
  120. package/src/ui/server/public/settings-page.js +759 -0
  121. package/src/ui/server/public/settings-registry.js +521 -0
  122. package/src/ui/server/public/settings.html +590 -0
  123. package/src/ui/server/public/shell.js +3869 -0
  124. package/src/ui/server/public/topbar.js +1413 -0
  125. package/src/ui/server/public/version-label.js +12 -0
  126. package/src/ui/server/public/workspace-picker.js +74 -34
  127. package/src/ui/server/readonly-routes.ts +24 -4
  128. package/src/ui/server/release-service.ts +25 -3
  129. package/src/ui/server/services-list.ts +94 -17
  130. package/src/ui/server/settings-distribution.ts +74 -0
  131. package/src/ui/server/shell-prefs.ts +192 -0
  132. package/src/ui/server/theme-routes.ts +292 -0
  133. package/src/ui/server/theme-runtime.ts +407 -0
  134. package/src/ui/server/ui-assets.ts +16 -1
  135. package/src/ui/server/ui-language.ts +50 -0
  136. package/src/ui/server/workspace-routes.ts +55 -9
  137. package/themes/den/den-theme.json +72 -0
  138. package/themes/den/favicon.ico +0 -0
  139. package/themes/den/mark.svg +6 -0
@@ -1,9 +1,12 @@
1
1
  /**
2
2
  * 1Password Developer API Keys setup.
3
3
  *
4
- * Creates and populates the developer-keys item (OP_DEV_KEYS_ITEM) inside the
5
- * personal vault (OP_API_KEYS_VAULT), which services can use to inject
6
- * secrets at `make up` time via `op run --env-file=.env.op`.
4
+ * Creates and populates the developer-keys item inside a vault, which
5
+ * services can use to inject secrets at `make up` time via
6
+ * `op run --env-file=.env.op`. Where it lives is the distribution's choice:
7
+ * `secrets.developer_keys.{vault,item}` in the org config, else the vault
8
+ * and item of `secrets.refs.sudo`. Den has no default; with neither set the
9
+ * script says "not configured" and exits 0 (1 with --check).
7
10
  *
8
11
  * Which keys to collect is org data: `secrets.developer_keys.fields` in the
9
12
  * org config (default: only the keys the engine itself uses), and
@@ -11,7 +14,8 @@
11
14
  * offered as defaults.
12
15
  *
13
16
  * Usage:
14
- * make den-setup-1password — full interactive setup
17
+ * make <cli>-setup-1password — full interactive setup (only when
18
+ * secrets.provider is 1password)
15
19
  * npx tsx scripts/setup-1password.ts --check — non-interactive vault check
16
20
  * exits 0 if ready, 1 if not
17
21
  */
@@ -20,22 +24,23 @@ import { execFile, spawnSync } from "child_process";
20
24
  import { promisify } from "util";
21
25
  import * as fs from "fs";
22
26
  import * as path from "path";
23
- import { fileURLToPath } from "url";
24
27
  import chalk from "chalk";
25
28
  import {
26
- OP_ACCOUNT_DEFAULT,
27
- OP_API_KEYS_VAULT,
28
- OP_DEV_KEYS_ITEM,
29
- } from "../src/shared/constants.js";
30
- import {
29
+ notConfiguredMessage,
31
30
  readSecretsConfig,
31
+ secretsStrategy,
32
32
  type DeveloperKeyDef,
33
33
  } from "../src/shared/secrets/index.js";
34
34
 
35
35
  import { product } from "../src/shared/product.js";
36
- // Ensure op invocations hit the den-owned account even when this script is
37
- // run directly via `npx tsx` (not through make, which already exports OP_ACCOUNT).
38
- process.env.OP_ACCOUNT ||= OP_ACCOUNT_DEFAULT;
36
+ import { getDistRoot } from "../src/shared/paths.js";
37
+ const SECRETS_CFG = readSecretsConfig();
38
+ // op uses the distribution's account (secrets.onepassword.account) or the
39
+ // engineer's own OP_ACCOUNT; Den has none of its own.
40
+ Object.assign(
41
+ process.env,
42
+ secretsStrategy("1password")?.capabilities.env?.(SECRETS_CFG, process.env),
43
+ );
39
44
 
40
45
  const execFileAsync = promisify(execFile);
41
46
 
@@ -46,11 +51,19 @@ async function opJson<T>(args: string[]): Promise<T> {
46
51
 
47
52
  // ─── Constants ────────────────────────────────────────────────────────────────
48
53
 
49
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
50
- const DEN_ROOT = path.resolve(__dirname, "..");
54
+ // dotenv paths are relative to the distribution root, never the engine.
55
+ const DEN_ROOT = getDistRoot();
51
56
 
52
- const VAULT_NAME = OP_API_KEYS_VAULT;
53
- const ITEM_NAME = OP_DEV_KEYS_ITEM;
57
+ const pick = (...v: Array<string | undefined>): string =>
58
+ v.map((x) => (typeof x === "string" ? x.trim() : "")).find(Boolean) ?? "";
59
+ const VAULT_NAME = pick(
60
+ SECRETS_CFG.developer_keys?.vault,
61
+ SECRETS_CFG.refs?.sudo?.vault,
62
+ );
63
+ const ITEM_NAME = pick(
64
+ SECRETS_CFG.developer_keys?.item,
65
+ SECRETS_CFG.refs?.sudo?.item,
66
+ );
54
67
 
55
68
  // ─── .env reader ─────────────────────────────────────────────────────────────
56
69
 
@@ -89,7 +102,7 @@ function parseEnvFile(filePath: string): Record<string, string> {
89
102
  return result;
90
103
  }
91
104
 
92
- const DEVELOPER_KEYS = readSecretsConfig().developer_keys ?? {};
105
+ const DEVELOPER_KEYS = SECRETS_CFG.developer_keys ?? {};
93
106
 
94
107
  /**
95
108
  * The dotenv file `secrets.developer_keys.env_file` names (relative to the
@@ -560,7 +573,16 @@ async function setupMode(): Promise<void> {
560
573
 
561
574
  const isCheck = process.argv.includes("--check");
562
575
 
563
- if (isCheck) {
576
+ if (!VAULT_NAME || !ITEM_NAME) {
577
+ // No place for the developer keys: nothing to set up, and no op call.
578
+ console.log(
579
+ `Developer keys item ${notConfiguredMessage("sudo").replace(
580
+ /secrets\.refs\.sudo/,
581
+ "secrets.developer_keys.vault + .item (or secrets.refs.sudo)",
582
+ )}`,
583
+ );
584
+ process.exit(isCheck ? 1 : 0);
585
+ } else if (isCheck) {
564
586
  checkMode().catch(() => process.exit(1));
565
587
  } else {
566
588
  setupMode().catch((err: Error) => {
@@ -1,17 +1,21 @@
1
1
  /**
2
2
  * Backend-agnostic developer-secrets setup.
3
3
  *
4
- * Dispatches to the strategy selected by `secrets.provider` in
5
- * config/den.yml (override: DEN_SECRETS_PROVIDER or
6
- * config/local/user.yml). See src/shared/secrets.
4
+ * Dispatches to the `setup` capability of the strategy selected by
5
+ * `secrets.provider` in the org config (override: DEN_SECRETS_PROVIDER or
6
+ * config/local/user.yml). See src/shared/secrets/strategies.ts.
7
7
  *
8
8
  * Usage:
9
9
  * make den-setup-secrets — interactive setup
10
10
  * npx tsx scripts/setup-secrets.ts --check — non-interactive; exit 0 when ready
11
11
  * npx tsx scripts/setup-secrets.ts --cli — print "<provider> <binary> <formula>"
12
- * npx tsx scripts/setup-secrets.ts --gate — like --check, but a backend that
13
- * is not ready only warns unless
14
- * secrets.required is true
12
+ * npx tsx scripts/setup-secrets.ts --provider — print the selected strategy id
13
+ * npx tsx scripts/setup-secrets.ts --gate — the up / web gate: passes
14
+ * silently unless secrets.required
15
+ * is true, then like --check
16
+ *
17
+ * With `secrets.provider: none` there is nothing to set up: it says so and
18
+ * exits 0.
15
19
  */
16
20
  import { spawnSync } from "child_process";
17
21
  import * as path from "path";
@@ -20,6 +24,8 @@ import chalk from "chalk";
20
24
  import {
21
25
  getSecretsProvider,
22
26
  readSecretsConfig,
27
+ resolveProvider,
28
+ strategyFor,
23
29
  } from "../src/shared/secrets/index.js";
24
30
 
25
31
  import { envKey, product } from "../src/shared/product.js";
@@ -31,126 +37,86 @@ function run(cmd: string, args: string[]): number {
31
37
  return spawnSync(cmd, args, { stdio: "inherit", cwd: ROOT }).status ?? 1;
32
38
  }
33
39
 
34
- function installed(cmd: string): boolean {
35
- return !spawnSync(cmd, ["--version"], { stdio: "ignore" }).error;
36
- }
37
-
38
- const INSTALL: Record<string, string> = {
39
- doppler: "brew install dopplerhq/cli/doppler",
40
- op: "brew install 1password-cli",
41
- };
42
-
43
- function otherBackends(current: string): string {
44
- return (
45
- "Or pick another backend for just you (config/local/user.yml):\n" +
46
- " secrets:\n" +
47
- ` provider: env # or 1password / doppler (team default: ${current})\n` +
48
- ` or for one shell: ${envKey("SECRETS_PROVIDER")}=env make web`
49
- );
50
- }
40
+ const NONE_MESSAGE =
41
+ "No secrets backend is configured (secrets.provider: none), so there is nothing to set up.\n" +
42
+ `To use one, set secrets.provider (doppler | 1password | env) and secrets.refs.<name> in ${product().configFile}.`;
51
43
 
52
44
  async function main(): Promise<number> {
53
- const provider = getSecretsProvider();
54
45
  const cfg = readSecretsConfig();
55
- const label = `secrets backend: ${provider.name}`;
46
+ const resolved = resolveProvider(cfg);
47
+ const strategy = strategyFor(resolved.name);
48
+ const label = `secrets backend: ${resolved.name}`;
49
+
50
+ // `--provider`: print the selected strategy id (make reads it to offer
51
+ // backend-specific targets). Never probes the backend.
52
+ if (process.argv.includes("--provider")) {
53
+ console.log(resolved.name);
54
+ return 0;
55
+ }
56
56
 
57
57
  // `--cli`: print "<provider> <binary> <brew formula>" for make setup's
58
- // install step ("env -" when the backend needs no CLI).
58
+ // install step ("<provider> - -" when the backend needs no CLI). An unset
59
+ // provider needs nothing installed until something asks for a secret.
59
60
  if (process.argv.includes("--cli")) {
60
- const bin =
61
- provider.name === "doppler"
62
- ? "doppler"
63
- : provider.name === "1password"
64
- ? "op"
65
- : "";
66
- const formula = bin ? INSTALL[bin].replace(/^brew install /, "") : "";
67
- console.log(`${provider.name} ${bin || "-"} ${formula || "-"}`);
61
+ const cli = resolved.implicit ? undefined : strategy.capabilities.cli;
62
+ console.log(`${resolved.name} ${cli?.bin ?? "-"} ${cli?.formula ?? "-"}`);
68
63
  return 0;
69
64
  }
70
65
 
66
+ // The up / web gate: only a distribution that requires its backend
67
+ // (secrets.required) is checked. Nothing required: pass, silently, whatever
68
+ // the provider; a feature that needs a secret reports it when it asks.
69
+ if (gate && !cfg.required) return 0;
70
+
71
71
  if (check || gate) {
72
- const r = await provider.preflight();
73
- if (r.ok) return 0;
74
- if (gate && !cfg.required) {
75
- // Signed in but no default scope: secret_env refs name their own
76
- // (<project>/<config>/<key>), so only the engine's own secrets lack one.
77
- if (await provider.isReady()) {
78
- console.error(
79
- chalk.gray(
80
- ` ${label}: signed in, no default scope set (secret_env refs still resolve;` +
81
- ` test logins and the saved sudo password need one: \`make ${product().cli}-setup-secrets\`).`,
82
- ),
83
- );
84
- return 0;
85
- }
86
- console.error(
87
- chalk.yellow(
88
- `⚠️ ${label} is not ready — continuing.\n` +
89
- " Test logins and the saved sudo password need it; run " +
90
- `\`make ${product().cli}-setup-secrets\` when you want them.`,
91
- ),
92
- );
72
+ if (resolved.name === "none" || (resolved.implicit && !cfg.required)) {
73
+ if (resolved.name === "none")
74
+ console.log(` ✓ ${NONE_MESSAGE.split("\n")[0]}`);
93
75
  return 0;
94
76
  }
77
+ const r = await getSecretsProvider().preflight();
78
+ if (r.ok) return 0;
95
79
  console.error(chalk.red(`✗ ${label}\n${r.message ?? ""}`));
96
80
  return 1;
97
81
  }
98
82
 
83
+ const setup = strategy.capabilities.setup;
84
+ if (resolved.name === "none" || !setup) {
85
+ console.log(NONE_MESSAGE);
86
+ return 0;
87
+ }
88
+ if (resolved.implicit && !cfg.required) {
89
+ // Nothing chose a backend and nothing requires one: no store to set up.
90
+ console.log(
91
+ "secrets.provider is not set, so there is nothing to set up.\n" +
92
+ `To use a backend, set secrets.provider (doppler | 1password | env | none) in ${product().configFile}.`,
93
+ );
94
+ return 0;
95
+ }
99
96
  console.log(
100
97
  chalk.bold(`\n${product().title} developer secrets (${label})\n`),
101
98
  );
102
- switch (provider.name) {
103
- case "doppler": {
104
- if (!installed("doppler")) {
105
- console.error(
106
- chalk.red("✗ The Doppler CLI is not installed.\n") +
107
- ` Install it: ${INSTALL.doppler}\n then re-run: make ${product().cli}-setup-secrets\n\n ` +
108
- otherBackends(provider.name),
109
- );
110
- return 1;
111
- }
112
- if (!(await provider.isReady())) {
113
- console.log("→ doppler login");
114
- if (run("doppler", ["login"]) !== 0) return 1;
115
- }
116
- const scope: string[] = [];
117
- if (cfg.doppler?.project) scope.push("--project", cfg.doppler.project);
118
- if (cfg.doppler?.config) scope.push("--config", cfg.doppler.config);
119
- console.log(`→ doppler setup ${scope.join(" ")}`.trim());
120
- if (
121
- run("doppler", ["setup", "--no-interactive", ...scope]) !== 0 &&
122
- scope.length === 0
123
- ) {
124
- run("doppler", ["setup"]);
125
- }
126
- break;
127
- }
128
- case "1password":
129
- if (!installed("op")) {
130
- console.error(
131
- chalk.red("✗ The 1Password CLI (op) is not installed.\n") +
132
- ` Install it: ${INSTALL.op}\n\n ` +
133
- otherBackends(provider.name),
134
- );
135
- return 1;
136
- }
137
- return run(path.join(ROOT, "node_modules", ".bin", "tsx"), [
138
- path.join(ROOT, "scripts", "setup-1password.ts"),
139
- ]);
140
- case "env":
141
- console.log(
142
- "The env backend reads secrets from the process environment.\n" +
143
- `Export them yourself (direnv, CI secrets, \`doppler run -- ${product().cli} up\`).`,
144
- );
145
- break;
146
- }
99
+ const provider = getSecretsProvider();
100
+ const code = await setup({
101
+ cfg,
102
+ provider,
103
+ engineRoot: ROOT,
104
+ cli: product().cli,
105
+ run,
106
+ log: (l) => console.log(l),
107
+ error: (l) => console.error(chalk.red(l)),
108
+ });
109
+ if (code !== 0) return code;
147
110
  const r = await provider.preflight();
148
111
  console.log(
149
112
  r.ok ? chalk.green(`✓ ${label} ready`) : chalk.red(`✗ ${r.message}`),
150
113
  );
114
+ if (!r.ok)
115
+ console.log(
116
+ ` Or for one shell: ${envKey("SECRETS_PROVIDER")}=none make web`,
117
+ );
151
118
  return r.ok ? 0 : 1;
152
119
  }
153
-
154
120
  main().then(
155
121
  (code) => process.exit(code),
156
122
  (err: unknown) => {
@@ -86,7 +86,7 @@ async function globalSetup(_config: FullConfig) {
86
86
  ` ⚠ No credentials for ${role} — authenticated ${role} tests will fail`,
87
87
  );
88
88
  console.warn(
89
- ` eval $(op signin) && make auth-configure (admin) or verify 1Password vault`,
89
+ ` set E2E_${role.toUpperCase()}_EMAIL / _PASSWORD, or set up the secrets backend (secrets.provider)`,
90
90
  );
91
91
  } else {
92
92
  // Token mint failure, IdP misconfiguration, etc. — fail fast
@@ -36,10 +36,10 @@
36
36
 
37
37
  import * as fs from "fs";
38
38
  import * as path from "path";
39
- import { OP_API_KEYS_VAULT, OP_DEV_KEYS_ITEM } from "../../shared/constants.js";
40
39
  import {
41
40
  getSecretsProvider,
42
41
  namedSecretRef,
42
+ notConfiguredMessage,
43
43
  type SecretRef,
44
44
  } from "../../shared/secrets/index.js";
45
45
  import type { Environment } from "../../shared/types/index.js";
@@ -51,22 +51,12 @@ import { getDistRoot } from "../../shared/paths.js";
51
51
  import { containsValue } from "../../shared/redact.js";
52
52
  // ── Layer 1: captureSecrets ──────────────────────────────────────────────────
53
53
 
54
- /** 1Password field label holding the Cursor API key (= env var name). */
54
+ /** The Cursor API key's env var name (and the usual field label). */
55
55
  export const CURSOR_API_KEY_FIELD = "CURSOR_API_KEY";
56
56
 
57
57
  /** Name of the agent-key ref under `secrets.refs` in the org config. */
58
58
  export const JOURNEYS_AGENT_KEY_REF = "journeys_agent_key";
59
59
 
60
- /**
61
- * Where the agent key lives when `secrets.refs.journeys_agent_key` is not
62
- * configured: the engineer's own developer-keys item (neutral default).
63
- */
64
- export const JOURNEYS_AGENT_KEY_DEFAULT: SecretRef = {
65
- vault: OP_API_KEYS_VAULT,
66
- item: OP_DEV_KEYS_ITEM,
67
- field: CURSOR_API_KEY_FIELD,
68
- };
69
-
70
60
  /**
71
61
  * Secrets captured into memory before environment sanitization.
72
62
  *
@@ -84,6 +74,7 @@ export interface OpFieldRequest {
84
74
  item: string;
85
75
  vault: string;
86
76
  field: string;
77
+ key?: string;
87
78
  }
88
79
 
89
80
  export interface CaptureSecretsDeps {
@@ -91,8 +82,11 @@ export interface CaptureSecretsDeps {
91
82
  opReadField?: (request: OpFieldRequest) => Promise<string>;
92
83
  /** Env-var source; defaults to process.env. */
93
84
  env?: NodeJS.ProcessEnv;
94
- /** Where the key lives; defaults to the configured `journeys_agent_key` ref. */
95
- agentKeyRef?: SecretRef;
85
+ /**
86
+ * Where the key lives; defaults to the configured `journeys_agent_key`
87
+ * ref. null: not configured (there is no built-in location).
88
+ */
89
+ agentKeyRef?: SecretRef | null;
96
90
  }
97
91
 
98
92
  /** Default reader: the configured secrets strategy. */
@@ -118,14 +112,25 @@ export async function captureSecrets(
118
112
  if (cursorApiKey.length === 0) {
119
113
  let opError = "";
120
114
  const ref =
121
- deps.agentKeyRef ??
122
- namedSecretRef(JOURNEYS_AGENT_KEY_REF, JOURNEYS_AGENT_KEY_DEFAULT);
115
+ deps.agentKeyRef !== undefined
116
+ ? deps.agentKeyRef
117
+ : namedSecretRef(JOURNEYS_AGENT_KEY_REF);
118
+ if (!ref) {
119
+ // No configured place for the key: say so, without asking a backend.
120
+ throw new Error(
121
+ `No Cursor API key available for journey agents.\n` +
122
+ ` Options:\n` +
123
+ ` • set the ${CURSOR_API_KEY_FIELD} env var\n` +
124
+ ` • secrets backend: ${notConfiguredMessage(JOURNEYS_AGENT_KEY_REF)}\n`,
125
+ );
126
+ }
123
127
  try {
124
128
  cursorApiKey = (
125
129
  await opReadField({
126
130
  item: ref.item,
127
131
  vault: ref.vault,
128
132
  field: ref.field,
133
+ ...(ref.key ? { key: ref.key } : {}),
129
134
  })
130
135
  ).trim();
131
136
  } catch (error) {
@@ -137,8 +142,8 @@ export async function captureSecrets(
137
142
  ` Options:\n` +
138
143
  ` • CI: set the ${CURSOR_API_KEY_FIELD} env var\n` +
139
144
  ` • Local: sign in to the secrets backend (make ${product().cli}-setup-secrets) and\n` +
140
- ` make sure the "${ref.item}" item in the "${ref.vault}" vault has a\n` +
141
- ` ${ref.field} field (move it with secrets.refs.${JOURNEYS_AGENT_KEY_REF})\n` +
145
+ ` make sure secrets.refs.${JOURNEYS_AGENT_KEY_REF} (item "${ref.item}", vault "${ref.vault}",\n` +
146
+ ` field ${ref.field}) holds the key\n` +
142
147
  (opError ? ` Secrets backend error: ${opError}\n` : ""),
143
148
  );
144
149
  }
@@ -32,7 +32,7 @@ import { product } from "../../../shared/product.js";
32
32
  * Mint a fresh authenticated browser session for `role` against `env`'s
33
33
  * web surface and return the storageState auth-file path (and nothing
34
34
  * else). Throws a loud, actionable error when credentials cannot be
35
- * sourced — naming both the 1Password path (local) and the E2E_* env-var
35
+ * sourced — naming both the secrets-backend path (local) and the E2E_* env-var
36
36
  * path (CI), mirroring browser/global-setup.ts.
37
37
  */
38
38
  export async function provisionBrowserSession(
@@ -47,7 +47,7 @@ export async function provisionBrowserSession(
47
47
  if (!credentials) {
48
48
  const localHint =
49
49
  role === "admin"
50
- ? ` Local: make auth-configure env=${env} (handles 1Password sign-in automatically)\n`
50
+ ? ` Local: with secrets.provider: 1password, make auth-configure env=${env}\n`
51
51
  : ` Local: sign in to the secrets backend (make ${product().cli}-setup-secrets); the ${role} account comes from test_accounts in the org config\n`;
52
52
  throw new Error(
53
53
  `No credentials for ${role}.\n` +
@@ -145,7 +145,7 @@ async function main() {
145
145
  ' This builds the "default" workspace — every standard system repo,\n' +
146
146
  ` all managed: ${product().cli} clones and keeps each one up to date for\n` +
147
147
  " you, no checkouts or decisions required. For anything you actively\n" +
148
- " develop, flip that repo to dev-managed in the web UX (Local Dev)\n" +
148
+ " develop, flip that repo to dev-managed in the web UX (Workspaces)\n" +
149
149
  " and point it at your own checkout.\n",
150
150
  );
151
151
 
@@ -183,7 +183,7 @@ async function main() {
183
183
  `\n ✓ Default workspace configured with ${seed.length} service(s), all managed.`,
184
184
  );
185
185
  console.log(
186
- " Change any repo to dev-managed in the web UX: make web → Local Dev.",
186
+ " Change any repo to dev-managed in the web UX: make web → Workspaces.",
187
187
  );
188
188
  console.log(` Run 'make ${product().cli}-up' to start your services.\n`);
189
189
  }
@@ -12,8 +12,20 @@ export const AUDIT_HEADERS = {
12
12
  clientVersion: "x-den-mcp-client-version",
13
13
  pid: "x-den-mcp-pid",
14
14
  session: "x-den-mcp-session",
15
+ /** A scoped server's actor id (`agent:<id>`, AG-2a-core). */
16
+ agent: "x-den-mcp-agent",
17
+ /** The workspace a scoped server is pinned to (AG-2a-core). */
18
+ workspace: "x-den-mcp-workspace",
15
19
  } as const;
16
20
 
21
+ /**
22
+ * `agent:<id>`: what `<cli> mcp --actor` takes. The id is letters, digits,
23
+ * dots, dashes and underscores, 1-64 long, starting with a letter or digit.
24
+ */
25
+ export const AGENT_ACTOR_RE = /^agent:[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
26
+ /** A workspace name, as the config accepts it (1-64 long here). */
27
+ export const WORKSPACE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
28
+
17
29
  export interface AgentInfo {
18
30
  /** The MCP client's self-reported name (clientInfo.name). */
19
31
  client: string;
@@ -23,6 +35,10 @@ export interface AgentInfo {
23
35
  mcpPid: number | null;
24
36
  /** The MCP server's session id (one per `<cli> mcp` process). */
25
37
  session: string;
38
+ /** A scoped server's actor (`agent:<id>`); absent when unscoped. */
39
+ agent?: string;
40
+ /** The workspace a scoped server is pinned to; absent when unscoped. */
41
+ workspace?: string;
26
42
  }
27
43
 
28
44
  export interface AgentActor extends AgentInfo {
@@ -49,6 +65,13 @@ export function agentAuditHeaders(info: AgentInfo): Record<string, string> {
49
65
  [AUDIT_HEADERS.clientVersion]: clean(info.clientVersion) || "unknown",
50
66
  [AUDIT_HEADERS.pid]: info.mcpPid === null ? "" : String(info.mcpPid),
51
67
  [AUDIT_HEADERS.session]: clean(info.session).replace(/[^A-Za-z0-9-]/g, ""),
68
+ // Only a scoped server sends these: an unscoped one's headers are as before.
69
+ ...(info.agent && AGENT_ACTOR_RE.test(info.agent)
70
+ ? { [AUDIT_HEADERS.agent]: info.agent }
71
+ : {}),
72
+ ...(info.workspace && WORKSPACE_NAME_RE.test(info.workspace)
73
+ ? { [AUDIT_HEADERS.workspace]: info.workspace }
74
+ : {}),
52
75
  };
53
76
  }
54
77
 
@@ -59,19 +82,31 @@ export function actorFromHeaders(
59
82
  const h = headers as Record<string, string | string[] | undefined>;
60
83
  if (clean(h[AUDIT_HEADERS.actor]) !== "agent") return null;
61
84
  const pid = clean(h[AUDIT_HEADERS.pid], 10);
85
+ // Kept only when well-formed: a malformed value is dropped, never cleaned
86
+ // into something that looks like another agent's id.
87
+ const agent = clean(h[AUDIT_HEADERS.agent], 80);
88
+ const workspace = clean(h[AUDIT_HEADERS.workspace], 80);
62
89
  return {
63
90
  kind: "agent",
64
91
  client: clean(h[AUDIT_HEADERS.client]) || "unknown",
65
92
  clientVersion: clean(h[AUDIT_HEADERS.clientVersion], 32) || "unknown",
66
93
  mcpPid: /^\d+$/.test(pid) ? Number(pid) : null,
67
94
  session: clean(h[AUDIT_HEADERS.session]).replace(/[^A-Za-z0-9-]/g, ""),
95
+ ...(AGENT_ACTOR_RE.test(agent) ? { agent } : {}),
96
+ ...(WORKSPACE_NAME_RE.test(workspace) ? { workspace } : {}),
68
97
  };
69
98
  }
70
99
 
71
- /** "agent <client> <version> (mcp pid <pid>, session <id>)". */
100
+ /**
101
+ * "agent <client> <version> (mcp pid <pid>, session <id>)"; a scoped
102
+ * server's actor leads: "agent:<id> <client> <version> (workspace <ws>,
103
+ * mcp pid <pid>, session <id>)".
104
+ */
72
105
  export function actorLabel(actor: AgentActor): string {
106
+ const who = actor.agent ?? "agent";
107
+ const ws = actor.workspace ? `workspace ${actor.workspace}, ` : "";
73
108
  return (
74
- `agent ${actor.client} ${actor.clientVersion} ` +
75
- `(mcp pid ${actor.mcpPid ?? "?"}, session ${actor.session || "?"})`
109
+ `${who} ${actor.client} ${actor.clientVersion} ` +
110
+ `(${ws}mcp pid ${actor.mcpPid ?? "?"}, session ${actor.session || "?"})`
76
111
  );
77
112
  }
@@ -34,7 +34,7 @@ const PROJECT_ROOT = getDistRoot();
34
34
  const AUTH_DIR = path.resolve(PROJECT_ROOT, "playwright/.auth");
35
35
 
36
36
  // Per-engineer admin config — stores email + secret reference (no password)
37
- // Created by: make auth-configure [env=<env>]
37
+ // Created by: make auth-configure [env=<env>] (secrets.provider: 1password)
38
38
  // Lookup order: config/local/admin-auth-{env}.json → config/local/admin-auth.json
39
39
  const LOCAL_CONFIG_DIR = path.resolve(PROJECT_ROOT, "config/local");
40
40