@monster-orange/den 0.2.8 → 0.4.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/CHANGELOG.md +1731 -0
- package/README.md +4 -2
- package/bin/den +2 -1
- package/docs/distributions.md +730 -6
- package/make/den.mk +19 -4
- package/make/journeys.mk +2 -1
- package/make/local_dev.mk +6 -13
- package/make/secrets-1password.mk +18 -0
- package/make/test.mk +1 -1
- package/package.json +12 -2
- package/schemas/den.plugin.v1.json +156 -0
- package/schemas/den.theme.v1.json +269 -0
- package/scripts/auth-configure.ts +16 -8
- package/scripts/heal-config.ts +0 -5
- package/scripts/setup-1password.ts +41 -19
- package/scripts/setup-secrets.ts +67 -101
- package/src/pillars/api/global-setup.ts +1 -1
- package/src/pillars/journeys/sandbox.ts +23 -18
- package/src/pillars/journeys/tools/browser-session.ts +2 -2
- package/src/scripts/setup-default-workspace.ts +2 -2
- package/src/shared/agents/default-pack/be-dev.md +20 -0
- package/src/shared/agents/default-pack/devops.md +14 -0
- package/src/shared/agents/default-pack/fe-dev.md +20 -0
- package/src/shared/agents/default-pack/orchestrator.md +16 -0
- package/src/shared/agents/default-pack/planner.md +17 -0
- package/src/shared/agents/default-pack/reviewer.md +14 -0
- package/src/shared/agents/default-pack/ux-designer.md +15 -0
- package/src/shared/agents/den-claude.ts +173 -0
- package/src/shared/agents/generic.ts +52 -0
- package/src/shared/agents/personas.ts +218 -0
- package/src/shared/agents/records.ts +118 -0
- package/src/shared/agents/runtime.ts +192 -0
- package/src/shared/agents/runtimes.ts +47 -0
- package/src/shared/auth/headless-login.ts +1 -1
- package/src/shared/auth/sudo-vault.ts +93 -31
- package/src/shared/auth/token-providers/index.ts +7 -3
- package/src/shared/brand.ts +7 -0
- package/src/shared/config-error.ts +33 -3
- package/src/shared/constants.ts +3 -24
- package/src/shared/den-config.ts +257 -5
- package/src/shared/edge/config.ts +147 -21
- package/src/shared/edge/exec.ts +31 -0
- package/src/shared/edge/included.ts +169 -0
- package/src/shared/edge/render.ts +69 -14
- package/src/shared/edge/runtime.ts +128 -121
- package/src/shared/edge/status.ts +68 -8
- package/src/shared/edge/tls.ts +328 -0
- package/src/shared/edge/types.ts +25 -9
- package/src/shared/engine.ts +23 -0
- package/src/shared/environment-manager/default-workspace.ts +16 -1
- package/src/shared/environment-manager/health.ts +73 -38
- package/src/shared/environment-manager/scope.ts +168 -0
- package/src/shared/environment-manager/service-runner.ts +789 -43
- package/src/shared/environment-manager/up-targets.ts +132 -0
- package/src/shared/environment-manager/workspaces.ts +19 -0
- package/src/shared/health/rollup.ts +48 -0
- package/src/shared/open-url.ts +21 -2
- package/src/shared/org-sources.ts +78 -59
- package/src/shared/org.ts +53 -5
- package/src/shared/plugins/manifest.ts +406 -0
- package/src/shared/plugins/store.ts +358 -0
- package/src/shared/process/legacy-adopt.ts +285 -0
- package/src/shared/process/process-group.ts +202 -0
- package/src/shared/process/spawn-streaming.ts +7 -0
- package/src/shared/secret-env.ts +70 -0
- package/src/shared/secrets/doppler.ts +1 -1
- package/src/shared/secrets/index.ts +279 -50
- package/src/shared/secrets/none.ts +56 -0
- package/src/shared/secrets/strategies.ts +202 -0
- package/src/shared/secrets/types.ts +1 -1
- package/src/shared/sessions/client.ts +277 -0
- package/src/shared/sessions/den-pty.ts +97 -0
- package/src/shared/sessions/env.ts +79 -0
- package/src/shared/sessions/host.ts +667 -0
- package/src/shared/sessions/names.ts +85 -0
- package/src/shared/sessions/paths.ts +27 -0
- package/src/shared/sessions/protocol.ts +72 -0
- package/src/shared/sessions/pty.ts +141 -0
- package/src/shared/sessions/ring-buffer.ts +160 -0
- package/src/shared/sessions/types.ts +81 -0
- package/src/shared/smoke/git-remote.ts +40 -0
- package/src/shared/stacks/manifest-commands.ts +1 -1
- package/src/shared/stacks/namespace.ts +178 -0
- package/src/shared/stacks/owners.ts +303 -0
- package/src/shared/stacks/render.ts +8 -3
- package/src/shared/stacks/runtime.ts +100 -16
- package/src/shared/stacks/status.ts +63 -12
- package/src/shared/strategies/registry.ts +89 -0
- package/src/shared/theme/contract.ts +538 -0
- package/src/shared/types/index.ts +12 -3
- package/src/ui/cli/agents-command.ts +812 -0
- package/src/ui/cli/create.ts +24 -7
- package/src/ui/cli/doctor-command.ts +260 -0
- package/src/ui/cli/edge-command.ts +56 -10
- package/src/ui/cli/env-probe.ts +266 -0
- package/src/ui/cli/flag-parser.ts +18 -5
- package/src/ui/cli/health-display.ts +25 -0
- package/src/ui/cli/index.ts +580 -60
- package/src/ui/cli/notice.ts +20 -0
- package/src/ui/cli/plugin-command.ts +849 -0
- package/src/ui/cli/sessions-command.ts +718 -0
- package/src/ui/cli/stack-command.ts +63 -2
- package/src/ui/cli/test-runner.ts +11 -9
- package/src/ui/cli/theme-command.ts +136 -0
- package/src/ui/cli/web-command.ts +47 -23
- package/src/ui/mcp/api.ts +28 -2
- package/src/ui/mcp/scope.ts +145 -20
- package/src/ui/mcp/server.ts +27 -2
- package/src/ui/mcp/tools.ts +94 -12
- package/src/ui/server/appearance-prefs.ts +95 -0
- package/src/ui/server/audit.ts +35 -0
- package/src/ui/server/daemon-reaper.ts +29 -1
- package/src/ui/server/engine-stamp.ts +83 -0
- package/src/ui/server/github-service.ts +23 -44
- package/src/ui/server/graph-page.ts +20 -0
- package/src/ui/server/graph-prefs.ts +44 -1
- package/src/ui/server/http-errors.ts +293 -0
- package/src/ui/server/http-helpers.ts +22 -3
- package/src/ui/server/index.ts +539 -51
- package/src/ui/server/public/appearance.js +531 -0
- package/src/ui/server/public/edge-page.js +248 -0
- package/src/ui/server/public/edge.html +251 -0
- package/src/ui/server/public/environments.html +65 -83
- package/src/ui/server/public/error-page.js +247 -0
- package/src/ui/server/public/error.html +260 -0
- package/src/ui/server/public/graph-model.js +332 -14
- package/src/ui/server/public/graph.css +141 -47
- package/src/ui/server/public/graph.js +223 -44
- package/src/ui/server/public/header.js +396 -600
- package/src/ui/server/public/health-fit.js +248 -0
- package/src/ui/server/public/health-rollup.js +128 -0
- package/src/ui/server/public/i18n/es.js +287 -0
- package/src/ui/server/public/i18n/ja.js +276 -0
- package/src/ui/server/public/index.html +139 -438
- package/src/ui/server/public/local-dev.html +3468 -4044
- package/src/ui/server/public/logs.html +71 -208
- package/src/ui/server/public/panel.js +14 -2
- package/src/ui/server/public/release.html +175 -389
- package/src/ui/server/public/services.html +596 -770
- package/src/ui/server/public/settings-page.js +759 -0
- package/src/ui/server/public/settings-registry.js +521 -0
- package/src/ui/server/public/settings.html +590 -0
- package/src/ui/server/public/shell.js +3869 -0
- package/src/ui/server/public/topbar.js +1413 -0
- package/src/ui/server/public/version-label.js +12 -0
- package/src/ui/server/public/workspace-picker.js +74 -34
- package/src/ui/server/readonly-routes.ts +37 -3
- package/src/ui/server/services-list.ts +94 -17
- package/src/ui/server/settings-distribution.ts +74 -0
- package/src/ui/server/shell-prefs.ts +192 -0
- package/src/ui/server/theme-routes.ts +292 -0
- package/src/ui/server/theme-runtime.ts +407 -0
- package/src/ui/server/ui-assets.ts +16 -1
- package/src/ui/server/ui-language.ts +50 -0
- package/src/ui/server/workspace-routes.ts +48 -18
- package/themes/den/den-theme.json +72 -0
- package/themes/den/favicon.ico +0 -0
- package/themes/den/mark.svg +6 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime contract (strategy kind `runtime`, AG spec §4): which CLI an
|
|
3
|
+
* agent runs. A runtime turns a persona and a workspace into argv, env and
|
|
4
|
+
* files (a per-agent MCP config), and says how to resume. It knows nothing
|
|
5
|
+
* about sessions, tmux or the browser; Den joins it to a session backend.
|
|
6
|
+
*
|
|
7
|
+
* Built-ins register here exactly as a runtime plugin would (AG-8a):
|
|
8
|
+
* den-claude and generic.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import {
|
|
13
|
+
getStrategy,
|
|
14
|
+
registerStrategy,
|
|
15
|
+
strategyIds,
|
|
16
|
+
type StrategyEntry,
|
|
17
|
+
} from "../strategies/registry.js";
|
|
18
|
+
import type { Persona } from "./personas.js";
|
|
19
|
+
|
|
20
|
+
export const RUNTIME_KIND = "runtime";
|
|
21
|
+
|
|
22
|
+
/** The runtime an agent gets when neither it nor its persona names one. */
|
|
23
|
+
export const DEFAULT_RUNTIME_ID = "den-claude";
|
|
24
|
+
|
|
25
|
+
export interface RuntimeDetect {
|
|
26
|
+
available: boolean;
|
|
27
|
+
version?: string;
|
|
28
|
+
/** Why it is unavailable. */
|
|
29
|
+
why?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The MCP server an agent acts through: Den's, scoped to its workspace. */
|
|
33
|
+
export interface AgentMcp {
|
|
34
|
+
/** The server's name in an MCP config (the distribution's CLI). */
|
|
35
|
+
name: string;
|
|
36
|
+
/** `<cli> mcp --workspace <ws> --actor agent:<id> [--grant <ws>…]`. */
|
|
37
|
+
command: string[];
|
|
38
|
+
/** The server's own environment (PATH pinned), never a secret. */
|
|
39
|
+
env: Record<string, string>;
|
|
40
|
+
/** The workspace it is pinned to. */
|
|
41
|
+
scope: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface LaunchInput {
|
|
45
|
+
persona: Persona;
|
|
46
|
+
workspace: { name: string; dir: string };
|
|
47
|
+
agentId: string;
|
|
48
|
+
/** Where the agent works: the workspace's dir (a worktree from AG-9a). */
|
|
49
|
+
worktree: string;
|
|
50
|
+
model?: string;
|
|
51
|
+
mcp: AgentMcp;
|
|
52
|
+
/** A directory of this agent's own (0700) for the files a runtime writes. */
|
|
53
|
+
filesDir: string;
|
|
54
|
+
/**
|
|
55
|
+
* The caller's environment, for finding the runtime's CLI on PATH. The
|
|
56
|
+
* agent does not get it: it starts from an allowlist (sessions/env.ts),
|
|
57
|
+
* plus envPassthrough and LaunchOutput.env.
|
|
58
|
+
*/
|
|
59
|
+
env: NodeJS.ProcessEnv;
|
|
60
|
+
/**
|
|
61
|
+
* A conversation id to start under instead of a new one: a resume of a
|
|
62
|
+
* run that was never prompted starts afresh with the id it had.
|
|
63
|
+
*/
|
|
64
|
+
resumeToken?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface LaunchOutput {
|
|
68
|
+
argv: string[];
|
|
69
|
+
/** Added to the session's environment (after the allowlist). */
|
|
70
|
+
env: Record<string, string>;
|
|
71
|
+
/** Absolute path (inside filesDir) → content; Den writes each 0600. */
|
|
72
|
+
files?: Record<string, string>;
|
|
73
|
+
/** What resume needs to continue this run (kept in the agent record). */
|
|
74
|
+
resumeToken?: string;
|
|
75
|
+
/** One line each, printed when the agent starts (never secret). */
|
|
76
|
+
warnings?: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type ResumeInput = LaunchInput;
|
|
80
|
+
|
|
81
|
+
export interface Runtime {
|
|
82
|
+
/** "den-claude", "generic". */
|
|
83
|
+
id: string;
|
|
84
|
+
detect(env?: NodeJS.ProcessEnv): RuntimeDetect;
|
|
85
|
+
/** Throws LaunchError when this persona or this machine can't run it. */
|
|
86
|
+
launch(input: LaunchInput): LaunchOutput;
|
|
87
|
+
/** A new run that continues the last one; null when there is nothing to resume. */
|
|
88
|
+
resume?(input: ResumeInput): Omit<LaunchOutput, "resumeToken"> | null;
|
|
89
|
+
/** Where permission prompts surface (Q-AG-2). */
|
|
90
|
+
approvals?: "terminal" | "hook";
|
|
91
|
+
/**
|
|
92
|
+
* Caller variables this runtime needs beyond the allowlist (PATH, HOME,
|
|
93
|
+
* USER, LOGNAME, SHELL, TERM, COLORTERM, LANG, LC_*, TZ, TMPDIR). Named
|
|
94
|
+
* one by one; Claude Code's session plumbing never passes.
|
|
95
|
+
*/
|
|
96
|
+
envPassthrough?: readonly string[];
|
|
97
|
+
/**
|
|
98
|
+
* Names this runtime owns that must never reach a session Den starts
|
|
99
|
+
* (an exact name, or a prefix ending in `*`): den-claude's CLAUDECODE and
|
|
100
|
+
* CLAUDE_CODE_*. Den removes them after building any session's env,
|
|
101
|
+
* whatever put them there (sessions/env.ts).
|
|
102
|
+
*/
|
|
103
|
+
scrubEnv: readonly string[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A runtime cannot launch this agent here: a refusal, not a crash. */
|
|
107
|
+
export class LaunchError extends Error {
|
|
108
|
+
constructor(message: string) {
|
|
109
|
+
super(message);
|
|
110
|
+
this.name = "LaunchError";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Register a runtime as strategy kind `runtime` (doctor lists it). */
|
|
115
|
+
export function registerRuntime(rt: Runtime): void {
|
|
116
|
+
const entry: StrategyEntry<typeof RUNTIME_KIND, Runtime> = {
|
|
117
|
+
id: rt.id,
|
|
118
|
+
kind: RUNTIME_KIND,
|
|
119
|
+
detect(env) {
|
|
120
|
+
const d = rt.detect(env);
|
|
121
|
+
if (!d.available)
|
|
122
|
+
return { available: false, reason: d.why ?? "unavailable" };
|
|
123
|
+
return d.version
|
|
124
|
+
? { available: true, reason: `version ${d.version}` }
|
|
125
|
+
: { available: true };
|
|
126
|
+
},
|
|
127
|
+
capabilities: rt,
|
|
128
|
+
};
|
|
129
|
+
registerStrategy(entry);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getRuntime(id: string): Runtime | undefined {
|
|
133
|
+
return getStrategy<typeof RUNTIME_KIND, Runtime>(RUNTIME_KIND, id)
|
|
134
|
+
?.capabilities;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function runtimeIds(): string[] {
|
|
138
|
+
return strategyIds(RUNTIME_KIND);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Every registered runtime's scrubEnv: applied to every session Den starts. */
|
|
142
|
+
export function allScrubEnv(): string[] {
|
|
143
|
+
return [
|
|
144
|
+
...new Set(runtimeIds().flatMap((id) => getRuntime(id)?.scrubEnv ?? [])),
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The first executable `bin` on `env.PATH`, or null. Runs nothing. */
|
|
149
|
+
export function whichBin(
|
|
150
|
+
bin: string,
|
|
151
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
152
|
+
): string | null {
|
|
153
|
+
for (const dir of (env.PATH ?? "").split(path.delimiter)) {
|
|
154
|
+
if (!dir) continue;
|
|
155
|
+
const file = path.join(dir, bin);
|
|
156
|
+
try {
|
|
157
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
158
|
+
if (fs.statSync(file).isFile()) return file;
|
|
159
|
+
} catch {
|
|
160
|
+
/* not here */
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* A standard MCP config (`mcpServers`, the shape most MCP clients read)
|
|
168
|
+
* with Den's server as the only entry.
|
|
169
|
+
*/
|
|
170
|
+
export function mcpConfigJson(mcp: AgentMcp): string {
|
|
171
|
+
const [command, ...args] = mcp.command;
|
|
172
|
+
return (
|
|
173
|
+
JSON.stringify(
|
|
174
|
+
{
|
|
175
|
+
mcpServers: {
|
|
176
|
+
[mcp.name]: {
|
|
177
|
+
command,
|
|
178
|
+
args,
|
|
179
|
+
...(Object.keys(mcp.env).length ? { env: mcp.env } : {}),
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
null,
|
|
184
|
+
2,
|
|
185
|
+
) + "\n"
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Where a runtime puts an agent's MCP config. */
|
|
190
|
+
export function mcpConfigPath(input: Pick<LaunchInput, "filesDir">): string {
|
|
191
|
+
return path.join(input.filesDir, "mcp.json");
|
|
192
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Den's built-in runtimes, registered as strategy kind `runtime`: import
|
|
3
|
+
* this module to have them. den-claude first: it is the default.
|
|
4
|
+
*/
|
|
5
|
+
import { denClaude } from "./den-claude.js";
|
|
6
|
+
import { generic } from "./generic.js";
|
|
7
|
+
import { allScrubEnv, registerRuntime, type Runtime } from "./runtime.js";
|
|
8
|
+
import { AGENT_ACTOR_ENV, buildSessionEnv } from "../sessions/env.js";
|
|
9
|
+
|
|
10
|
+
registerRuntime(denClaude);
|
|
11
|
+
registerRuntime(generic);
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
allScrubEnv,
|
|
15
|
+
DEFAULT_RUNTIME_ID,
|
|
16
|
+
getRuntime,
|
|
17
|
+
LaunchError,
|
|
18
|
+
RUNTIME_KIND,
|
|
19
|
+
runtimeIds,
|
|
20
|
+
type AgentMcp,
|
|
21
|
+
type LaunchInput,
|
|
22
|
+
type LaunchOutput,
|
|
23
|
+
type ResumeInput,
|
|
24
|
+
type Runtime,
|
|
25
|
+
} from "./runtime.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* An agent's session env, as `agents start` builds it and `doctor --probe`
|
|
29
|
+
* checks it: the allowlist, the runtime's passthrough and launch env, Den's
|
|
30
|
+
* marker (DEN_AGENT=agent:<id>), then every runtime's scrubEnv.
|
|
31
|
+
*/
|
|
32
|
+
export function agentSessionEnv(
|
|
33
|
+
caller: NodeJS.ProcessEnv,
|
|
34
|
+
rt: Pick<Runtime, "envPassthrough">,
|
|
35
|
+
launch: Record<string, string>,
|
|
36
|
+
agentId: string,
|
|
37
|
+
): Record<string, string> {
|
|
38
|
+
return buildSessionEnv({
|
|
39
|
+
caller,
|
|
40
|
+
passthrough: rt.envPassthrough,
|
|
41
|
+
launch,
|
|
42
|
+
// The seam for a persona's or runtime's declared env: none yet.
|
|
43
|
+
extra: {},
|
|
44
|
+
den: { [AGENT_ACTOR_ENV]: `agent:${agentId}` },
|
|
45
|
+
scrub: allScrubEnv(),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -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
|
|
|
@@ -1,51 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Secrets-backed persistence for the local-dev sudo password.
|
|
3
3
|
*
|
|
4
|
-
* Goes through the configured secrets strategy (
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Goes through the configured secrets strategy (see src/shared/secrets),
|
|
5
|
+
* addressing the named ref `sudo`. There is no built-in location: a
|
|
6
|
+
* distribution that wants the saved password sets `secrets.refs.sudo` in its
|
|
7
|
+
* config. Without it, the vault path is "not configured" and only the
|
|
8
|
+
* interactive unlock is offered; nothing is spawned.
|
|
9
9
|
*
|
|
10
10
|
* All read helpers fail *soft*: if the backend isn't installed, the user isn't
|
|
11
|
-
* signed in, or the value is missing, they return
|
|
12
|
-
* fall back to the manual unlock
|
|
11
|
+
* signed in, the ref is not configured or the value is missing, they return
|
|
12
|
+
* null/false so the server can fall back to the manual unlock without
|
|
13
|
+
* surfacing CLI noise.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
|
-
import {
|
|
16
|
-
OP_API_KEYS_VAULT,
|
|
17
|
-
OP_DEV_KEYS_ITEM,
|
|
18
|
-
OP_DEN_SUDO_FIELD,
|
|
19
|
-
} from "../constants.js";
|
|
20
16
|
import {
|
|
21
17
|
getSecretsProvider,
|
|
22
18
|
namedSecretRef,
|
|
19
|
+
notConfiguredMessage,
|
|
20
|
+
readSecretsConfig,
|
|
21
|
+
secretsStatus,
|
|
22
|
+
SecretsNotConfiguredError,
|
|
23
23
|
type SecretRef,
|
|
24
|
+
type SecretsProviderName,
|
|
24
25
|
} from "../secrets/index.js";
|
|
26
|
+
import { product } from "../product.js";
|
|
25
27
|
|
|
26
|
-
/**
|
|
27
|
-
export const
|
|
28
|
-
vault: OP_API_KEYS_VAULT,
|
|
29
|
-
item: OP_DEV_KEYS_ITEM,
|
|
30
|
-
field: OP_DEN_SUDO_FIELD,
|
|
31
|
-
};
|
|
28
|
+
/** Name of the sudo ref under `secrets.refs`. */
|
|
29
|
+
export const SUDO_REF_NAME = "sudo";
|
|
32
30
|
|
|
33
|
-
/** The
|
|
34
|
-
function sudoRef(): SecretRef {
|
|
35
|
-
|
|
31
|
+
/** The configured sudo ref, or null when `secrets.refs.sudo` is not set. */
|
|
32
|
+
export function sudoRef(): SecretRef | null {
|
|
33
|
+
try {
|
|
34
|
+
return namedSecretRef(SUDO_REF_NAME, readSecretsConfig());
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
/** True when the
|
|
40
|
+
/** True when the sudo ref is configured AND its backend is ready. */
|
|
39
41
|
export async function isSecretsBackendReady(): Promise<boolean> {
|
|
42
|
+
if (!sudoRef()) return false;
|
|
40
43
|
return getSecretsProvider().isReady();
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
/** @deprecated use isSecretsBackendReady */
|
|
44
47
|
export const isOpSignedIn = isSecretsBackendReady;
|
|
45
48
|
|
|
46
|
-
/** Read the stored sudo password. Null when unavailable or empty. */
|
|
49
|
+
/** Read the stored sudo password. Null when unavailable, unconfigured or empty. */
|
|
47
50
|
export async function readSudoFromVault(): Promise<string | null> {
|
|
48
|
-
|
|
51
|
+
const ref = sudoRef();
|
|
52
|
+
if (!ref) return null;
|
|
53
|
+
return getSecretsProvider().tryGet(ref);
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
/** Whether a non-empty sudo password is stored (without returning it). */
|
|
@@ -55,15 +60,72 @@ export async function hasSudoInVault(): Promise<boolean> {
|
|
|
55
60
|
|
|
56
61
|
/** Save (or overwrite) the sudo password. Throws on backend errors. */
|
|
57
62
|
export async function saveSudoToVault(password: string): Promise<void> {
|
|
58
|
-
|
|
63
|
+
const ref = sudoRef();
|
|
64
|
+
if (!ref)
|
|
65
|
+
throw new SecretsNotConfiguredError(notConfiguredMessage(SUDO_REF_NAME));
|
|
66
|
+
await getSecretsProvider().set(ref, password);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** What the UI's unlock form may offer: `/api/sudo-password/vault-status`. */
|
|
70
|
+
export interface SudoVaultStatus {
|
|
71
|
+
/** The selected strategy id. */
|
|
72
|
+
backend: SecretsProviderName;
|
|
73
|
+
/** Name people see ("1Password"), for "Save to <label>". */
|
|
74
|
+
label: string;
|
|
75
|
+
/** `secrets.refs.sudo` is set for this backend. */
|
|
76
|
+
configured: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* The backend can work here, whether or not a sudo ref is configured
|
|
79
|
+
* (`configured` says that). With no ref: detect() only (a PATH check,
|
|
80
|
+
* nothing spawned), and only for an explicitly chosen provider; an unset
|
|
81
|
+
* provider is never probed (it is not needed). With a ref: detect() and
|
|
82
|
+
* signed in.
|
|
83
|
+
*/
|
|
84
|
+
backendAvailable: boolean;
|
|
85
|
+
/** Why it is not available (not configured, CLI not on PATH, signed out). */
|
|
86
|
+
reason?: string;
|
|
87
|
+
/** The distribution's org config file (e.g. config/acme.yml), for UI copy. */
|
|
88
|
+
configFile: string;
|
|
89
|
+
hasStored: boolean;
|
|
90
|
+
/** @deprecated alias of backendAvailable; removed in a later minor. */
|
|
91
|
+
opAvailable: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function sudoVaultStatus(): Promise<SudoVaultStatus> {
|
|
95
|
+
const cfg = readSecretsConfig();
|
|
96
|
+
const ref = sudoRef();
|
|
97
|
+
const pre = secretsStatus(cfg, process.env, { detect: false });
|
|
98
|
+
// Probe only what is needed or was chosen: a ref, or an explicit backend.
|
|
99
|
+
const probe = !!ref || (!pre.implicit && pre.backend !== "none");
|
|
100
|
+
const st = probe ? secretsStatus(cfg, process.env) : pre;
|
|
101
|
+
const out = (
|
|
102
|
+
backendAvailable: boolean,
|
|
103
|
+
hasStored: boolean,
|
|
104
|
+
reason?: string,
|
|
105
|
+
): SudoVaultStatus => ({
|
|
106
|
+
backend: st.backend,
|
|
107
|
+
label: st.label,
|
|
108
|
+
configured: !!ref,
|
|
109
|
+
backendAvailable,
|
|
110
|
+
...(reason ? { reason } : {}),
|
|
111
|
+
configFile: product().configFile,
|
|
112
|
+
hasStored,
|
|
113
|
+
opAvailable: backendAvailable,
|
|
114
|
+
});
|
|
115
|
+
if (!ref)
|
|
116
|
+
return out(
|
|
117
|
+
probe && st.detect.available,
|
|
118
|
+
false,
|
|
119
|
+
notConfiguredMessage(SUDO_REF_NAME),
|
|
120
|
+
);
|
|
121
|
+
if (!st.detect.available)
|
|
122
|
+
return out(false, false, `not available: ${st.detect.reason ?? ""}`.trim());
|
|
123
|
+
if (!(await getSecretsProvider().isReady()))
|
|
124
|
+
return out(false, false, `${st.label} is not signed in`);
|
|
125
|
+
return out(true, await hasSudoInVault());
|
|
59
126
|
}
|
|
60
127
|
|
|
61
128
|
/** Name of the active backend, for UI copy ("Save to Doppler"). */
|
|
62
129
|
export function secretsBackendLabel(): string {
|
|
63
|
-
|
|
64
|
-
return n === "1password"
|
|
65
|
-
? "1Password"
|
|
66
|
-
: n === "doppler"
|
|
67
|
-
? "Doppler"
|
|
68
|
-
: "environment";
|
|
130
|
+
return secretsStatus().label;
|
|
69
131
|
}
|
|
@@ -115,9 +115,13 @@ const localJwtProvider: TokenProvider = {
|
|
|
115
115
|
const l = cfg.auth?.localJwt;
|
|
116
116
|
if (!l) throw new Error(`auth.localJwt is not set for ${cfg.name}`);
|
|
117
117
|
const env = o.env ?? process.env;
|
|
118
|
+
// An explicit key the environment file names: not a default ref, so it
|
|
119
|
+
// resolves as it always has (env first, then the selected backend).
|
|
120
|
+
const fromEnv = env[l.secretKey];
|
|
121
|
+
const backend = fromEnv ? null : getSecretsProvider();
|
|
118
122
|
const secret =
|
|
119
|
-
|
|
120
|
-
(await
|
|
123
|
+
fromEnv ??
|
|
124
|
+
(await backend!.tryGet({
|
|
121
125
|
vault: "",
|
|
122
126
|
item: "",
|
|
123
127
|
field: l.secretKey,
|
|
@@ -125,7 +129,7 @@ const localJwtProvider: TokenProvider = {
|
|
|
125
129
|
}));
|
|
126
130
|
if (!secret) {
|
|
127
131
|
throw new Error(
|
|
128
|
-
`local-jwt signing secret ${l.secretKey} not found in env or the secrets backend`,
|
|
132
|
+
`local-jwt signing secret ${l.secretKey} not found in env or the ${backend?.name ?? "configured"} secrets backend (secrets.provider)`,
|
|
129
133
|
);
|
|
130
134
|
}
|
|
131
135
|
const ttl = l.ttlSeconds ?? 3600;
|
package/src/shared/brand.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* name: Acme Dev # shown in titles and the banner
|
|
9
9
|
* tagline: Acme local dev and E2E harness
|
|
10
10
|
* banner: [ "...", "..." ] # optional ASCII art lines
|
|
11
|
+
* theme: acme # the web UI's default theme (an id in
|
|
12
|
+
* # plugins/<id>/, or den)
|
|
11
13
|
*/
|
|
12
14
|
import * as fs from "fs";
|
|
13
15
|
import * as path from "path";
|
|
@@ -19,6 +21,8 @@ export interface BrandConfig {
|
|
|
19
21
|
name: string;
|
|
20
22
|
tagline: string;
|
|
21
23
|
banner?: string[];
|
|
24
|
+
/** The web UI's default theme id; the user's appearance.theme wins. */
|
|
25
|
+
theme?: string;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export function defaultBrand(): BrandConfig {
|
|
@@ -43,6 +47,9 @@ export function readBrand(root: string = getDistRoot()): BrandConfig {
|
|
|
43
47
|
name: raw.name?.trim() || fallback.name,
|
|
44
48
|
tagline: raw.tagline?.trim() || fallback.tagline,
|
|
45
49
|
banner: Array.isArray(raw.banner) ? raw.banner.map(String) : undefined,
|
|
50
|
+
...(typeof raw.theme === "string" && raw.theme.trim()
|
|
51
|
+
? { theme: raw.theme.trim() }
|
|
52
|
+
: {}),
|
|
46
53
|
};
|
|
47
54
|
return cached;
|
|
48
55
|
}
|
|
@@ -7,12 +7,36 @@
|
|
|
7
7
|
* The message names config keys (`orgs.initech.repos`), never values a
|
|
8
8
|
* secret could hide in.
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* One validation problem: the config file (as the distribution names it),
|
|
12
|
+
* the key path (`orgs.acme.sources[0].owner`) and what is wrong with it.
|
|
13
|
+
* `text` is the whole line after "<file>: ".
|
|
14
|
+
*/
|
|
15
|
+
export interface ConfigIssue {
|
|
16
|
+
file: string;
|
|
17
|
+
key: string;
|
|
18
|
+
message: string;
|
|
19
|
+
text: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
export class ConfigError extends Error {
|
|
11
23
|
readonly code = "config_invalid";
|
|
24
|
+
/** Every problem found (0.3.0-dev.40), in config order. */
|
|
25
|
+
readonly issues: ConfigIssue[];
|
|
12
26
|
|
|
13
|
-
|
|
14
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The message is one line per issue, "<file>: <text>". A plain string is
|
|
29
|
+
* one issue-less message.
|
|
30
|
+
*/
|
|
31
|
+
constructor(issues: ConfigIssue[] | string) {
|
|
32
|
+
const list = typeof issues === "string" ? [] : issues;
|
|
33
|
+
super(
|
|
34
|
+
typeof issues === "string"
|
|
35
|
+
? issues
|
|
36
|
+
: list.map((i) => `${i.file}: ${i.text}`).join("\n"),
|
|
37
|
+
);
|
|
15
38
|
this.name = "ConfigError";
|
|
39
|
+
this.issues = list;
|
|
16
40
|
}
|
|
17
41
|
}
|
|
18
42
|
|
|
@@ -28,6 +52,12 @@ export function configErrorBody(e: ConfigError): {
|
|
|
28
52
|
ok: false;
|
|
29
53
|
error: string;
|
|
30
54
|
code: "config_invalid";
|
|
55
|
+
issues: Array<{ file: string; key: string; message: string }>;
|
|
31
56
|
} {
|
|
32
|
-
return {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
error: e.message,
|
|
60
|
+
code: e.code,
|
|
61
|
+
issues: e.issues.map(({ file, key, message }) => ({ file, key, message })),
|
|
62
|
+
};
|
|
33
63
|
}
|
package/src/shared/constants.ts
CHANGED
|
@@ -5,17 +5,7 @@
|
|
|
5
5
|
* If it's specific to one module (e.g. a Playwright config timeout), keep it local.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
// ── 1Password strategy (only used when secrets.provider: 1password) ──────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Default 1Password account domain used by every `op` invocation across den.
|
|
13
|
-
* Kept in sync with the `OP_ACCOUNT ?= …` default in the Makefile. Scripts that
|
|
14
|
-
* may run outside `make` (setup-1password, auth-configure)
|
|
15
|
-
* set `process.env.OP_ACCOUNT ||= OP_ACCOUNT_DEFAULT` at entry so op picks up
|
|
16
|
-
* the right account even when invoked directly via `npx tsx`.
|
|
17
|
-
*/
|
|
18
|
-
export const OP_ACCOUNT_DEFAULT = "example.1password.com";
|
|
8
|
+
// ── Test accounts ─────────────────────────────────────────────────────────────
|
|
19
9
|
|
|
20
10
|
/**
|
|
21
11
|
* Default vault for shared test accounts. Deprecated: the vault is org data —
|
|
@@ -24,19 +14,8 @@ export const OP_ACCOUNT_DEFAULT = "example.1password.com";
|
|
|
24
14
|
*/
|
|
25
15
|
export const OP_TEST_VAULT = "test-accounts";
|
|
26
16
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/** Item within OP_API_KEYS_VAULT holding the developer's API keys. */
|
|
31
|
-
export const OP_DEV_KEYS_ITEM = "Developer API Keys";
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Field within OP_DEV_KEYS_ITEM where the user's local admin (sudo) password
|
|
35
|
-
* is optionally stored. Populated when the user checks "Save to 1Password" on
|
|
36
|
-
* the den local-dev unlock banner; read on server startup so subsequent
|
|
37
|
-
* sessions unlock without prompting.
|
|
38
|
-
*/
|
|
39
|
-
export const OP_DEN_SUDO_FIELD = `${envKey("LOCAL_ADMIN_PWD")}`;
|
|
17
|
+
// No 1Password account, vault, item or field is built in: a distribution
|
|
18
|
+
// names its own in `secrets` (src/shared/secrets).
|
|
40
19
|
|
|
41
20
|
// ── Timeouts ─────────────────────────────────────────────────────────────────
|
|
42
21
|
|