@arjunkhera/atlas 0.3.7 → 0.3.9

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 (67) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/artifact-renderer.md +22 -22
  3. package/agents/verifier.md +29 -0
  4. package/door/cli.mjs +62 -12
  5. package/door/kit-releases.json +4 -0
  6. package/door/lib/design-build.mjs +409 -0
  7. package/door/lib/design.mjs +199 -118
  8. package/door/lib/markdown.mjs +160 -0
  9. package/door/lib/proof.mjs +127 -0
  10. package/door/lib/tests.mjs +190 -0
  11. package/package.json +2 -1
  12. package/skills/lead/SKILL.md +4 -1
  13. package/skills/sdlc-task/SKILL.md +33 -24
  14. package/skills/sdlc-task/design/README.md +187 -0
  15. package/skills/sdlc-task/design/parts/actors.md +26 -0
  16. package/skills/sdlc-task/design/parts/alternatives.md +24 -0
  17. package/skills/sdlc-task/design/parts/build.md +23 -0
  18. package/skills/sdlc-task/design/parts/calls.md +25 -0
  19. package/skills/sdlc-task/design/parts/change.md +27 -0
  20. package/skills/sdlc-task/design/parts/data.md +22 -0
  21. package/skills/sdlc-task/design/parts/done.md +23 -0
  22. package/skills/sdlc-task/design/parts/edges.md +24 -0
  23. package/skills/sdlc-task/design/parts/goals.md +27 -0
  24. package/skills/sdlc-task/design/parts/key.md +25 -0
  25. package/skills/sdlc-task/design/parts/migration.md +22 -0
  26. package/skills/sdlc-task/design/parts/order.md +24 -0
  27. package/skills/sdlc-task/design/parts/problem.md +22 -0
  28. package/skills/sdlc-task/design/parts/proof.md +24 -0
  29. package/skills/sdlc-task/design/parts/proposal.md +24 -0
  30. package/skills/sdlc-task/design/parts/records.md +24 -0
  31. package/skills/sdlc-task/design/parts/repos.md +25 -0
  32. package/skills/sdlc-task/design/parts/risks.md +24 -0
  33. package/skills/sdlc-task/design/parts/rollout.md +24 -0
  34. package/skills/sdlc-task/design/parts/routes.md +24 -0
  35. package/skills/sdlc-task/design/parts/scorecard.md +25 -0
  36. package/skills/sdlc-task/design/parts/security.md +22 -0
  37. package/skills/sdlc-task/design/parts/shared-decisions.md +24 -0
  38. package/skills/sdlc-task/design/parts/states.md +25 -0
  39. package/skills/sdlc-task/design/parts/stories.md +26 -0
  40. package/skills/sdlc-task/design/parts/summary.md +33 -0
  41. package/skills/sdlc-task/design/parts/why.md +22 -0
  42. package/skills/sdlc-task/design/parts/words.md +29 -0
  43. package/skills/sdlc-task/design/parts/yardstick.md +25 -0
  44. package/skills/sdlc-task/design/parts.yaml +306 -0
  45. package/skills/sdlc-task/lifecycle.yaml +2 -2
  46. package/skills/tests/SKILL.md +234 -0
  47. package/tests/contract.mjs +398 -0
  48. package/tests/drivers/function.mjs +30 -0
  49. package/tests/drivers/http.mjs +68 -0
  50. package/tests/drivers/index.mjs +65 -0
  51. package/tests/drivers/mcp-stdio.mjs +174 -0
  52. package/tests/environment.mjs +227 -0
  53. package/tests/errors.mjs +27 -0
  54. package/tests/evidence.mjs +113 -0
  55. package/tests/fresh.mjs +42 -0
  56. package/tests/guards.mjs +159 -0
  57. package/tests/index.mjs +11 -0
  58. package/tests/link-check.mjs +576 -0
  59. package/tests/procs.mjs +43 -0
  60. package/tests/redact.mjs +58 -0
  61. package/tests/scenario.mjs +325 -0
  62. package/tests/stand-in.mjs +74 -0
  63. package/tests/tests-yaml.mjs +258 -0
  64. package/tests/wait.mjs +44 -0
  65. package/tests/yaml.mjs +327 -0
  66. package/agents/artifact-format/walkthrough.html +0 -706
  67. package/skills/sdlc-task/templates/design-doc.md +0 -126
