@prestack/thread 0.0.1
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 +192 -0
- package/dist/attach.d.ts +1 -0
- package/dist/attach.js +1 -0
- package/dist/core/actor.d.ts +12 -0
- package/dist/core/actor.js +8 -0
- package/dist/core/asking.d.ts +180 -0
- package/dist/core/asking.js +1109 -0
- package/dist/core/attach.d.ts +54 -0
- package/dist/core/attach.js +510 -0
- package/dist/core/chain.d.ts +16 -0
- package/dist/core/chain.js +114 -0
- package/dist/core/cli.d.ts +2 -0
- package/dist/core/cli.js +1682 -0
- package/dist/core/condition.d.ts +52 -0
- package/dist/core/condition.js +198 -0
- package/dist/core/descend.d.ts +81 -0
- package/dist/core/descend.js +879 -0
- package/dist/core/finding.d.ts +12 -0
- package/dist/core/finding.js +23 -0
- package/dist/core/generators/meta.d.ts +2 -0
- package/dist/core/generators/meta.js +823 -0
- package/dist/core/init.d.ts +15 -0
- package/dist/core/init.js +134 -0
- package/dist/core/invoked.d.ts +9 -0
- package/dist/core/invoked.js +41 -0
- package/dist/core/round.d.ts +1 -0
- package/dist/core/round.js +57 -0
- package/dist/core/rows.d.ts +26 -0
- package/dist/core/rows.js +354 -0
- package/dist/core/scope.d.ts +6 -0
- package/dist/core/scope.js +12 -0
- package/dist/core/thread.d.ts +26 -0
- package/dist/core/thread.js +5 -0
- package/dist/core/tree.d.ts +12 -0
- package/dist/core/tree.js +56 -0
- package/dist/core/types.d.ts +49 -0
- package/dist/core/types.js +311 -0
- package/dist/derivation/attach.d.ts +2 -0
- package/dist/derivation/attach.js +61 -0
- package/dist/derivation/generators/build.d.ts +16 -0
- package/dist/derivation/generators/build.js +217 -0
- package/dist/derivation/generators/check.d.ts +18 -0
- package/dist/derivation/generators/check.js +78 -0
- package/dist/derivation/generators/derivation.d.ts +3 -0
- package/dist/derivation/generators/derivation.js +273 -0
- package/dist/derivation/generators/determinism.d.ts +20 -0
- package/dist/derivation/generators/determinism.js +110 -0
- package/dist/derivation/generators/release.d.ts +3 -0
- package/dist/derivation/generators/release.js +183 -0
- package/dist/derivation/makers/release.d.ts +1 -0
- package/dist/derivation/makers/release.js +107 -0
- package/dist/derivation/release.d.ts +10 -0
- package/dist/derivation/release.js +12 -0
- package/dist/integrations/attach.d.ts +6 -0
- package/dist/integrations/attach.js +177 -0
- package/dist/integrations/generators/read.d.ts +1 -0
- package/dist/integrations/generators/read.js +92 -0
- package/dist/integrations/generators/sdk.d.ts +1 -0
- package/dist/integrations/generators/sdk.js +148 -0
- package/dist/integrations/generators/secret.d.ts +1 -0
- package/dist/integrations/generators/secret.js +233 -0
- package/dist/integrations/generators/target.d.ts +1 -0
- package/dist/integrations/generators/target.js +765 -0
- package/dist/integrations/generators/vault.d.ts +1 -0
- package/dist/integrations/generators/vault.js +164 -0
- package/dist/integrations/sdk.d.ts +6 -0
- package/dist/integrations/sdk.js +151 -0
- package/dist/integrations/secret.d.ts +16 -0
- package/dist/integrations/secret.js +30 -0
- package/dist/integrations/target.d.ts +49 -0
- package/dist/integrations/target.js +225 -0
- package/dist/integrations/vault.d.ts +12 -0
- package/dist/integrations/vault.js +104 -0
- package/dist/integrity/attach.d.ts +2 -0
- package/dist/integrity/attach.js +213 -0
- package/dist/integrity/check-threads.d.ts +10 -0
- package/dist/integrity/check-threads.js +153 -0
- package/dist/integrity/constraint.d.ts +16 -0
- package/dist/integrity/constraint.js +1105 -0
- package/dist/integrity/generators/compiler.d.ts +1 -0
- package/dist/integrity/generators/compiler.js +130 -0
- package/dist/integrity/generators/gate.d.ts +1 -0
- package/dist/integrity/generators/gate.js +172 -0
- package/dist/integrity/generators/issues.d.ts +1 -0
- package/dist/integrity/generators/issues.js +100 -0
- package/dist/integrity/generators/publish.d.ts +1 -0
- package/dist/integrity/generators/publish.js +186 -0
- package/dist/integrity/generators/rule.d.ts +2 -0
- package/dist/integrity/generators/rule.js +213 -0
- package/dist/integrity/generators/seal.d.ts +4 -0
- package/dist/integrity/generators/seal.js +101 -0
- package/dist/integrity/generators/trial.d.ts +39 -0
- package/dist/integrity/generators/trial.js +254 -0
- package/dist/integrity/lint.d.ts +50 -0
- package/dist/integrity/lint.js +161 -0
- package/dist/integrity/lsp/bin.d.ts +2 -0
- package/dist/integrity/lsp/bin.js +35 -0
- package/dist/integrity/lsp/diagnostics.d.ts +25 -0
- package/dist/integrity/lsp/diagnostics.js +57 -0
- package/dist/integrity/lsp/findings.d.ts +20 -0
- package/dist/integrity/lsp/findings.js +39 -0
- package/dist/integrity/lsp/lint.d.ts +7 -0
- package/dist/integrity/lsp/lint.js +95 -0
- package/dist/integrity/lsp/protocol.d.ts +20 -0
- package/dist/integrity/lsp/protocol.js +57 -0
- package/dist/integrity/lsp/server.d.ts +15 -0
- package/dist/integrity/lsp/server.js +171 -0
- package/dist/integrity/sealed.d.ts +1 -0
- package/dist/integrity/sealed.js +56 -0
- package/dist/integrity/slopnator.d.ts +90 -0
- package/dist/integrity/slopnator.js +267 -0
- package/dist/integrity/slopnators/sl_broken_lineage.d.ts +2 -0
- package/dist/integrity/slopnators/sl_broken_lineage.js +35 -0
- package/dist/integrity/slopnators/sl_declared_elsewhere.d.ts +2 -0
- package/dist/integrity/slopnators/sl_declared_elsewhere.js +33 -0
- package/dist/integrity/slopnators/sl_derived_drift.d.ts +8 -0
- package/dist/integrity/slopnators/sl_derived_drift.js +91 -0
- package/dist/integrity/slopnators/sl_hardcoded_style.d.ts +2 -0
- package/dist/integrity/slopnators/sl_hardcoded_style.js +46 -0
- package/dist/integrity/slopnators/sl_inert_island.d.ts +2 -0
- package/dist/integrity/slopnators/sl_inert_island.js +85 -0
- package/dist/integrity/slopnators/sl_inert_slopnator.d.ts +2 -0
- package/dist/integrity/slopnators/sl_inert_slopnator.js +76 -0
- package/dist/integrity/slopnators/sl_missing_row.d.ts +2 -0
- package/dist/integrity/slopnators/sl_missing_row.js +180 -0
- package/dist/integrity/slopnators/sl_no_barrel.d.ts +2 -0
- package/dist/integrity/slopnators/sl_no_barrel.js +41 -0
- package/dist/integrity/slopnators/sl_no_child_process.d.ts +2 -0
- package/dist/integrity/slopnators/sl_no_child_process.js +61 -0
- package/dist/integrity/slopnators/sl_no_comment.d.ts +2 -0
- package/dist/integrity/slopnators/sl_no_comment.js +41 -0
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.d.ts +2 -0
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.js +301 -0
- package/dist/integrity/slopnators/sl_no_import_cycle.d.ts +2 -0
- package/dist/integrity/slopnators/sl_no_import_cycle.js +42 -0
- package/dist/integrity/slopnators/sl_no_such_command.d.ts +2 -0
- package/dist/integrity/slopnators/sl_no_such_command.js +55 -0
- package/dist/integrity/slopnators/sl_only_takes_a_literal.d.ts +2 -0
- package/dist/integrity/slopnators/sl_only_takes_a_literal.js +59 -0
- package/dist/integrity/slopnators/sl_secret_literal.d.ts +2 -0
- package/dist/integrity/slopnators/sl_secret_literal.js +89 -0
- package/dist/integrity/slopnators/sl_stale_path.d.ts +2 -0
- package/dist/integrity/slopnators/sl_stale_path.js +92 -0
- package/dist/integrity/slopnators/sl_twice_meant.d.ts +2 -0
- package/dist/integrity/slopnators/sl_twice_meant.js +30 -0
- package/dist/integrity/slopnators/sl_twice_parented.d.ts +2 -0
- package/dist/integrity/slopnators/sl_twice_parented.js +34 -0
- package/dist/integrity/slopnators/sl_undeclared_crossing.d.ts +2 -0
- package/dist/integrity/slopnators/sl_undeclared_crossing.js +78 -0
- package/dist/integrity/slopnators/sl_undeclared_destination.d.ts +2 -0
- package/dist/integrity/slopnators/sl_undeclared_destination.js +56 -0
- package/dist/integrity/slopnators/sl_undeclared_element.d.ts +2 -0
- package/dist/integrity/slopnators/sl_undeclared_element.js +34 -0
- package/dist/integrity/slopnators/sl_undeclared_node.d.ts +2 -0
- package/dist/integrity/slopnators/sl_undeclared_node.js +59 -0
- package/dist/integrity/slopnators/sl_undeclared_operation.d.ts +2 -0
- package/dist/integrity/slopnators/sl_undeclared_operation.js +120 -0
- package/dist/integrity/slopnators/sl_undeclared_variant.d.ts +2 -0
- package/dist/integrity/slopnators/sl_undeclared_variant.js +117 -0
- package/dist/integrity/slopnators/sl_undescribed.d.ts +2 -0
- package/dist/integrity/slopnators/sl_undescribed.js +54 -0
- package/dist/integrity/slopnators/sl_ungoverned_island.d.ts +2 -0
- package/dist/integrity/slopnators/sl_ungoverned_island.js +58 -0
- package/dist/integrity/slopnators/sl_unkept_rows.d.ts +2 -0
- package/dist/integrity/slopnators/sl_unkept_rows.js +104 -0
- package/dist/integrity/slopnators/sl_unnamed_island.d.ts +2 -0
- package/dist/integrity/slopnators/sl_unnamed_island.js +37 -0
- package/dist/integrity/slopnators/sl_unpaired_member.d.ts +2 -0
- package/dist/integrity/slopnators/sl_unpaired_member.js +114 -0
- package/dist/integrity/slopnators/sl_unpaired_slopnator.d.ts +2 -0
- package/dist/integrity/slopnators/sl_unpaired_slopnator.js +60 -0
- package/dist/integrity/slopnators/sl_unprefixed_member.d.ts +2 -0
- package/dist/integrity/slopnators/sl_unprefixed_member.js +126 -0
- package/dist/integrity/slopnators/sl_unseamed_reach.d.ts +2 -0
- package/dist/integrity/slopnators/sl_unseamed_reach.js +73 -0
- package/dist/integrity/slopnators/sl_unthreaded.d.ts +56 -0
- package/dist/integrity/slopnators/sl_unthreaded.js +352 -0
- package/dist/integrity/slopnators/sl_untranslated_key.d.ts +2 -0
- package/dist/integrity/slopnators/sl_untranslated_key.js +157 -0
- package/dist/integrity/slopnators/sl_unversioned.d.ts +2 -0
- package/dist/integrity/slopnators/sl_unversioned.js +31 -0
- package/dist/integrity/sweep.d.ts +24 -0
- package/dist/integrity/sweep.js +264 -0
- package/dist/knowledge/attach.d.ts +2 -0
- package/dist/knowledge/attach.js +339 -0
- package/dist/knowledge/generators/docsgen.d.ts +1 -0
- package/dist/knowledge/generators/docsgen.js +145 -0
- package/dist/knowledge/generators/glossary.d.ts +1 -0
- package/dist/knowledge/generators/glossary.js +56 -0
- package/dist/knowledge/generators/skills.d.ts +1 -0
- package/dist/knowledge/generators/skills.js +222 -0
- package/dist/knowledge/glossary.d.ts +6 -0
- package/dist/knowledge/glossary.js +1 -0
- package/dist/operations/access.d.ts +27 -0
- package/dist/operations/access.js +61 -0
- package/dist/operations/attach.d.ts +6 -0
- package/dist/operations/attach.js +414 -0
- package/dist/operations/capabilities.d.ts +37 -0
- package/dist/operations/capabilities.js +35 -0
- package/dist/operations/cost.d.ts +14 -0
- package/dist/operations/cost.js +37 -0
- package/dist/operations/dispatch.d.ts +14 -0
- package/dist/operations/dispatch.js +327 -0
- package/dist/operations/error-results.d.ts +4 -0
- package/dist/operations/error-results.js +25 -0
- package/dist/operations/errors/catalog.d.ts +58 -0
- package/dist/operations/errors/catalog.js +91 -0
- package/dist/operations/errors/classes.d.ts +73 -0
- package/dist/operations/errors/classes.js +22 -0
- package/dist/operations/errors/errors.d.ts +52 -0
- package/dist/operations/errors/errors.js +257 -0
- package/dist/operations/generators/capability.d.ts +5 -0
- package/dist/operations/generators/capability.js +603 -0
- package/dist/operations/generators/compile.d.ts +2 -0
- package/dist/operations/generators/compile.js +541 -0
- package/dist/operations/generators/grant.d.ts +1 -0
- package/dist/operations/generators/grant.js +267 -0
- package/dist/operations/generators/operation.d.ts +1 -0
- package/dist/operations/generators/operation.js +811 -0
- package/dist/operations/generators/permits.d.ts +1 -0
- package/dist/operations/generators/permits.js +196 -0
- package/dist/operations/generators/seed.d.ts +20 -0
- package/dist/operations/generators/seed.js +285 -0
- package/dist/operations/generators/trial.d.ts +53 -0
- package/dist/operations/generators/trial.js +376 -0
- package/dist/operations/http.d.ts +30 -0
- package/dist/operations/http.js +121 -0
- package/dist/operations/makers/capability.d.ts +1 -0
- package/dist/operations/makers/capability.js +117 -0
- package/dist/operations/makers/trial.d.ts +1 -0
- package/dist/operations/makers/trial.js +413 -0
- package/dist/operations/memory.d.ts +104 -0
- package/dist/operations/memory.js +365 -0
- package/dist/operations/metering.d.ts +55 -0
- package/dist/operations/metering.js +247 -0
- package/dist/operations/nodes.d.ts +35 -0
- package/dist/operations/nodes.js +15 -0
- package/dist/operations/operations.d.ts +18 -0
- package/dist/operations/operations.js +5 -0
- package/dist/operations/queue-policy.d.ts +3 -0
- package/dist/operations/queue-policy.js +8 -0
- package/dist/operations/result.d.ts +19 -0
- package/dist/operations/result.js +10 -0
- package/dist/operations/seams.d.ts +239 -0
- package/dist/operations/seams.js +45 -0
- package/dist/operations/store.d.ts +14 -0
- package/dist/operations/store.js +1 -0
- package/package.json +16 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, resolve } from 'node:path';
|
|
11
|
+
import { argv, stdout } from 'node:process';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import { ANONYMOUS } from "../../core/actor.js";
|
|
14
|
+
import { ROOT, TREE, descend } from "../../core/descend.js";
|
|
15
|
+
import { exit, invoked } from "../../core/invoked.js";
|
|
16
|
+
import { recordingHttp } from "../http.js";
|
|
17
|
+
import { inMemorySeams } from "../memory.js";
|
|
18
|
+
import { TRIALS_AT } from "../operations.js";
|
|
19
|
+
import { PERMITS_AT } from "../access.js";
|
|
20
|
+
import { operations, types } from "./seed.js";
|
|
21
|
+
export { TRIALS_AT };
|
|
22
|
+
const DISPATCH_AT = 'operations/_dispatch.ts';
|
|
23
|
+
const ENTITIES_AT = `${TREE}entities.ts`;
|
|
24
|
+
const ACCESS_AT = '_threads/access.ts';
|
|
25
|
+
const WHEN = 1_767_225_600_000;
|
|
26
|
+
const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
27
|
+
function stable(value) {
|
|
28
|
+
if (Array.isArray(value))
|
|
29
|
+
return value.map(stable);
|
|
30
|
+
if (value instanceof Map)
|
|
31
|
+
return stable(Object.fromEntries(value));
|
|
32
|
+
if (value instanceof Set)
|
|
33
|
+
return [...value].map(stable);
|
|
34
|
+
if (typeof value === 'object' && value !== null) {
|
|
35
|
+
const found = {};
|
|
36
|
+
for (const key of Object.keys(value).sort(compare)) {
|
|
37
|
+
found[key] = stable(value[key]);
|
|
38
|
+
}
|
|
39
|
+
return found;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === 'bigint')
|
|
42
|
+
return String(value);
|
|
43
|
+
if (typeof value === 'function')
|
|
44
|
+
return '[function]';
|
|
45
|
+
if (value === undefined)
|
|
46
|
+
return null;
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
const printed = (value) => `${JSON.stringify(stable(value), null, 2)}\n`;
|
|
50
|
+
const asking = (scenario) => ({
|
|
51
|
+
...(scenario.actor ?? ANONYMOUS),
|
|
52
|
+
run: 'trial',
|
|
53
|
+
replay: `trial_${scenario.id}`,
|
|
54
|
+
});
|
|
55
|
+
function seamsFor(one, scenario, stood, gate, known) {
|
|
56
|
+
const made = inMemorySeams({
|
|
57
|
+
secrets: stood,
|
|
58
|
+
...(scenario.views === undefined ? {} : { views: scenario.views }),
|
|
59
|
+
...(known === undefined ? {} : { entities: known.entities, keys: known.keys }),
|
|
60
|
+
});
|
|
61
|
+
const found = made.seams;
|
|
62
|
+
found['permits'] = gate ?? {
|
|
63
|
+
check() {
|
|
64
|
+
return Promise.resolve({ allow: true, because: 'this app derives no gate, so nothing here can refuse' });
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
found['clock'] = { now: () => WHEN };
|
|
68
|
+
let drawn = 0;
|
|
69
|
+
found['random'] = {
|
|
70
|
+
next: () => {
|
|
71
|
+
drawn += 1;
|
|
72
|
+
return drawn / 100;
|
|
73
|
+
},
|
|
74
|
+
uuid: () => {
|
|
75
|
+
drawn += 1;
|
|
76
|
+
return `00000000-0000-4000-8000-${String(drawn).padStart(12, '0')}`;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
const recorder = recordingHttp(scenario.answers ?? {});
|
|
80
|
+
found['http'] = recorder;
|
|
81
|
+
return { seams: made.seams, sink: made.sink, kept: () => made.store, calls: () => recorder.calls };
|
|
82
|
+
}
|
|
83
|
+
function entered(sink) {
|
|
84
|
+
return sink.logs.some((one) => {
|
|
85
|
+
const data = one.data;
|
|
86
|
+
return data !== undefined && 'observability' in data;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function denied(result) {
|
|
90
|
+
if (typeof result !== 'object' || result === null)
|
|
91
|
+
return false;
|
|
92
|
+
const held = result;
|
|
93
|
+
return held.ok === false && held.code === 'denied';
|
|
94
|
+
}
|
|
95
|
+
export async function runScenario(one, scenario, dispatch, stood = {}, gate, known) {
|
|
96
|
+
const made = seamsFor(one, scenario, stood, gate, known);
|
|
97
|
+
const actor = asking(scenario);
|
|
98
|
+
for (const seeded of scenario.rows ?? []) {
|
|
99
|
+
await made.seams.store.insert(actor, seeded.entity, seeded.row);
|
|
100
|
+
}
|
|
101
|
+
let result;
|
|
102
|
+
try {
|
|
103
|
+
result = await dispatch(one, scenario.input, actor, made.seams);
|
|
104
|
+
}
|
|
105
|
+
catch (thrown) {
|
|
106
|
+
result = { threw: thrown instanceof Error ? thrown.message : String(thrown) };
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
operation: one.id,
|
|
110
|
+
scenario: scenario.id,
|
|
111
|
+
refused: denied(result),
|
|
112
|
+
measured: entered(made.sink),
|
|
113
|
+
result,
|
|
114
|
+
kept: made.kept(),
|
|
115
|
+
observed: made.sink,
|
|
116
|
+
called: made.calls(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export async function scenariosFor(one, takes, granted = []) {
|
|
120
|
+
const from = resolve(ROOT, takes);
|
|
121
|
+
if (existsSync(from)) {
|
|
122
|
+
const made = (await import(__rewriteRelativeImportExtension(pathToFileURL(from).href)));
|
|
123
|
+
const found = made.SCENARIOS;
|
|
124
|
+
if (found !== undefined) {
|
|
125
|
+
if (found.length === 0) {
|
|
126
|
+
return `${takes} states an empty \`SCENARIOS\`, so nothing would be measured — remove it, or state one`;
|
|
127
|
+
}
|
|
128
|
+
const named = new Set(found.map((each) => each.id));
|
|
129
|
+
if (named.size !== found.length) {
|
|
130
|
+
return `${takes} states two scenarios with the same id, and a trial is named after the one it ran`;
|
|
131
|
+
}
|
|
132
|
+
return [...found].sort((a, b) => compare(a.id, b.id));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (granted.length > 0) {
|
|
136
|
+
return (`\`${one.id}\` is granted to ${granted.join(', ')}, and no scenario says who runs it. A caller is not ` +
|
|
137
|
+
`something to derive — a trial run as somebody the grant must allow proves nothing about access. State one ` +
|
|
138
|
+
`with \`thread run trial ${one.id}\`, which asks for the roles it arrives wearing, and export \`SCENARIOS\` ` +
|
|
139
|
+
`from ${takes}`);
|
|
140
|
+
}
|
|
141
|
+
const empty = one.input.safeParse({});
|
|
142
|
+
if (empty.success) {
|
|
143
|
+
return [{ id: 'empty', describes: `runs ${one.id} with no input, as whoever arrives having said nothing`, input: {} }];
|
|
144
|
+
}
|
|
145
|
+
return (`\`${one.id}\` takes an input that no empty object satisfies, and states no scenario to try it ` +
|
|
146
|
+
`with. Export \`SCENARIOS\` from ${takes} — until then this operation is not proven`);
|
|
147
|
+
}
|
|
148
|
+
export function compared(was, now) {
|
|
149
|
+
const before = new Map(was.map((one) => [`${one.operation}/${one.scenario}`, one]));
|
|
150
|
+
const outcomes = [];
|
|
151
|
+
for (const one of now) {
|
|
152
|
+
const id = `${one.operation}/${one.scenario}`;
|
|
153
|
+
const failures = [];
|
|
154
|
+
const older = before.get(id);
|
|
155
|
+
if (!one.refused && !one.measured) {
|
|
156
|
+
failures.push('the run never entered the operation and nothing denied it, so nothing about the operation was ' +
|
|
157
|
+
`measured and no refusal was proven either. What it answered was ${JSON.stringify(one.result)}`);
|
|
158
|
+
}
|
|
159
|
+
if (one.refused && one.measured) {
|
|
160
|
+
failures.push('this app\'s own gate denied the caller and the run REACHED THE OPERATION anyway — dispatch let a ' +
|
|
161
|
+
'denied request through to the compiled function, which is the gate not holding rather than a trial drifting');
|
|
162
|
+
}
|
|
163
|
+
if (older === undefined) {
|
|
164
|
+
failures.push('no trial holds this scenario. Run `thread trials` and commit the result');
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
for (const part of ['result', 'kept', 'observed']) {
|
|
168
|
+
const then = printed(older[part]);
|
|
169
|
+
const said = printed(one[part]);
|
|
170
|
+
if (then === said)
|
|
171
|
+
continue;
|
|
172
|
+
failures.push(`${part} is not what was recorded — the operation's behaviour has changed.\n was: ${then.trim().slice(0, 400)}\n now: ${said.trim().slice(0, 400)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
outcomes.push({
|
|
176
|
+
id,
|
|
177
|
+
operation: one.operation,
|
|
178
|
+
passed: failures.length === 0,
|
|
179
|
+
refused: one.refused,
|
|
180
|
+
measured: one.measured,
|
|
181
|
+
failures,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
for (const one of was) {
|
|
185
|
+
const id = `${one.operation}/${one.scenario}`;
|
|
186
|
+
if (now.some((each) => `${each.operation}/${each.scenario}` === id))
|
|
187
|
+
continue;
|
|
188
|
+
outcomes.push({
|
|
189
|
+
id,
|
|
190
|
+
operation: one.operation,
|
|
191
|
+
passed: false,
|
|
192
|
+
refused: one.refused,
|
|
193
|
+
measured: false,
|
|
194
|
+
failures: ['this scenario is recorded and no longer runs — an operation or a scenario was removed without the trials being regenerated'],
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
outcomes,
|
|
199
|
+
passed: outcomes.filter((one) => one.passed).length,
|
|
200
|
+
failed: outcomes.filter((one) => !one.passed).length,
|
|
201
|
+
measured: outcomes.filter((one) => one.measured).length,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
export function unproven(run) {
|
|
205
|
+
const each = new Map();
|
|
206
|
+
for (const one of run.outcomes) {
|
|
207
|
+
const held = each.get(one.operation);
|
|
208
|
+
if (held === undefined)
|
|
209
|
+
each.set(one.operation, [one]);
|
|
210
|
+
else
|
|
211
|
+
held.push(one);
|
|
212
|
+
}
|
|
213
|
+
const said = [];
|
|
214
|
+
for (const [operation, outcomes] of [...each].sort((one, two) => compare(one[0], two[0]))) {
|
|
215
|
+
if (outcomes.some((one) => one.measured))
|
|
216
|
+
continue;
|
|
217
|
+
const denied = outcomes.filter((one) => one.refused).length;
|
|
218
|
+
const neither = outcomes.length - denied;
|
|
219
|
+
const how = [
|
|
220
|
+
denied === 0
|
|
221
|
+
? ''
|
|
222
|
+
: `${String(denied)} were denied by this app's own gate, which proves the gate turns somebody away and nothing whatever about the operation`,
|
|
223
|
+
neither === 0 ? '' : `${String(neither)} neither reached it nor were denied`,
|
|
224
|
+
].filter((one) => one !== '');
|
|
225
|
+
said.push(`\`${operation}\` is UNPROVEN — ${String(outcomes.length)} scenario(s) ran and not one of them reached it: ${how.join(', and ')}. ` +
|
|
226
|
+
'Grant a caller the trial can run as, or give it a scenario the gate admits');
|
|
227
|
+
}
|
|
228
|
+
return said;
|
|
229
|
+
}
|
|
230
|
+
async function standing() {
|
|
231
|
+
const at = resolve(ROOT, '_threads/secrets.ts');
|
|
232
|
+
if (!existsSync(at))
|
|
233
|
+
return {};
|
|
234
|
+
let held;
|
|
235
|
+
try {
|
|
236
|
+
const taken = (await import(__rewriteRelativeImportExtension(pathToFileURL(at).href)));
|
|
237
|
+
held = taken.SECRETS ?? [];
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return {};
|
|
241
|
+
}
|
|
242
|
+
const found = {};
|
|
243
|
+
for (const one of held) {
|
|
244
|
+
found[one.id] = one.sends === undefined ? `https://${one.id.replace(/_/g, '-')}.invalid` : `${one.id}-stood-in-for`;
|
|
245
|
+
}
|
|
246
|
+
return found;
|
|
247
|
+
}
|
|
248
|
+
export async function record() {
|
|
249
|
+
const pass = descend();
|
|
250
|
+
const known = types(pass);
|
|
251
|
+
const refusals = [];
|
|
252
|
+
const trials = [];
|
|
253
|
+
const declared = [...operations(pass, known)].filter(([, one]) => one.body !== undefined);
|
|
254
|
+
if (declared.length === 0)
|
|
255
|
+
return { trials: [], refusals: [] };
|
|
256
|
+
const stood = await standing();
|
|
257
|
+
const at = resolve(ROOT, DISPATCH_AT);
|
|
258
|
+
if (!existsSync(at)) {
|
|
259
|
+
return {
|
|
260
|
+
trials: [],
|
|
261
|
+
refusals: [
|
|
262
|
+
`this app has no ${DISPATCH_AT}. Run \`thread compile\` — an operation runs through dispatch and there is nothing here to run it with`,
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const made = (await import(__rewriteRelativeImportExtension(pathToFileURL(at).href)));
|
|
267
|
+
const dispatch = made.dispatch;
|
|
268
|
+
if (dispatch === undefined) {
|
|
269
|
+
return { trials: [], refusals: [`${DISPATCH_AT} exports no \`dispatch\`, so nothing here can run an operation`] };
|
|
270
|
+
}
|
|
271
|
+
const kept = resolve(ROOT, ENTITIES_AT);
|
|
272
|
+
const entities = new Map();
|
|
273
|
+
const keys = new Map();
|
|
274
|
+
if (existsSync(kept)) {
|
|
275
|
+
const sheet = (await import(__rewriteRelativeImportExtension(pathToFileURL(kept).href)));
|
|
276
|
+
for (const one of sheet.ENTITIES ?? []) {
|
|
277
|
+
entities.set(one.id, new Set([...(one.holds ?? []).map((each) => each.id), ...(one.links ?? []).map((each) => `${each.id}_id`)]));
|
|
278
|
+
const named = (one.must ?? []).flatMap((each) => (each.unique?.length === 1 ? [each.unique[0]] : []));
|
|
279
|
+
if (named.length === 1)
|
|
280
|
+
keys.set(one.id, named[0]);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const permitted = resolve(ROOT, ACCESS_AT);
|
|
284
|
+
const may = new Map();
|
|
285
|
+
if (existsSync(permitted)) {
|
|
286
|
+
const sheet = (await import(__rewriteRelativeImportExtension(pathToFileURL(permitted).href)));
|
|
287
|
+
for (const one of sheet.GRANTS ?? []) {
|
|
288
|
+
for (const id of one.may ?? []) {
|
|
289
|
+
may.set(id, [...new Set([...(may.get(id) ?? []), ...(one.to?.roles ?? [])])]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const compiled = resolve(ROOT, PERMITS_AT);
|
|
294
|
+
const permits = existsSync(compiled)
|
|
295
|
+
? (await import(__rewriteRelativeImportExtension(pathToFileURL(compiled).href))).permits
|
|
296
|
+
: undefined;
|
|
297
|
+
if (existsSync(compiled) && permits === undefined) {
|
|
298
|
+
return {
|
|
299
|
+
trials: [],
|
|
300
|
+
refusals: [`${PERMITS_AT} exports no \`permits\`, so a trial cannot be answered by the gate this app derives`],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const gate = permits === undefined
|
|
304
|
+
? undefined
|
|
305
|
+
: { check: (actor, action, resource) => Promise.resolve(permits(actor, action, resource)) };
|
|
306
|
+
for (const [id, one] of [...declared].sort((a, b) => compare(a[0], b[0]))) {
|
|
307
|
+
if (one.body === undefined)
|
|
308
|
+
continue;
|
|
309
|
+
const from = resolve(ROOT, one.body);
|
|
310
|
+
if (!existsSync(from)) {
|
|
311
|
+
refusals.push(`\`${id}\` derives ${one.body} and that file is not there. Run \`thread compile\``);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const held = (await import(__rewriteRelativeImportExtension(pathToFileURL(from).href)));
|
|
315
|
+
const operation = held.operation;
|
|
316
|
+
if (operation === undefined) {
|
|
317
|
+
refusals.push(`${one.body} exports no \`operation\`, so \`${id}\` cannot be run`);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const takes = one.takes[0]?.at;
|
|
321
|
+
if (takes === undefined) {
|
|
322
|
+
refusals.push(`\`${id}\` names nothing it takes, so nothing states its input or what to try it with`);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const scenarios = await scenariosFor(operation, takes, may.get(id) ?? []);
|
|
326
|
+
if (typeof scenarios === 'string') {
|
|
327
|
+
refusals.push(scenarios);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
for (const scenario of scenarios) {
|
|
331
|
+
trials.push(await runScenario(operation, scenario, dispatch, stood, gate, { entities, keys }));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return { trials, refusals };
|
|
335
|
+
}
|
|
336
|
+
function kept() {
|
|
337
|
+
const at = resolve(ROOT, TRIALS_AT);
|
|
338
|
+
if (!existsSync(at))
|
|
339
|
+
return [];
|
|
340
|
+
return JSON.parse(readFileSync(at, 'utf8'));
|
|
341
|
+
}
|
|
342
|
+
if (invoked(import.meta.url)) {
|
|
343
|
+
const { trials, refusals } = await record();
|
|
344
|
+
for (const one of refusals)
|
|
345
|
+
console.error(`trial: ${one}`);
|
|
346
|
+
if (argv.includes('--check')) {
|
|
347
|
+
const run = compared(kept(), trials);
|
|
348
|
+
for (const outcome of run.outcomes) {
|
|
349
|
+
if (outcome.passed)
|
|
350
|
+
continue;
|
|
351
|
+
for (const failure of outcome.failures)
|
|
352
|
+
console.error(`trial: ${outcome.id} — ${failure}`);
|
|
353
|
+
}
|
|
354
|
+
if (run.outcomes.length === 0 && refusals.length === 0) {
|
|
355
|
+
console.log('trial: this app declares no operation, so there is nothing to prove.');
|
|
356
|
+
exit(0);
|
|
357
|
+
}
|
|
358
|
+
const short = unproven(run);
|
|
359
|
+
for (const one of short)
|
|
360
|
+
console.error(`trial: ${one}`);
|
|
361
|
+
console.log(`trial: ${String(run.passed)} of ${String(run.outcomes.length)} passed, ${String(run.measured)} measured, ` +
|
|
362
|
+
`${String(short.length)} operation(s) unproven.`);
|
|
363
|
+
exit(run.failed === 0 && refusals.length === 0 && short.length === 0 ? 0 : 1);
|
|
364
|
+
}
|
|
365
|
+
if (refusals.length > 0) {
|
|
366
|
+
console.error(`trial: ${String(refusals.length)} refusal(s) above, so ${TRIALS_AT} is left as it was — ` +
|
|
367
|
+
'a run that was refused has nothing to record, and recording it would erase what does');
|
|
368
|
+
exit(1);
|
|
369
|
+
}
|
|
370
|
+
const at = resolve(ROOT, TRIALS_AT);
|
|
371
|
+
mkdirSync(dirname(at), { recursive: true });
|
|
372
|
+
writeFileSync(at, printed(trials));
|
|
373
|
+
const measured = trials.filter((one) => one.measured).length;
|
|
374
|
+
stdout.write(`trial: wrote ${TRIALS_AT} — ${String(trials.length)} trial(s) from ${String(measured)} run(s) that reached an operation.\n`);
|
|
375
|
+
exit(0);
|
|
376
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Http } from './seams.ts';
|
|
2
|
+
export declare const HTTP_CALL_ACTION = "http.call";
|
|
3
|
+
export declare class HttpNotConfiguredError extends Error {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
export declare class OutwardTransportError extends Error {
|
|
7
|
+
readonly target: string;
|
|
8
|
+
constructor(target: string, cause: unknown);
|
|
9
|
+
}
|
|
10
|
+
export declare function outwardUrl(target: string, address: string, path: string): URL;
|
|
11
|
+
export interface HttpSeamOptions {
|
|
12
|
+
readonly transport?: typeof fetch;
|
|
13
|
+
readonly timeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function httpSeam(options?: HttpSeamOptions): Http;
|
|
16
|
+
export interface RecordedCall {
|
|
17
|
+
readonly target: string;
|
|
18
|
+
readonly method: string;
|
|
19
|
+
readonly path: string;
|
|
20
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
21
|
+
readonly body: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ScriptedAnswer {
|
|
24
|
+
readonly status: number;
|
|
25
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
26
|
+
readonly body?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function recordingHttp(script?: Readonly<Record<string, ScriptedAnswer>>): Http & {
|
|
29
|
+
readonly calls: readonly RecordedCall[];
|
|
30
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { payloadBytes } from "./metering.js";
|
|
2
|
+
import { ValidationError } from "./result.js";
|
|
3
|
+
export const HTTP_CALL_ACTION = 'http.call';
|
|
4
|
+
export class HttpNotConfiguredError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super('no outward seam is configured. `http.call` needs `seams.http`, which is the transport an app ' +
|
|
7
|
+
'wires — which hosts it may reach is declared in its tree and is not this seam\'s business.');
|
|
8
|
+
this.name = 'HttpNotConfiguredError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class OutwardTransportError extends Error {
|
|
12
|
+
target;
|
|
13
|
+
constructor(target, cause) {
|
|
14
|
+
super(`outward call to "${target}" never reached it: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
15
|
+
this.target = target;
|
|
16
|
+
this.name = 'OutwardTransportError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function outwardUrl(target, address, path) {
|
|
20
|
+
let base;
|
|
21
|
+
try {
|
|
22
|
+
base = new URL(address);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new ValidationError(`the address resolved for "${target}" is not an absolute URL: ${JSON.stringify(address)}`);
|
|
26
|
+
}
|
|
27
|
+
if (base.protocol !== 'https:') {
|
|
28
|
+
throw new ValidationError(`outward calls go over https only, and "${target}" resolved to ${base.protocol.replace(':', '')}`);
|
|
29
|
+
}
|
|
30
|
+
if (base.search !== '' || base.hash !== '') {
|
|
31
|
+
throw new ValidationError(`the address for "${target}" carries a query or a fragment — those belong to the call`);
|
|
32
|
+
}
|
|
33
|
+
if (!path.startsWith('/')) {
|
|
34
|
+
throw new ValidationError(`an outward path starts with "/", and "${path}" does not`);
|
|
35
|
+
}
|
|
36
|
+
if (path.startsWith('//')) {
|
|
37
|
+
throw new ValidationError(`the path "${path}" is protocol-relative, which points at another host`);
|
|
38
|
+
}
|
|
39
|
+
const url = new URL(`${base.origin}${base.pathname.replace(/\/$/, '')}${path}`);
|
|
40
|
+
if (url.origin !== base.origin) {
|
|
41
|
+
throw new ValidationError(`the path "${path}" resolves to ${url.origin}, which is not where "${target}" is`);
|
|
42
|
+
}
|
|
43
|
+
return url;
|
|
44
|
+
}
|
|
45
|
+
function requestBytes(headers, body) {
|
|
46
|
+
return payloadBytes(headers) + (body === undefined ? 0 : body.byteLength);
|
|
47
|
+
}
|
|
48
|
+
function wireBody(body) {
|
|
49
|
+
const copy = new ArrayBuffer(body.byteLength);
|
|
50
|
+
new Uint8Array(copy).set(body);
|
|
51
|
+
return copy;
|
|
52
|
+
}
|
|
53
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
54
|
+
export function httpSeam(options = {}) {
|
|
55
|
+
const send = options.transport ?? fetch;
|
|
56
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
57
|
+
return {
|
|
58
|
+
async call(request) {
|
|
59
|
+
const url = outwardUrl(request.target, request.address, request.path);
|
|
60
|
+
const headers = { ...request.headers };
|
|
61
|
+
if (request.contentType !== undefined)
|
|
62
|
+
headers['content-type'] = request.contentType;
|
|
63
|
+
if (request.idempotencyKey !== undefined)
|
|
64
|
+
headers['idempotency-key'] = request.idempotencyKey;
|
|
65
|
+
const bytesSent = requestBytes(headers, request.body);
|
|
66
|
+
const startedAt = Date.now();
|
|
67
|
+
let response;
|
|
68
|
+
try {
|
|
69
|
+
response = await send(url.toString(), {
|
|
70
|
+
method: request.method,
|
|
71
|
+
headers,
|
|
72
|
+
...(request.body === undefined ? {} : { body: wireBody(request.body) }),
|
|
73
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
throw new OutwardTransportError(request.target, error);
|
|
78
|
+
}
|
|
79
|
+
const body = new Uint8Array(await response.arrayBuffer());
|
|
80
|
+
const responseHeaders = Object.fromEntries(response.headers.entries());
|
|
81
|
+
return {
|
|
82
|
+
status: response.status,
|
|
83
|
+
headers: responseHeaders,
|
|
84
|
+
body,
|
|
85
|
+
bytesSent,
|
|
86
|
+
bytesReceived: payloadBytes(responseHeaders) + body.byteLength,
|
|
87
|
+
durationMs: Date.now() - startedAt,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const REDACTED = '(sent)';
|
|
93
|
+
export function recordingHttp(script = {}) {
|
|
94
|
+
const calls = [];
|
|
95
|
+
const decoder = new TextDecoder();
|
|
96
|
+
return {
|
|
97
|
+
calls,
|
|
98
|
+
async call(request) {
|
|
99
|
+
outwardUrl(request.target, request.address, request.path);
|
|
100
|
+
const body = request.body === undefined ? '' : decoder.decode(request.body);
|
|
101
|
+
calls.push({
|
|
102
|
+
target: request.target,
|
|
103
|
+
method: request.method,
|
|
104
|
+
path: request.path,
|
|
105
|
+
headers: Object.fromEntries(Object.keys(request.headers ?? {}).map((one) => [one, REDACTED])),
|
|
106
|
+
body,
|
|
107
|
+
});
|
|
108
|
+
const answer = script[`${request.method} ${request.path}`] ?? { status: 404, body: '{"message":"not scripted"}' };
|
|
109
|
+
const answered = new TextEncoder().encode(answer.body ?? '');
|
|
110
|
+
const headers = answer.headers ?? {};
|
|
111
|
+
return {
|
|
112
|
+
status: answer.status,
|
|
113
|
+
headers,
|
|
114
|
+
body: answered,
|
|
115
|
+
bytesSent: requestBytes({ ...request.headers }, request.body),
|
|
116
|
+
bytesReceived: payloadBytes(headers) + answered.byteLength,
|
|
117
|
+
durationMs: 0,
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { argv, exit, stdout } from 'node:process';
|
|
5
|
+
import { RECEIPT, answer, asking, given, issue, only, refuse, spend, wordsIn } from "../../core/asking.js";
|
|
6
|
+
import { findTree } from "../../core/tree.js";
|
|
7
|
+
import { CAPABILITIES_AT, SURFACES, isSurface } from "../capabilities.js";
|
|
8
|
+
const TREE = findTree();
|
|
9
|
+
const at = resolve(TREE.at, CAPABILITIES_AT);
|
|
10
|
+
if (!existsSync(at)) {
|
|
11
|
+
refuse(`${CAPABILITIES_AT} is not there, so this app has nothing reachable to switch. Declare an operation with ` +
|
|
12
|
+
'`thread create operation` — it writes this table on its way out — or run `thread capabilities`.');
|
|
13
|
+
}
|
|
14
|
+
let table;
|
|
15
|
+
try {
|
|
16
|
+
table = JSON.parse(readFileSync(at, 'utf8'));
|
|
17
|
+
}
|
|
18
|
+
catch (thrown) {
|
|
19
|
+
refuse(`${CAPABILITIES_AT} could not be read: ${thrown instanceof Error ? thrown.message : String(thrown)}. ` +
|
|
20
|
+
'Run `thread capabilities` — it is derived, so it can always be written again.');
|
|
21
|
+
}
|
|
22
|
+
if (table.length === 0) {
|
|
23
|
+
refuse('this app declares no operation, so there is nothing to switch on or off yet.');
|
|
24
|
+
}
|
|
25
|
+
const mark = (one) => [
|
|
26
|
+
` ${one.operation} — ${one.describes}`,
|
|
27
|
+
` ${SURFACES.map((each) => `${one.live.includes(each) ? '[on] ' : '[off]'} ${each}`).join(' ')}`,
|
|
28
|
+
` ${one.browsable ? 'may be called from a page' : 'server only — it reaches a target that needs a server secret'}`,
|
|
29
|
+
];
|
|
30
|
+
const listing = [`${String(table.length)} operation(s):`, ...table.flatMap(mark)].join('\n');
|
|
31
|
+
const words = wordsIn(['edit']);
|
|
32
|
+
const verb = words[0];
|
|
33
|
+
stdout.write(`${listing}\n`);
|
|
34
|
+
const ASKS = [
|
|
35
|
+
{
|
|
36
|
+
key: 'live',
|
|
37
|
+
says: [
|
|
38
|
+
'which surfaces are on, comma-separated. Answering nothing switches every',
|
|
39
|
+
'one of them off, which leaves the operation declared, compiled and proven',
|
|
40
|
+
'and reachable by nobody — a real thing to want, and said out loud rather',
|
|
41
|
+
'than refused.',
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
const TAKES = ASKS.map((one) => one.key);
|
|
46
|
+
const ASKED = ASKS.flatMap((one) => one.says.map((line, at) => ` ${at === 0 ? `${one.key}:`.padEnd(8) : ' '.repeat(8)}${line}`));
|
|
47
|
+
const method = (nonce) => [
|
|
48
|
+
'',
|
|
49
|
+
'what this changes:',
|
|
50
|
+
'',
|
|
51
|
+
` thread create capability edit <operation> ${RECEIPT} ${nonce}`,
|
|
52
|
+
'',
|
|
53
|
+
` ${RECEIPT} is spent by one run and refused if the table moved after it was issued.`,
|
|
54
|
+
'',
|
|
55
|
+
`a surface is a way this app is CALLED, and there are ${String(SURFACES.length)}: ${SURFACES.join(', ')}.`,
|
|
56
|
+
'',
|
|
57
|
+
' cli a command a terminal runs',
|
|
58
|
+
' api a route a request arrives on, and the OpenAPI document written from it',
|
|
59
|
+
' mcp a tool an agent is offered. Switch this off and an AI actor cannot call the operation,',
|
|
60
|
+
' while everything else about it goes on working',
|
|
61
|
+
'',
|
|
62
|
+
'`edit` asks, one answer per line:',
|
|
63
|
+
'',
|
|
64
|
+
...ASKED,
|
|
65
|
+
'',
|
|
66
|
+
'It writes the flag and runs `thread capabilities`, which carries every flag across and rewrites',
|
|
67
|
+
'the tool list, the OpenAPI document and the client bundle from them.',
|
|
68
|
+
].join('\n');
|
|
69
|
+
const flagged = argv.indexOf(RECEIPT);
|
|
70
|
+
if (verb === undefined && flagged === -1) {
|
|
71
|
+
const nonce = issue(TREE.at, listing);
|
|
72
|
+
stdout.write(`${method(nonce)}\n`);
|
|
73
|
+
stdout.write(`\ncapability: receipt ${nonce} — spent on use, and stale the moment ${CAPABILITIES_AT} changes.\n`);
|
|
74
|
+
exit(0);
|
|
75
|
+
}
|
|
76
|
+
spend(TREE.at, listing, argv[flagged + 1], CAPABILITIES_AT);
|
|
77
|
+
if (verb !== 'edit') {
|
|
78
|
+
refuse(`there is nothing called "${verb ?? ''}" to do here — say edit. A capability is derived, so there is none to add`);
|
|
79
|
+
}
|
|
80
|
+
const which = words[1];
|
|
81
|
+
if (which === undefined) {
|
|
82
|
+
refuse('`edit` takes the name of an operation this app declares');
|
|
83
|
+
}
|
|
84
|
+
const one = table.find((each) => each.operation === which);
|
|
85
|
+
if (one === undefined) {
|
|
86
|
+
refuse(`\`${which}\` is not an operation this app declares. What is here is ${table.map((each) => each.operation).join(', ')}`);
|
|
87
|
+
}
|
|
88
|
+
const piped = given();
|
|
89
|
+
only(piped, TAKES, 'editing a capability');
|
|
90
|
+
const { ask, shut } = asking(piped);
|
|
91
|
+
const said = (piped.length > 0
|
|
92
|
+
? (answer(piped, 'live') ?? '')
|
|
93
|
+
: await ask('live', `which surfaces are on — ${SURFACES.join(', ')} (now: ${one.live.join(', ') || 'none'}): `)).trim();
|
|
94
|
+
shut();
|
|
95
|
+
const wanted = said
|
|
96
|
+
.split(',')
|
|
97
|
+
.map((each) => each.trim().toLowerCase())
|
|
98
|
+
.filter((each) => each !== '');
|
|
99
|
+
const wrong = wanted.filter((each) => !isSurface(each));
|
|
100
|
+
if (wrong.length > 0) {
|
|
101
|
+
refuse(`there is no surface called ${wrong.map((each) => `"${each}"`).join(', ')}. There are ${String(SURFACES.length)}: ` +
|
|
102
|
+
`${SURFACES.join(', ')}. Nothing was written.`);
|
|
103
|
+
}
|
|
104
|
+
const live = SURFACES.filter((each) => wanted.includes(each));
|
|
105
|
+
writeFileSync(at, `${JSON.stringify(table.map((each) => (each.operation === one.operation ? { ...each, live } : each)), null, 2)}\n`);
|
|
106
|
+
const off = SURFACES.filter((each) => !live.includes(each));
|
|
107
|
+
stdout.write(`capability: ${one.operation} is live on ${live.join(', ') || 'nothing'}` +
|
|
108
|
+
`${off.length === 0 ? '' : `, and off for ${off.join(', ')}`}.\n`);
|
|
109
|
+
for (const what of [['capabilities'], ['docs']]) {
|
|
110
|
+
const made = spawnSync('npx', ['thread', ...what], { cwd: TREE.at, encoding: 'utf8' });
|
|
111
|
+
stdout.write(`${`${made.stdout ?? ''}${made.stderr ?? ''}`.trim()}\n`);
|
|
112
|
+
if (made.status !== 0) {
|
|
113
|
+
stdout.write(`capability: the flag is written and stands; \`thread ${what.join(' ')}\` refused above.\n`);
|
|
114
|
+
exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
exit(0);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|