@monster-orange/den 0.0.0-stage → 0.1.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 +67 -0
- package/LICENSE +21 -0
- package/README.md +166 -2
- package/bin/den +61 -0
- package/docs/distributions.md +256 -0
- package/make/demo.mk +82 -0
- package/make/den.mk +80 -0
- package/make/journeys.mk +57 -0
- package/make/local_dev.mk +154 -0
- package/make/security.mk +59 -0
- package/make/test.mk +405 -0
- package/package.json +104 -4
- package/scripts/auth-configure.ts +415 -0
- package/scripts/heal-config.ts +92 -0
- package/scripts/jsonl-playwright-reporter.js +70 -0
- package/scripts/manifest-read.cjs +91 -0
- package/scripts/setup-1password.ts +570 -0
- package/scripts/setup-secrets.ts +149 -0
- package/scripts/view-results.js +156 -0
- package/service/install.sh +112 -0
- package/src/pillars/api/fixtures/index.ts +128 -0
- package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
- package/src/pillars/api/global-setup.ts +103 -0
- package/src/pillars/api/playwright.config.ts +105 -0
- package/src/pillars/api/schemas/example-api.json +110 -0
- package/src/pillars/api/tests/health/health.spec.ts +19 -0
- package/src/pillars/api/tests/health/manifest.yml +6 -0
- package/src/pillars/browser/fixtures/index.ts +232 -0
- package/src/pillars/browser/fixtures/package.json +10 -0
- package/src/pillars/browser/global-setup.ts +140 -0
- package/src/pillars/browser/global-teardown.ts +48 -0
- package/src/pillars/browser/playwright.config.ts +212 -0
- package/src/pillars/browser/setup.ts +219 -0
- package/src/pillars/journeys/app-config.ts +237 -0
- package/src/pillars/journeys/discover.ts +160 -0
- package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
- package/src/pillars/journeys/observability/events.ts +288 -0
- package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
- package/src/pillars/journeys/report/friction.ts +304 -0
- package/src/pillars/journeys/report/junit.ts +105 -0
- package/src/pillars/journeys/report/render.ts +273 -0
- package/src/pillars/journeys/report/results.ts +323 -0
- package/src/pillars/journeys/run.ts +673 -0
- package/src/pillars/journeys/runner.ts +1025 -0
- package/src/pillars/journeys/sandbox.ts +406 -0
- package/src/pillars/journeys/spec/loader.ts +152 -0
- package/src/pillars/journeys/spec/schema.ts +208 -0
- package/src/pillars/journeys/teardown.ts +98 -0
- package/src/pillars/journeys/tools/browser-session.ts +219 -0
- package/src/pillars/journeys/tools/browser-tool.ts +914 -0
- package/src/pillars/journeys/tools/http-tool.ts +564 -0
- package/src/pillars/journeys/tools/ledger.ts +278 -0
- package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
- package/src/pillars/journeys/verify/assertions.ts +502 -0
- package/src/pillars/journeys/verify/verdict.ts +71 -0
- package/src/pillars/perf/build-report.ts +36 -0
- package/src/pillars/perf/k6.config.js +112 -0
- package/src/pillars/perf/run.ts +345 -0
- package/src/pillars/perf/scenarios/load-profile.js +97 -0
- package/src/pillars/perf/staging.ts +73 -0
- package/src/pillars/security/discover.ts +108 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +729 -0
- package/src/pillars/smoke/admin-probe.ts +68 -0
- package/src/pillars/smoke/base-url.ts +56 -0
- package/src/pillars/smoke/cleanup.ts +8 -0
- package/src/pillars/smoke/discover.ts +237 -0
- package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
- package/src/pillars/smoke/fixture-manager.ts +217 -0
- package/src/pillars/smoke/fixtures.ts +468 -0
- package/src/pillars/smoke/global-setup-all.ts +262 -0
- package/src/pillars/smoke/global-setup-global.ts +262 -0
- package/src/pillars/smoke/global-setup.ts +273 -0
- package/src/pillars/smoke/global-teardown-all.ts +8 -0
- package/src/pillars/smoke/global-teardown-global.ts +8 -0
- package/src/pillars/smoke/global-teardown.ts +139 -0
- package/src/pillars/smoke/playwright-all.config.ts +147 -0
- package/src/pillars/smoke/playwright-global.config.ts +141 -0
- package/src/pillars/smoke/playwright.config.ts +162 -0
- package/src/pillars/smoke/setup-env.ts +132 -0
- package/src/pillars/smoke/setup-global.ts +126 -0
- package/src/pillars/smoke/setup.ts +115 -0
- package/src/pillars/smoke/smoke-repo.ts +16 -0
- package/src/pillars/smoke/staging.ts +49 -0
- package/src/pillars/suites/run.ts +147 -0
- package/src/scripts/configure-aws-profiles.ts +114 -0
- package/src/scripts/register-repos.ts +307 -0
- package/src/scripts/setup-default-workspace.ts +203 -0
- package/src/shared/applications.ts +138 -0
- package/src/shared/auth/api-token.ts +416 -0
- package/src/shared/auth/auth0-helper.ts +284 -0
- package/src/shared/auth/headless-login.ts +265 -0
- package/src/shared/auth/jwt-helper.ts +186 -0
- package/src/shared/auth/sudo-vault.ts +69 -0
- package/src/shared/auth/test-accounts.ts +98 -0
- package/src/shared/auth/token-injection.ts +184 -0
- package/src/shared/auth/token-providers/auth0.ts +80 -0
- package/src/shared/auth/token-providers/index.ts +182 -0
- package/src/shared/auth/token-refresh.ts +273 -0
- package/src/shared/banner.ts +15 -0
- package/src/shared/brand.ts +79 -0
- package/src/shared/cli-tabs.ts +131 -0
- package/src/shared/constants.ts +128 -0
- package/src/shared/den-config.ts +566 -0
- package/src/shared/dev-metrics/emit.ts +354 -0
- package/src/shared/edge/config.ts +229 -0
- package/src/shared/edge/render.ts +221 -0
- package/src/shared/edge/runtime.ts +268 -0
- package/src/shared/edge/types.ts +62 -0
- package/src/shared/engine.ts +67 -0
- package/src/shared/environment/aws-profiles.ts +49 -0
- package/src/shared/environment/env-selector.ts +161 -0
- package/src/shared/environment/playwright-env.ts +82 -0
- package/src/shared/environment-manager/catalog.ts +77 -0
- package/src/shared/environment-manager/clone-guard.ts +163 -0
- package/src/shared/environment-manager/dag.ts +286 -0
- package/src/shared/environment-manager/default-workspace.ts +182 -0
- package/src/shared/environment-manager/derivation/alias.ts +90 -0
- package/src/shared/environment-manager/derivation/cache.ts +97 -0
- package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
- package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
- package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
- package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
- package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
- package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
- package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
- package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
- package/src/shared/environment-manager/derivation/scan.ts +160 -0
- package/src/shared/environment-manager/derivation/text.ts +40 -0
- package/src/shared/environment-manager/derivation/types.ts +69 -0
- package/src/shared/environment-manager/health.ts +589 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +78 -0
- package/src/shared/environment-manager/log-tail.ts +212 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +334 -0
- package/src/shared/environment-manager/readiness.ts +93 -0
- package/src/shared/environment-manager/register-local.ts +72 -0
- package/src/shared/environment-manager/secrets-check.ts +260 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1588 -0
- package/src/shared/environment-manager/up-plan.ts +302 -0
- package/src/shared/environment-manager/workspace-manager.ts +905 -0
- package/src/shared/environment-manager/workspaces.ts +860 -0
- package/src/shared/manifest/deps.ts +27 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/read-manifest.ts +160 -0
- package/src/shared/manifest/schema.ts +299 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-manifest.ts +140 -0
- package/src/shared/manifest/workspace-manifests.ts +92 -0
- package/src/shared/open-url.ts +122 -0
- package/src/shared/org.ts +44 -0
- package/src/shared/paths.ts +178 -0
- package/src/shared/perf/optional-fields.ts +105 -0
- package/src/shared/perf/report-normalizer.ts +404 -0
- package/src/shared/perf/report-paths.ts +19 -0
- package/src/shared/perf/report-store.ts +110 -0
- package/src/shared/perf/report-types.ts +70 -0
- package/src/shared/perf/test-paths.ts +25 -0
- package/src/shared/process/log-rotation.ts +147 -0
- package/src/shared/process/spawn-streaming.ts +229 -0
- package/src/shared/product.ts +92 -0
- package/src/shared/profile.ts +141 -0
- package/src/shared/repo-kind.ts +32 -0
- package/src/shared/reporting/jsonl-reporter.ts +131 -0
- package/src/shared/reporting/slack-reporter.ts +340 -0
- package/src/shared/results/result-reader.ts +266 -0
- package/src/shared/results/result-writer.ts +168 -0
- package/src/shared/secrets/doppler.ts +126 -0
- package/src/shared/secrets/env.ts +48 -0
- package/src/shared/secrets/index.ts +197 -0
- package/src/shared/secrets/keys.ts +33 -0
- package/src/shared/secrets/onepassword.ts +75 -0
- package/src/shared/secrets/types.ts +52 -0
- package/src/shared/service-url.ts +119 -0
- package/src/shared/smoke/branch-resolver.ts +141 -0
- package/src/shared/smoke/git-remote.ts +70 -0
- package/src/shared/smoke/repo-cloner.ts +151 -0
- package/src/shared/stacks/config.ts +137 -0
- package/src/shared/stacks/manifest-commands.ts +88 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/types.ts +53 -0
- package/src/shared/terminal/tabs.ts +212 -0
- package/src/shared/test-discovery/module-scanner.ts +569 -0
- package/src/shared/types/index.ts +678 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +116 -0
- package/src/ui/cli/config-command.ts +68 -0
- package/src/ui/cli/create-main.ts +9 -0
- package/src/ui/cli/create.ts +360 -0
- package/src/ui/cli/edge-command.ts +91 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +524 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3656 -0
- package/src/ui/cli/init.ts +855 -0
- package/src/ui/cli/invocation-target.ts +175 -0
- package/src/ui/cli/logs-command.ts +104 -0
- package/src/ui/cli/module-selection.ts +31 -0
- package/src/ui/cli/prompts.ts +767 -0
- package/src/ui/cli/results-viewer.ts +436 -0
- package/src/ui/cli/secrets-preflight.ts +91 -0
- package/src/ui/cli/stack-command.ts +144 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1312 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/server/changelog-parser.ts +94 -0
- package/src/ui/server/daemon-reaper.ts +129 -0
- package/src/ui/server/editor-manager.ts +301 -0
- package/src/ui/server/github-service.ts +223 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3236 -0
- package/src/ui/server/log-history.ts +452 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +494 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/header.js +1371 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +7405 -0
- package/src/ui/server/public/logs.html +1489 -0
- package/src/ui/server/public/perf-report.js +359 -0
- package/src/ui/server/public/release.html +2111 -0
- package/src/ui/server/public/services.html +1098 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/release-deploy-session.ts +246 -0
- package/src/ui/server/release-service.ts +1125 -0
- package/src/ui/server/sudo-store.ts +111 -0
- package/src/ui/server/workspace-routes.ts +2154 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schema for *.journey.yml journey specs (journeys pillar).
|
|
3
|
+
*
|
|
4
|
+
* Design rules (plan: agent-e2e-journeys, packet jrn-spec-schema):
|
|
5
|
+
* - STRICT everywhere: unknown keys are rejected at every level, so typos
|
|
6
|
+
* and unsupported fields fail loudly at load time.
|
|
7
|
+
* - Budgets are fail-closed: max_turns, timeout_s, and max_total_tokens are
|
|
8
|
+
* all REQUIRED with sanity bounds only — no default values are ever
|
|
9
|
+
* injected; spec authors set every budget explicitly.
|
|
10
|
+
* - `model` is an optional spec field — the journey's declared default
|
|
11
|
+
* agent model. The JOURNEYS_MODEL env var, when set, overrides every
|
|
12
|
+
* spec's model (operator control for cost pinning / CI). A journey with
|
|
13
|
+
* neither source configured is refused at run time (fail-closed).
|
|
14
|
+
* - persona.role is limited to admin|user|beta: browser session minting via
|
|
15
|
+
* headlessLogin only supports those roles
|
|
16
|
+
* (src/shared/auth/headless-login.ts). root_admin is excluded until a
|
|
17
|
+
* browser login path exists for it.
|
|
18
|
+
* - success.assertions is the sole pass/fail authority: a discriminated
|
|
19
|
+
* union on `kind`, each carrying a deterministic API probe request
|
|
20
|
+
* ({method, path, body?} — POST probes with bodies are first-class).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
|
|
25
|
+
// ── Budgets ──────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
// HLD catalog rule: an individual journey must complete within 10 minutes.
|
|
28
|
+
export const MAX_TIMEOUT_S = 600;
|
|
29
|
+
|
|
30
|
+
const budgetsSchema = z
|
|
31
|
+
.strictObject({
|
|
32
|
+
/** Hard cap on agent turns; exceeded → run.cancel(). */
|
|
33
|
+
max_turns: z.int().positive(),
|
|
34
|
+
/** Wall-clock cap in seconds; bounded by the HLD's 10-minute catalog rule. */
|
|
35
|
+
timeout_s: z.int().positive().max(MAX_TIMEOUT_S),
|
|
36
|
+
/** Cumulative TokenUsage.totalTokens cap; exceeded → run.cancel(). */
|
|
37
|
+
max_total_tokens: z.int().positive(),
|
|
38
|
+
})
|
|
39
|
+
.describe(
|
|
40
|
+
"All budgets are required and fail-closed; no defaults are injected.",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// ── Persona / surface / environments ────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
// Roles supported by headlessLogin (browser session minting); root_admin
|
|
46
|
+
// intentionally excluded — no headless browser login path exists for it.
|
|
47
|
+
const personaRoleSchema = z.enum(["admin", "user", "beta"]);
|
|
48
|
+
|
|
49
|
+
const personaSchema = z.strictObject({
|
|
50
|
+
role: personaRoleSchema,
|
|
51
|
+
/** Natural-language brief the synthetic user acts on. */
|
|
52
|
+
brief: z.string().min(1),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const surfaceSchema = z.enum(["browser", "api", "mixed"]);
|
|
56
|
+
|
|
57
|
+
// ── HTTP capabilities (review finding R2-1) ─────────────────────────────────
|
|
58
|
+
|
|
59
|
+
// Only POST/PUT/PATCH are grantable: GET stays origin-wide on the scoped
|
|
60
|
+
// HTTP tool (reads are its purpose) and DELETE is harness-only (teardown).
|
|
61
|
+
const httpCapabilityMethodSchema = z.enum(["POST", "PUT", "PATCH"]);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* One journey-scoped mutating-capability grant for the agent's HTTP tool.
|
|
65
|
+
* `path_pattern` matches the full request path; `*` matches exactly one
|
|
66
|
+
* path segment (e.g. `/api/v1/items/{*}/notes`, written without the
|
|
67
|
+
* braces in real patterns).
|
|
68
|
+
*/
|
|
69
|
+
const httpCapabilitySchema = z.strictObject({
|
|
70
|
+
methods: z.array(httpCapabilityMethodSchema).min(1),
|
|
71
|
+
path_pattern: z.string().min(1),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Must match config/environments/*.json (env-selector's known set).
|
|
75
|
+
// The runner additionally hard-refuses production and prod regardless of spec content.
|
|
76
|
+
const environmentNameSchema = z.enum([
|
|
77
|
+
"local",
|
|
78
|
+
"dev",
|
|
79
|
+
"sandbox",
|
|
80
|
+
"staging",
|
|
81
|
+
"beta",
|
|
82
|
+
"prod",
|
|
83
|
+
"production",
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
// ── Assertion probe request ──────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
const probeMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Deterministic API probe executed by the verifier (harness-side,
|
|
92
|
+
* allowlist + JWT client). `{placeholders}` in `path` bind to
|
|
93
|
+
* ledger-extracted resource ids at verification time.
|
|
94
|
+
*/
|
|
95
|
+
const probeRequestSchema = z.strictObject({
|
|
96
|
+
method: probeMethodSchema,
|
|
97
|
+
path: z.string().min(1),
|
|
98
|
+
/** Optional JSON request body (e.g. search POST probes). */
|
|
99
|
+
body: z.record(z.string(), z.unknown()).optional(),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// JSON literal usable in equality / containment / match predicates.
|
|
103
|
+
const jsonLiteralSchema = z.union([
|
|
104
|
+
z.string(),
|
|
105
|
+
z.number(),
|
|
106
|
+
z.boolean(),
|
|
107
|
+
z.null(),
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
// ── Assertion discriminated union ────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
const httpStatusAssertion = z.strictObject({
|
|
113
|
+
kind: z.literal("http_status"),
|
|
114
|
+
request: probeRequestSchema,
|
|
115
|
+
/** Expected HTTP status code. */
|
|
116
|
+
equals: z.int().min(100).max(599),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const jsonPathExistsAssertion = z.strictObject({
|
|
120
|
+
kind: z.literal("json_path_exists"),
|
|
121
|
+
request: probeRequestSchema,
|
|
122
|
+
/** JSONPath that must resolve to a value. */
|
|
123
|
+
path: z.string().min(1),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const jsonPathEqualsAssertion = z.strictObject({
|
|
127
|
+
kind: z.literal("json_path_equals"),
|
|
128
|
+
request: probeRequestSchema,
|
|
129
|
+
path: z.string().min(1),
|
|
130
|
+
/** Literal the JSONPath value must equal. */
|
|
131
|
+
equals: jsonLiteralSchema,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const jsonPathContainsAssertion = z.strictObject({
|
|
135
|
+
kind: z.literal("json_path_contains"),
|
|
136
|
+
request: probeRequestSchema,
|
|
137
|
+
path: z.string().min(1),
|
|
138
|
+
/** String/array containment target at the JSONPath. */
|
|
139
|
+
contains: jsonLiteralSchema,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const arrayMinLengthAssertion = z.strictObject({
|
|
143
|
+
kind: z.literal("array_min_length"),
|
|
144
|
+
request: probeRequestSchema,
|
|
145
|
+
path: z.string().min(1),
|
|
146
|
+
/** Minimum number of items the array at the JSONPath must contain. */
|
|
147
|
+
min: z.int().positive(),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const arrayContainsMatchAssertion = z.strictObject({
|
|
151
|
+
kind: z.literal("array_contains_match"),
|
|
152
|
+
request: probeRequestSchema,
|
|
153
|
+
path: z.string().min(1),
|
|
154
|
+
/** Field predicates at least one array item must match. */
|
|
155
|
+
match: z
|
|
156
|
+
.record(z.string(), jsonLiteralSchema)
|
|
157
|
+
.refine((m) => Object.keys(m).length > 0, {
|
|
158
|
+
message: "match must declare at least one field predicate",
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
export const journeyAssertionSchema = z.discriminatedUnion("kind", [
|
|
163
|
+
httpStatusAssertion,
|
|
164
|
+
jsonPathExistsAssertion,
|
|
165
|
+
jsonPathEqualsAssertion,
|
|
166
|
+
jsonPathContainsAssertion,
|
|
167
|
+
arrayMinLengthAssertion,
|
|
168
|
+
arrayContainsMatchAssertion,
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
// ── Journey spec ─────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
const successSchema = z.strictObject({
|
|
174
|
+
/** Deterministic assertions — the sole pass/fail authority. Min 1. */
|
|
175
|
+
assertions: z.array(journeyAssertionSchema).min(1),
|
|
176
|
+
/** Optional advisory rubric lines for the (advisory-only) LLM judge. */
|
|
177
|
+
rubric: z.array(z.string()).optional(),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
export const journeySpecSchema = z.strictObject({
|
|
181
|
+
name: z.string().min(1),
|
|
182
|
+
persona: personaSchema,
|
|
183
|
+
surface: surfaceSchema,
|
|
184
|
+
/**
|
|
185
|
+
* Default agent model for this journey (e.g. "composer"). The
|
|
186
|
+
* JOURNEYS_MODEL env var overrides it when set; with neither configured
|
|
187
|
+
* the runner refuses the journey (fail-closed, no invented default).
|
|
188
|
+
*/
|
|
189
|
+
model: z.string().min(1).optional(),
|
|
190
|
+
/** Environments this journey may run against. The runner targets exactly one per invocation. */
|
|
191
|
+
environments: z.array(environmentNameSchema).min(1),
|
|
192
|
+
budgets: budgetsSchema,
|
|
193
|
+
/**
|
|
194
|
+
* Mutating grants for the scoped HTTP tool (api/mixed surfaces).
|
|
195
|
+
* OMITTED → the tool is read-only for mutations (safe default): every
|
|
196
|
+
* POST/PUT/PATCH is rejected before fetching. GET needs no grant.
|
|
197
|
+
*/
|
|
198
|
+
http_capabilities: z.array(httpCapabilitySchema).optional(),
|
|
199
|
+
success: successSchema,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
export type JourneySpec = z.infer<typeof journeySpecSchema>;
|
|
203
|
+
export type JourneyHttpCapability = z.infer<typeof httpCapabilitySchema>;
|
|
204
|
+
export type JourneyAssertion = z.infer<typeof journeyAssertionSchema>;
|
|
205
|
+
export type JourneyProbeRequest = z.infer<typeof probeRequestSchema>;
|
|
206
|
+
export type JourneyBudgets = z.infer<typeof budgetsSchema>;
|
|
207
|
+
export type JourneyPersonaRole = z.infer<typeof personaRoleSchema>;
|
|
208
|
+
export type JourneySurface = z.infer<typeof surfaceSchema>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journeys teardown executor — pure library over the run ledger (plan:
|
|
3
|
+
* jrn-runner packet, Approach §Isolation and teardown for Phase-1 pilots).
|
|
4
|
+
*
|
|
5
|
+
* INVOKED ONLY BY THE CALLER (the pilot entrypoint, jrn-pilot-journeys),
|
|
6
|
+
* strictly after runJourney → verify → report, in a finally block. Never
|
|
7
|
+
* called from inside runJourney.
|
|
8
|
+
*
|
|
9
|
+
* Deletes recognized ledger resources — from BOTH feeds (http-tool and
|
|
10
|
+
* browser network interception) — in REVERSE creation order, executing
|
|
11
|
+
* each entry's explicit cleanup descriptor through the caller-supplied
|
|
12
|
+
* allowlisted+authenticated HTTP client. Deletion failures and
|
|
13
|
+
* unmanaged-mutation warnings surface in the TeardownReport; nothing is
|
|
14
|
+
* swallowed, and unmanaged mutations are reported, never guessed-deleted.
|
|
15
|
+
*
|
|
16
|
+
* Run identity note: the runner injects the runId (`jrn-<journey>-<ts>-
|
|
17
|
+
* <rand4>`) into the persona brief as a required display-name prefix —
|
|
18
|
+
* best-effort labeling for humans; the ledger stays the teardown source
|
|
19
|
+
* of truth.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type {
|
|
23
|
+
CleanupRequest,
|
|
24
|
+
LedgerEntry,
|
|
25
|
+
RunLedger,
|
|
26
|
+
UnmanagedMutationWarning,
|
|
27
|
+
} from "./tools/ledger.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Caller-supplied HTTP client executing one cleanup request. The pilot
|
|
31
|
+
* entrypoint wires this to the allowlisted+JWT client so teardown carries
|
|
32
|
+
* the same auth and reach constraints as the journey itself.
|
|
33
|
+
*/
|
|
34
|
+
export type TeardownHttpClient = (
|
|
35
|
+
request: CleanupRequest,
|
|
36
|
+
) => Promise<{ status: number }>;
|
|
37
|
+
|
|
38
|
+
/** Outcome of one attempted deletion. */
|
|
39
|
+
export interface TeardownResultItem {
|
|
40
|
+
entry: LedgerEntry;
|
|
41
|
+
ok: boolean;
|
|
42
|
+
/** HTTP status when the request completed. */
|
|
43
|
+
status?: number;
|
|
44
|
+
/** Error message when the client threw (transport failure etc.). */
|
|
45
|
+
error?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TeardownReport {
|
|
49
|
+
/** Every attempted deletion, in execution (reverse-creation) order. */
|
|
50
|
+
results: TeardownResultItem[];
|
|
51
|
+
/** Subset of results that did not succeed — never swallowed. */
|
|
52
|
+
failures: TeardownResultItem[];
|
|
53
|
+
/** Unmanaged mutations observed during the run (reported, not deleted). */
|
|
54
|
+
unmanagedWarnings: readonly UnmanagedMutationWarning[];
|
|
55
|
+
/**
|
|
56
|
+
* Browser interception-processing failures — a creation MAY have gone
|
|
57
|
+
* unrecorded, so these surface as report warnings (finding 7).
|
|
58
|
+
*/
|
|
59
|
+
interceptionErrors: readonly string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Delete every recognized ledger resource in reverse creation order via
|
|
64
|
+
* its explicit cleanup descriptor. A failed deletion (non-2xx or thrown
|
|
65
|
+
* client error) is recorded and execution continues with the remaining
|
|
66
|
+
* entries — partial teardown is better than none, and the report carries
|
|
67
|
+
* the full picture.
|
|
68
|
+
*/
|
|
69
|
+
export async function teardownLedger(
|
|
70
|
+
ledger: RunLedger,
|
|
71
|
+
httpClient: TeardownHttpClient,
|
|
72
|
+
): Promise<TeardownReport> {
|
|
73
|
+
const results: TeardownResultItem[] = [];
|
|
74
|
+
|
|
75
|
+
for (const entry of [...ledger.entries].reverse()) {
|
|
76
|
+
try {
|
|
77
|
+
const response = await httpClient(entry.cleanup);
|
|
78
|
+
results.push({
|
|
79
|
+
entry,
|
|
80
|
+
ok: response.status >= 200 && response.status < 300,
|
|
81
|
+
status: response.status,
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
results.push({
|
|
85
|
+
entry,
|
|
86
|
+
ok: false,
|
|
87
|
+
error: error instanceof Error ? error.message : String(error),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
results,
|
|
94
|
+
failures: results.filter((result) => !result.ok),
|
|
95
|
+
unmanagedWarnings: ledger.warnings,
|
|
96
|
+
interceptionErrors: ledger.interceptionErrors,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journeys browser-session provisioning — mints a FRESH Playwright
|
|
3
|
+
* storageState at every run start via the existing den machinery
|
|
4
|
+
* (getCredentials → headlessLogin), exactly mirroring
|
|
5
|
+
* src/pillars/browser/global-setup.ts, which re-mints every run. There is
|
|
6
|
+
* deliberately NO staleness heuristic.
|
|
7
|
+
*
|
|
8
|
+
* Ordering note for the runner (jrn-runner packet): getCredentials reads
|
|
9
|
+
* E2E_* env vars in CI, so this MUST run BEFORE sanitizeEnvironment().
|
|
10
|
+
* The return value is ONLY the auth-file path — credential values never
|
|
11
|
+
* leave this function, and downstream code (browser toolset) consumes the
|
|
12
|
+
* path alone.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from "fs";
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
import { getDistRoot } from "../../../shared/paths.js";
|
|
18
|
+
import {
|
|
19
|
+
getCredentials,
|
|
20
|
+
headlessLogin,
|
|
21
|
+
} from "../../../shared/auth/headless-login.js";
|
|
22
|
+
import { envSelector } from "../../../shared/environment/env-selector.js";
|
|
23
|
+
import {
|
|
24
|
+
resolveAuthPath,
|
|
25
|
+
type Role,
|
|
26
|
+
} from "../../../shared/environment/playwright-env.js";
|
|
27
|
+
import type { Environment } from "../../../shared/types/index.js";
|
|
28
|
+
import type { JourneysSessionTokenConfig } from "../app-config.js";
|
|
29
|
+
|
|
30
|
+
import { product } from "../../../shared/product.js";
|
|
31
|
+
/**
|
|
32
|
+
* Mint a fresh authenticated browser session for `role` against `env`'s
|
|
33
|
+
* web surface and return the storageState auth-file path (and nothing
|
|
34
|
+
* else). Throws a loud, actionable error when credentials cannot be
|
|
35
|
+
* sourced — naming both the 1Password path (local) and the E2E_* env-var
|
|
36
|
+
* path (CI), mirroring browser/global-setup.ts.
|
|
37
|
+
*/
|
|
38
|
+
export async function provisionBrowserSession(
|
|
39
|
+
role: Role,
|
|
40
|
+
env: Environment,
|
|
41
|
+
): Promise<string> {
|
|
42
|
+
const envConfig = await envSelector.loadEnvironment(env);
|
|
43
|
+
const webBaseUrl = envConfig.baseUrls.web;
|
|
44
|
+
const authFile = resolveAuthPath(role, env, getDistRoot());
|
|
45
|
+
|
|
46
|
+
const credentials = await getCredentials(role, env);
|
|
47
|
+
if (!credentials) {
|
|
48
|
+
const localHint =
|
|
49
|
+
role === "admin"
|
|
50
|
+
? ` Local: make auth-configure env=${env} (handles 1Password sign-in automatically)\n`
|
|
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
|
+
throw new Error(
|
|
53
|
+
`No credentials for ${role}.\n` +
|
|
54
|
+
localHint +
|
|
55
|
+
` CI: set E2E_${role.toUpperCase()}_EMAIL / E2E_${role.toUpperCase()}_PASSWORD`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fs.mkdirSync(path.dirname(authFile), { recursive: true });
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await headlessLogin(
|
|
63
|
+
role,
|
|
64
|
+
webBaseUrl,
|
|
65
|
+
credentials.email,
|
|
66
|
+
credentials.password,
|
|
67
|
+
authFile,
|
|
68
|
+
envConfig.auth?.browser,
|
|
69
|
+
);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// Re-throw with a credential-free message: headlessLogin failures can
|
|
72
|
+
// carry page/URL context but must never leak email or password values
|
|
73
|
+
// into transcripts or reports.
|
|
74
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
75
|
+
const scrubbed = raw
|
|
76
|
+
.split(credentials.password)
|
|
77
|
+
.join("[REDACTED]")
|
|
78
|
+
.split(credentials.email)
|
|
79
|
+
.join("[REDACTED]");
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Browser session mint failed for ${role} against ${webBaseUrl}: ${scrubbed}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return authFile;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Bearer + extra headers for API-assertion probes, from a browser session. */
|
|
89
|
+
export interface SessionApiToken {
|
|
90
|
+
/** Bearer token for the app's API. */
|
|
91
|
+
apiToken: string;
|
|
92
|
+
/**
|
|
93
|
+
* Extra headers every API call carries (`session_token.headers`: header
|
|
94
|
+
* name -> response field), e.g. the org or workspace the session resolves
|
|
95
|
+
* to. Not secrets; empty when none are declared.
|
|
96
|
+
*/
|
|
97
|
+
headers: Record<string, string>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Injectable HTTP layer for {@link deriveApiTokenFromSession} (unit tests). */
|
|
101
|
+
export interface DeriveApiTokenDeps {
|
|
102
|
+
/**
|
|
103
|
+
* GET `tokenPath` on the web origin with the saved session cookies. The
|
|
104
|
+
* default drives a short-lived Playwright request context seeded from the
|
|
105
|
+
* storageState auth file, so HttpOnly session cookies are replayed exactly
|
|
106
|
+
* as a browser would.
|
|
107
|
+
*/
|
|
108
|
+
fetchAccessToken?: (
|
|
109
|
+
webBaseUrl: string,
|
|
110
|
+
authFilePath: string,
|
|
111
|
+
tokenPath: string,
|
|
112
|
+
) => Promise<{ status: number; body: unknown }>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* istanbul ignore next - Playwright request context tested via E2E */
|
|
116
|
+
async function defaultFetchAccessToken(
|
|
117
|
+
webBaseUrl: string,
|
|
118
|
+
authFilePath: string,
|
|
119
|
+
tokenPath: string,
|
|
120
|
+
): Promise<{ status: number; body: unknown }> {
|
|
121
|
+
const { request } = await import("playwright");
|
|
122
|
+
const context = await request.newContext({
|
|
123
|
+
baseURL: webBaseUrl,
|
|
124
|
+
storageState: authFilePath,
|
|
125
|
+
});
|
|
126
|
+
try {
|
|
127
|
+
const response = await context.get(tokenPath, {
|
|
128
|
+
timeout: 15000,
|
|
129
|
+
});
|
|
130
|
+
let body: unknown;
|
|
131
|
+
try {
|
|
132
|
+
body = await response.json();
|
|
133
|
+
} catch {
|
|
134
|
+
body = await response.text();
|
|
135
|
+
}
|
|
136
|
+
return { status: response.status(), body };
|
|
137
|
+
} finally {
|
|
138
|
+
await context.dispose();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Derive an API Bearer token (and any declared scoping headers) from a
|
|
144
|
+
* minted browser session — no password grant. Calls the app's own
|
|
145
|
+
* session-token route (`journeys.app.session_token.path` in the manifest)
|
|
146
|
+
* on the web origin with the session cookies from `authFilePath`: the token
|
|
147
|
+
* comes from the same real-user session the agent navigates with.
|
|
148
|
+
*
|
|
149
|
+
* Fails loudly (never returns a partial/absent token) so a missing token can
|
|
150
|
+
* never silently degrade a deterministic API assertion into a false pass.
|
|
151
|
+
*/
|
|
152
|
+
export async function deriveApiTokenFromSession(
|
|
153
|
+
authFilePath: string,
|
|
154
|
+
env: Environment,
|
|
155
|
+
sessionToken: JourneysSessionTokenConfig | undefined,
|
|
156
|
+
deps: DeriveApiTokenDeps = {},
|
|
157
|
+
): Promise<SessionApiToken> {
|
|
158
|
+
if (sessionToken === undefined) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
"No journeys.app.session_token in the spec-hosting repo's manifest: " +
|
|
161
|
+
"the journeys pillar needs the app's route that returns an API " +
|
|
162
|
+
"token for a browser session (path, token_field, headers) to run " +
|
|
163
|
+
"deterministic API assertions.",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const fetchAccessToken = deps.fetchAccessToken ?? defaultFetchAccessToken;
|
|
167
|
+
const envConfig = await envSelector.loadEnvironment(env);
|
|
168
|
+
const webBaseUrl = envConfig.baseUrls.web;
|
|
169
|
+
const tokenPath = sessionToken.path;
|
|
170
|
+
const tokenField = sessionToken.token_field ?? "access_token";
|
|
171
|
+
|
|
172
|
+
const { status, body } = await fetchAccessToken(
|
|
173
|
+
webBaseUrl,
|
|
174
|
+
authFilePath,
|
|
175
|
+
tokenPath,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (status !== 200) {
|
|
179
|
+
const detail =
|
|
180
|
+
status === 401
|
|
181
|
+
? "session not authenticated (storageState has no valid session cookie)"
|
|
182
|
+
: status === 403
|
|
183
|
+
? "session is authenticated but has no API access"
|
|
184
|
+
: `unexpected status ${status}`;
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Failed to derive an API token from the browser session against ` +
|
|
187
|
+
`${webBaseUrl}${tokenPath}: ${detail}. Deterministic API ` +
|
|
188
|
+
`assertions require a real-user token minted from the session.`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const parsed =
|
|
193
|
+
typeof body === "object" && body !== null
|
|
194
|
+
? (body as Record<string, unknown>)
|
|
195
|
+
: {};
|
|
196
|
+
const field = (name: string): string => {
|
|
197
|
+
const v = parsed[name];
|
|
198
|
+
return typeof v === "string" ? v.trim() : "";
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const apiToken = field(tokenField);
|
|
202
|
+
const headers: Record<string, string> = {};
|
|
203
|
+
const missing: string[] = apiToken.length === 0 ? [tokenField] : [];
|
|
204
|
+
for (const [header, name] of Object.entries(sessionToken.headers ?? {})) {
|
|
205
|
+
const value = field(name);
|
|
206
|
+
if (value.length === 0) missing.push(name);
|
|
207
|
+
else headers[header] = value;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (missing.length > 0) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`The browser session's ${tokenPath} response was missing ` +
|
|
213
|
+
`${missing.join(", ")}. Cannot run deterministic API assertions ` +
|
|
214
|
+
`without a real-user token.`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { apiToken, headers };
|
|
219
|
+
}
|