@@ -0,0 +1,159 @@
1
+ // Guards: what a test may reach, and what a child process may see.
2
+ import { Blocked } from './errors.mjs';
3
+ import { randomBytes } from 'node:crypto';
4
+
5
+ export const LOCAL_HOSTS = Object.freeze(['127.0.0.1', 'localhost', '::1']);
6
+
7
+ const bare = (host) => String(host).toLowerCase().replace(/^\[|\]$/g, '');
8
+
9
+ // The hosts that a test may reach: `guards.hosts`, or this machine only.
10
+ export function hostsOf(tests) {
11
+ const hosts = tests?.guards?.hosts;
12
+ return Array.isArray(hosts) && hosts.length ? hosts.map(String) : [...LOCAL_HOSTS];
13
+ }
14
+
15
+ export function hostAllowed(host, hosts) {
16
+ const h = bare(host);
17
+ return hosts.some((entry) => {
18
+ const e = bare(entry);
19
+ return e === h || (e.startsWith('*.') && h.endsWith(e.slice(1)));
20
+ });
21
+ }
22
+
23
+ // Throws Blocked when the address is not on the list. The result is `blocked`,
24
+ // never `fail`.
25
+ export function requireAllowed(url, hosts, what = 'a request') {
26
+ let parsed;
27
+ try { parsed = new URL(url); } catch { throw new Blocked(`${what}: "${String(url).slice(0, 80)}" is not an address`); }
28
+ if (!hostAllowed(parsed.hostname, hosts)) {
29
+ throw new Blocked(`${what} to ${parsed.origin} was refused: guards.hosts allows only ${hosts.join(', ')}`);
30
+ }
31
+ return parsed;
32
+ }
33
+
34
+ export const sameOrigin = (a, b) => new URL(a).origin === new URL(b).origin;
35
+
36
+ // ---------------------------------------------------------------- child env
37
+
38
+ // A child process starts from an ALLOW-LIST, not from the parent environment
39
+ // minus a few names (design section 15, rule 1). It gets these names from the
40
+ // parent and nothing else: PATH, HOME, LANG, LC_*, TMPDIR, TERM and NODE_*
41
+ // (such as NODE_EXTRA_CA_CERTS). The rule follows packages/atlas/work/lib/child-env.mjs.
42
+ // The proxy variables pass only in a Claude cloud session, and only when the
43
+ // proxy is a loopback address. NODE_TEST_CONTEXT never passes.
44
+ const ALLOWED = /^(?:PATH|HOME|LANG|LC_[A-Z_]+|TMPDIR|TERM)$/;
45
+ const CLOUD_PROXY = ['HTTPS_PROXY', 'https_proxy'];
46
+ const CLOUD_NO_PROXY = ['NO_PROXY', 'no_proxy'];
47
+ const LOOPBACK_PROXY = /^http:\/\/(127\.0\.0\.1|localhost|\[::1\]):\d{1,5}\/?$/;
48
+
49
+ // The environment of a child process: the allow-list from `parent`, then
50
+ // `values` on top. A value in `values` replaces any parent value of the same
51
+ // name, so a parent value for a name that tests.yaml sets never reaches a child.
52
+ export function childEnv(parent = process.env, values = {}) {
53
+ const out = {};
54
+ for (const [name, value] of Object.entries(parent)) {
55
+ if (value === undefined) continue;
56
+ if (ALLOWED.test(name) || (name.startsWith('NODE_') && name !== 'NODE_TEST_CONTEXT')) out[name] = value;
57
+ }
58
+ if (parent.CLAUDE_CODE_REMOTE === 'true') {
59
+ const proxies = CLOUD_PROXY.filter((name) => parent[name] !== undefined);
60
+ if (proxies.length && proxies.every((name) => LOOPBACK_PROXY.test(parent[name]))) {
61
+ for (const name of [...proxies, ...CLOUD_NO_PROXY]) if (parent[name] !== undefined) out[name] = parent[name];
62
+ }
63
+ }
64
+ for (const [name, value] of Object.entries(values)) if (value !== undefined) out[name] = String(value);
65
+ return out;
66
+ }
67
+
68
+ // ---------------------------------------------------------------- addresses
69
+
70
+ const ADDRESS_NAME = /(?:URL|URI|HOST|ADDR|ADDRESS|ENDPOINT)/i;
71
+ const SCHEME_URL = /^[a-z][a-z0-9+.-]*:\/\/\S+/i;
72
+ const HOST_PORT = /^(\[[0-9a-f:.]+\]|[a-z0-9][a-z0-9.-]*):(\d{1,5})$/i;
73
+
74
+ // The host of one token, when the token is an address: a URL of any scheme
75
+ // (a file URL has no host and is no address), or host:port. Returns
76
+ // undefined when the token is not an address, and null when it is a URL that
77
+ // does not parse.
78
+ function hostOfToken(token) {
79
+ if (SCHEME_URL.test(token)) {
80
+ try { return new URL(token).hostname || undefined; } catch { return null; }
81
+ }
82
+ const m = HOST_PORT.exec(token);
83
+ if (!m) return undefined;
84
+ const host = m[1];
85
+ // "12:30" is a time, not an address: a host needs a letter, a dot or brackets.
86
+ if (!/[a-z.\[]/i.test(host)) return undefined;
87
+ return host;
88
+ }
89
+
90
+ // The addresses in an env map. A value is an address when it parses as a URL
91
+ // of any scheme or as host:port, whatever its name. A value may hold a list
92
+ // (split on commas, semicolons and white space). With `byName`, the value of
93
+ // a variable whose name says it is an address (URL, HOST, ...) is one too,
94
+ // even when it parses as nothing.
95
+ export function addressesIn(env = {}, { byName = true } = {}) {
96
+ const out = [];
97
+ for (const [key, value] of Object.entries(env)) {
98
+ const text = String(value ?? '').trim();
99
+ let found = false;
100
+ for (const token of text.split(/[\s,;]+/).filter(Boolean)) {
101
+ const host = hostOfToken(token);
102
+ if (host === undefined) continue;
103
+ found = true;
104
+ out.push({ key, value: token, host });
105
+ }
106
+ if (!found && byName && ADDRESS_NAME.test(key)) {
107
+ out.push({ key, value: text, host: text.replace(/:\d+$/, '').trim() || null });
108
+ }
109
+ }
110
+ return out;
111
+ }
112
+
113
+ // Every address in `env` must be on the list (`blocked` otherwise). Used for
114
+ // the vars of an environment and for the env of a way in.
115
+ export function requireAddressesAllowed(env, hosts, what) {
116
+ for (const one of addressesIn(env, { byName: false })) {
117
+ if (!one.host) throw new Blocked(`${what}: ${one.key} holds an address that cannot be read`);
118
+ if (!hostAllowed(one.host, hosts)) throw new Blocked(`${what}: ${one.key} points at ${one.host}, which guards.hosts does not allow (it allows only ${hosts.join(', ')})`);
119
+ }
120
+ }
121
+
122
+ // For a process that the library starts for a test, such as an MCP server:
123
+ // every address in the env that the library gives it must be on the list, and
124
+ // there must be one. A server with no address could fall back to a real one.
125
+ // `given` is what the way in sets. `full` is the whole env the server gets;
126
+ // the list check runs on all of it.
127
+ export function requireLocalAddresses(given, hosts, what, full = given) {
128
+ const own = addressesIn(given);
129
+ if (!own.length) throw new Blocked(`${what}: the env gives the server no address, so it could fall back to a real one; give it the local address`);
130
+ for (const one of addressesIn(full)) {
131
+ if (!one.host) throw new Blocked(`${what}: ${one.key} holds no address, so the server could fall back to a real one`);
132
+ if (!hostAllowed(one.host, hosts)) throw new Blocked(`${what}: ${one.key} points at ${one.host}, which guards.hosts does not allow`);
133
+ }
134
+ }
135
+
136
+ // ---------------------------------------------------------------- templates
137
+
138
+ // Replaces the placeholders of tests.yaml. Known forms:
139
+ // {run.secret} {run.id} {run.vars} {run.vars.NAME} {stand-in.NAME.url} {url} {port}
140
+ // `ctx` gives a function for each: secret(), id, varsFile, vars(name), standIn(name), url, port.
141
+ const PLACEHOLDER = /\{(run\.secret|run\.id|run\.vars(?:\.[A-Za-z0-9_]+)?|stand-in\.[A-Za-z0-9_-]+\.url|url|port)\}/g;
142
+
143
+ export function substitute(text, ctx) {
144
+ return String(text).replace(PLACEHOLDER, (whole, key) => {
145
+ let value;
146
+ if (key === 'run.secret') value = ctx.secret?.();
147
+ else if (key === 'run.id') value = ctx.id;
148
+ else if (key === 'run.vars') value = ctx.varsFile;
149
+ else if (key.startsWith('run.vars.')) value = ctx.vars?.(key.slice(9));
150
+ else if (key.startsWith('stand-in.')) value = ctx.standIn?.(key.slice(9, -4));
151
+ else if (key === 'url') value = ctx.url;
152
+ else if (key === 'port') value = ctx.port;
153
+ if (value === undefined || value === null || value === '') throw new Error(`no value for the placeholder ${whole}`);
154
+ return String(value);
155
+ });
156
+ }
157
+
158
+ // A fresh run secret: random, 40 characters.
159
+ export const newSecret = () => randomBytes(30).toString('base64url').slice(0, 40);
@@ -0,0 +1,11 @@
1
+ // The public face of the kit. A repo imports from here, or from the files directly.
2
+ export { scenario, defineAdapter, waitUntil, RUN_ID } from './contract.mjs';
3
+ export { startEnvironment, parseDotenv, provideSecrets } from './environment.mjs';
4
+ export { createStandIn, serveStandIn, CALLS_PATH, LIBRARY_HEADER } from './stand-in.mjs';
5
+ export { createHttpDriver, createMcpStdioDriver, createFunctionDriver, makeDriver } from './drivers/index.mjs';
6
+ export { parseTestsYaml, readTestsYaml, checkTestsYaml, guardParts, compareGuardParts, GUARD_PARTS, looksLikeSecret } from './tests-yaml.mjs';
7
+ export { parseScenario, readScenario, addFingerprints, FINGERPRINT_METHOD } from './scenario.mjs';
8
+ export { Blocked, Unavailable, WaitFailed, WaitTimeout, GateFailed } from './errors.mjs';
9
+ export { Redactor } from './redact.mjs';
10
+ export { Evidence, EVIDENCE_SCHEMA, RESULTS } from './evidence.mjs';
11
+ export { checkArea } from './link-check.mjs';