@mnstry/atelier 0.2.0-alpha.13 → 0.2.0-alpha.15
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 +283 -0
- package/README.md +102 -52
- package/SECURITY.md +1 -1
- package/TRADEMARKS.md +2 -2
- package/contracts/atelier-extension-manifest.v1.schema.json +412 -0
- package/contracts/atelier-graph-view.v1.schema.json +1085 -0
- package/contracts/public-api-baseline.json +298 -19
- package/docs/README.md +90 -0
- package/docs/blocks/capability-stewardship.md +1 -1
- package/docs/blocks/claims.md +2 -2
- package/docs/blocks/conformance-admission.md +1 -1
- package/docs/blocks/learning-harnesses.md +1 -1
- package/docs/{atelier-sync.md → guides/atelier-sync.md} +20 -14
- package/docs/{capability-stewardship.md → guides/capability-stewardship.md} +2 -4
- package/docs/guides/co-authoring.md +113 -0
- package/docs/{distributions.md → guides/distributions.md} +13 -8
- package/docs/guides/extensions.md +189 -0
- package/docs/guides/graph-refresh.md +152 -0
- package/docs/guides/graph-workspace.md +64 -0
- package/docs/{ingestion.md → guides/ingestion.md} +16 -15
- package/docs/{inquiry-harnesses.md → guides/inquiry-harnesses.md} +2 -3
- package/docs/{knowledge-workspace.md → guides/knowledge-workspace.md} +2 -2
- package/docs/{learning-harnesses.md → guides/learning-harnesses.md} +2 -3
- package/docs/{learning.md → guides/learning.md} +12 -5
- package/docs/{obsidian.md → guides/obsidian.md} +370 -23
- package/docs/guides/ontology-guide.md +111 -0
- package/docs/{personal-workspace.md → guides/personal-workspace.md} +17 -5
- package/docs/{research-starter.md → guides/research-starter.md} +1 -1
- package/docs/{tenant-readiness.md → guides/tenant-readiness.md} +39 -0
- package/docs/{upgrade.md → guides/upgrade.md} +197 -11
- package/docs/knowledge-health/assessment/capabilities.md +6 -15
- package/docs/knowledge-health/facilitator.md +15 -4
- package/docs/knowledge-health/{devday/participant-response.md → participant-response.md} +4 -2
- package/docs/knowledge-health/quickstart.md +97 -75
- package/docs/knowledge-health/readme.md +13 -5
- package/docs/maintainers/describing-the-atelier.md +173 -0
- package/docs/{release-engineering.md → maintainers/release-engineering.md} +5 -4
- package/docs/{announcements.md → reference/announcements.md} +1 -1
- package/docs/{architecture.md → reference/architecture.md} +31 -5
- package/docs/{atelier-runtime.md → reference/atelier-runtime.md} +1 -1
- package/docs/{connected-composition.md → reference/connected-composition.md} +7 -10
- package/docs/{coordination.md → reference/coordination.md} +1 -1
- package/docs/{decisions.md → reference/decisions.md} +3 -3
- package/docs/{design.md → reference/design.md} +1 -1
- package/docs/reference/edge-policy.md +151 -0
- package/docs/reference/graph-view-structured.md +119 -0
- package/docs/reference/graph-view.md +139 -0
- package/docs/reference/graph-workspace.md +37 -0
- package/docs/{intake-and-guides.md → reference/intake-and-guides.md} +3 -4
- package/docs/{knowledge-graph.md → reference/knowledge-graph.md} +120 -6
- package/docs/{local-services.md → reference/local-services.md} +43 -4
- package/docs/{obsidian-contract.md → reference/obsidian-contract.md} +29 -4
- package/docs/{obsidian-plugin.md → reference/obsidian-plugin.md} +218 -68
- package/docs/{practical-judgment.md → reference/phronesis.md} +12 -3
- package/docs/reference/portability.md +92 -0
- package/docs/{portable-presentation.md → reference/portable-presentation.md} +2 -2
- package/docs/{presentation-consumer-boundaries.md → reference/presentation-consumer-boundaries.md} +5 -0
- package/docs/{project-options.md → reference/project-options.md} +17 -0
- package/docs/{repo-boundary-guard.md → reference/repo-boundary-guard.md} +50 -4
- package/docs/{review-portability.md → reference/review-portability.md} +4 -0
- package/docs/{semantic-operation.md → reference/semantic-operation.md} +4 -3
- package/docs/{template-profile.md → reference/template-profile.md} +1 -1
- package/docs/{trackables.md → reference/trackables.md} +2 -2
- package/docs/start/independent-start.md +89 -0
- package/docs/{install.md → start/install.md} +9 -9
- package/docs/{knowledge-setup.md → start/knowledge-setup.md} +5 -5
- package/docs/{research-starter-quickstart.md → start/research-starter-quickstart.md} +6 -6
- package/fixtures/atelier-architecture/catalog/invalid/document.json +2 -2
- package/fixtures/atelier-architecture/catalog/valid/document.json +2 -2
- package/fixtures/graph-view/synthetic-authorized-input.v1.json +437 -0
- package/fixtures/graph-view/synthetic-view.v1.json +1 -0
- package/fixtures/knowledge-health/workshop/README.md +1 -1
- package/fixtures/templates/local-library.v1.json +3 -1
- package/package.json +22 -8
- package/plugins/obsidian/main.js +57 -8
- package/plugins/obsidian/manifest.json +1 -1
- package/skills/claude/atelier-guided-upgrade/SKILL.md +1 -1
- package/skills/claude/atelier-knowledge-setup/SKILL.md +2 -2
- package/skills/claude/atelier-local-service/SKILL.md +1 -1
- package/skills/claude/atelier-practical-judgment/SKILL.md +7 -2
- package/skills/claude/atelier-skill-steward/SKILL.md +1 -1
- package/skills/claude/mnstry-readiness/SKILL.md +2 -2
- package/skills/codex/atelier-guided-upgrade/SKILL.md +1 -1
- package/skills/codex/atelier-knowledge-setup/SKILL.md +2 -2
- package/skills/codex/atelier-local-service/SKILL.md +1 -1
- package/skills/codex/atelier-practical-judgment/SKILL.md +7 -2
- package/skills/codex/atelier-skill-steward/SKILL.md +1 -1
- package/skills/codex/mnstry-readiness/SKILL.md +2 -2
- package/src/architecture/catalog.mjs +2 -2
- package/src/attestation/sign.mjs +1 -1
- package/src/boundary/content-rules.mjs +534 -68
- package/src/boundary/policy.mjs +187 -60
- package/src/cli/run.mjs +26 -9
- package/src/commands/distribution.mjs +1 -1
- package/src/commands/extension.mjs +27 -0
- package/src/commands/ingest.mjs +1 -1
- package/src/commands/learn.mjs +1 -1
- package/src/commands/obsidian.mjs +49 -21
- package/src/commands/review.mjs +3 -0
- package/src/edge-policy/contract.mjs +69 -0
- package/src/edge-policy/policy.mjs +142 -0
- package/src/edge-policy/source-citation-adapter.mjs +43 -0
- package/src/extension-packs/loader.mjs +4 -3
- package/src/extensions/admission.mjs +83 -0
- package/src/extensions/client.d.mts +23 -0
- package/src/extensions/client.mjs +99 -0
- package/src/extensions/contracts.d.mts +29 -0
- package/src/extensions/contracts.mjs +163 -0
- package/src/extensions/convex.d.mts +24 -0
- package/src/extensions/convex.mjs +189 -0
- package/src/extensions/delegation.mjs +54 -0
- package/src/extensions/drizzle.d.mts +11 -0
- package/src/extensions/drizzle.mjs +33 -0
- package/src/extensions/postgres.d.mts +7 -0
- package/src/extensions/postgres.mjs +66 -0
- package/src/extensions/prisma.d.mts +13 -0
- package/src/extensions/prisma.mjs +63 -0
- package/src/extensions/project.mjs +432 -0
- package/src/extensions/scaffold-convex.mjs +130 -0
- package/src/extensions/scaffold-sql.mjs +144 -0
- package/src/extensions/server.d.mts +20 -0
- package/src/extensions/server.mjs +102 -0
- package/src/extensions/sql-scope.mjs +22 -0
- package/src/graph/graph.mjs +13 -5
- package/src/graph/knowledge-graph.mjs +20 -2
- package/src/graph/repo-contribution.mjs +405 -0
- package/src/graph/repo-contribution.types.mjs +32 -0
- package/src/graph-access/edge-decision-adapter.mjs +41 -0
- package/src/graph-access/edge-record-adapter.mjs +73 -0
- package/src/graph-access/index.mjs +253 -0
- package/src/graph-access/multi-contribution-record-adapter.mjs +101 -0
- package/src/graph-access/multi-record-projection-adapter.mjs +74 -0
- package/src/graph-access/source-adapter.mjs +81 -0
- package/src/graph-refresh/compare.mjs +110 -0
- package/src/graph-refresh/controller.mjs +324 -0
- package/src/graph-refresh/index.mjs +3 -0
- package/src/graph-refresh/local.mjs +78 -0
- package/src/graph-view/babylon/babylon-scene-port.mjs +64 -0
- package/src/graph-view/babylon/index.mjs +2 -0
- package/src/graph-view/babylon/navigator.mjs +312 -0
- package/src/graph-view/babylon/provenance.mjs +36 -0
- package/src/graph-view/babylon/scene-controller.mjs +131 -0
- package/src/graph-view/babylon/semantic-picking.mjs +35 -0
- package/src/graph-view/babylon/web-host.mjs +188 -0
- package/src/graph-view/cli.mjs +3 -0
- package/src/graph-view/command.mjs +34 -0
- package/src/graph-view/contracts.mjs +79 -0
- package/src/graph-view/errors.mjs +5 -0
- package/src/graph-view/index.mjs +4 -0
- package/src/graph-view/intents.mjs +30 -0
- package/src/graph-view/query.mjs +213 -0
- package/src/graph-view/refs.mjs +2 -0
- package/src/graph-view/structured/browser.mjs +262 -0
- package/src/graph-view/structured/styles.mjs +8 -0
- package/src/graph-workspace/authoring.mjs +185 -0
- package/src/graph-workspace/browser/constellation.mjs +369 -0
- package/src/graph-workspace/browser/dom.mjs +23 -0
- package/src/graph-workspace/browser/index.mjs +6 -0
- package/src/graph-workspace/browser/review.mjs +55 -0
- package/src/graph-workspace/browser/styles.mjs +11 -0
- package/src/graph-workspace/browser/workbench.mjs +351 -0
- package/src/graph-workspace/browser/workspace.mjs +431 -0
- package/src/graph-workspace/disclosure.mjs +39 -0
- package/src/graph-workspace/geometry.mjs +78 -0
- package/src/graph-workspace/index.mjs +11 -0
- package/src/graph-workspace/labels.mjs +50 -0
- package/src/graph-workspace/model.mjs +109 -0
- package/src/graph-workspace/recipes.mjs +88 -0
- package/src/graph-workspace/scale.mjs +95 -0
- package/src/graph-workspace/theme.mjs +156 -0
- package/src/ingestion/optional/graphify/graphify_supplier.py +5 -0
- package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
- package/src/ingestion/optional/graphify/native-pins.json +306 -0
- package/src/knowledge/ontology-guide.mjs +152 -0
- package/src/knowledge-health/proof/installed-check.mjs +2 -2
- package/src/knowledge-health/rule-identity.mjs +14 -9
- package/src/knowledge-health/workshop.mjs +22 -2
- package/src/personal-workspace/index.mjs +1 -1
- package/src/personal-workspace/selection.mjs +34 -3
- package/src/project/config.mjs +1 -1
- package/src/project/path-match.mjs +2 -1
- package/src/project/providers/github.mjs +24 -0
- package/src/project/repo-identity.mjs +3 -22
- package/src/projection/obsidian/contracts.mjs +1 -1
- package/src/projection/obsidian/materialize/path-registry.mjs +1 -1
- package/src/projection/obsidian/materialize/prepare-view.mjs +88 -6
- package/src/projection/obsidian/materialize/redaction.mjs +1 -1
- package/src/projection/obsidian/personal-workspace.mjs +84 -5
- package/src/projection/obsidian/plugin-bridge/channel.mjs +1 -1
- package/src/projection/obsidian/proposals/adapter.mjs +46 -23
- package/src/projection/obsidian/proposals/observation.mjs +40 -2
- package/src/projection/obsidian/publication/publisher.mjs +96 -8
- package/src/projection/project.mjs +21 -16
- package/src/readiness/readiness.mjs +20 -11
- package/src/readiness-protocols/bundled-pack.mjs +14 -766
- package/src/readiness-protocols/bundled-selection.mjs +32 -0
- package/src/readiness-protocols/evidence.mjs +2 -1
- package/src/readiness-protocols/packs/mnstry.mjs +766 -0
- package/src/readiness-protocols/runtime.mjs +43 -6
- package/src/runtime/obsidian/engine.mjs +387 -76
- package/src/runtime/obsidian/lifecycle.mjs +20 -15
- package/src/runtime/obsidian/login-item.mjs +6 -3
- package/src/runtime/obsidian/machine-settings.mjs +52 -8
- package/src/runtime/obsidian/observation.mjs +36 -4
- package/src/runtime/obsidian/opening.mjs +4 -3
- package/src/runtime/obsidian/personal-selection.mjs +187 -0
- package/src/runtime/obsidian/pipeline.mjs +115 -6
- package/src/runtime/obsidian/private-lock.mjs +50 -6
- package/src/runtime/obsidian/selection-continuity.mjs +124 -0
- package/src/runtime/obsidian/service-main.mjs +68 -16
- package/src/runtime/obsidian/service.mjs +80 -6
- package/src/runtime/obsidian/unheld-evidence.mjs +1 -1
- package/src/runtime/obsidian/view-permission.mjs +335 -0
- package/src/ui/control-system.mjs +201 -0
- package/src/ui/html-primitives.mjs +3 -0
- package/src/upgrade/closure-diagnostics.mjs +135 -0
- package/src/upgrade/closure.mjs +649 -0
- package/src/upgrade/upgrade.mjs +42 -11
- package/templates/distribution-workspace/README.md +2 -2
- package/templates/external-project-workspace/atelier.project.json +3 -0
- package/templates/external-project-workspace/pack-lifecycle.json +3 -1
- package/templates/knowledge-workspace/README.md +2 -2
- package/docs/integration-and-review-plan.md +0 -409
- package/docs/integration-contract-decisions.md +0 -44
- package/docs/knowledge-health/devday/facilitator.md +0 -16
- package/docs/knowledge-health/devday/public-default.md +0 -107
- package/docs/knowledge-health/devday/quickstart.md +0 -18
- package/docs/knowledge-health/devday/support.md +0 -22
- package/docs/knowledge-health/exercise.md +0 -17
- package/docs/knowledge-health/support.md +0 -34
- package/docs/presentation-review-disposition.md +0 -47
- package/docs/progressive-ingestion-acceptance.md +0 -89
- package/docs/progressive-ingestion.md +0 -235
- /package/docs/{exact-upgrades.md → guides/exact-upgrades.md} +0 -0
- /package/docs/{guided-upgrades.md → guides/guided-upgrades.md} +0 -0
- /package/docs/{local-review.md → guides/local-review.md} +0 -0
- /package/docs/{skill-steward.md → guides/skill-steward.md} +0 -0
- /package/docs/{template-upgrades.md → guides/template-upgrades.md} +0 -0
- /package/docs/{assurance-controls.md → maintainers/assurance-controls.md} +0 -0
- /package/docs/{attestation.md → reference/attestation.md} +0 -0
- /package/docs/{coauthor-session.md → reference/coauthor-session.md} +0 -0
- /package/docs/{continuity.md → reference/continuity.md} +0 -0
- /package/docs/{contract-stability.md → reference/contract-stability.md} +0 -0
- /package/docs/{decision-practices.md → reference/decision-practices.md} +0 -0
- /package/docs/{evidence-navigation.md → reference/evidence-navigation.md} +0 -0
- /package/docs/{extension-pack-lifecycle.md → reference/extension-pack-lifecycle.md} +0 -0
- /package/docs/{interaction-and-reflection.md → reference/interaction-and-reflection.md} +0 -0
- /package/docs/{local-state.md → reference/local-state.md} +0 -0
- /package/docs/{ontology.md → reference/ontology.md} +0 -0
- /package/docs/{optional-ingestion.md → reference/optional-ingestion.md} +0 -0
- /package/docs/{project-command-inventory.md → reference/project-command-inventory.md} +0 -0
- /package/docs/{semantic-ingestion.md → reference/semantic-ingestion.md} +0 -0
- /package/docs/{vault-service.md → reference/vault-service.md} +0 -0
|
@@ -17,7 +17,7 @@ import { BUILT_IN_OPERATIONS, UNAVAILABLE_APPLY_OPERATION, createObsidianRegistr
|
|
|
17
17
|
import { LIFECYCLE_PRIMITIVES, readServiceStatusDocument, requestServiceTick, runtimeRelease, serviceStatus, startService, stopService } from '../runtime/obsidian/lifecycle.mjs'
|
|
18
18
|
import {
|
|
19
19
|
DECISIONS, ONLY_YOU_AUDIENCES, authorizeAutomaticApply, defaultMachineSettings, ensureWorkspaceIdentity, installApplyPolicy, localPointerPath, protectedRoots,
|
|
20
|
-
readInstalledApplyPolicy, readMachineSettings, revokeApplyPolicy, withDecision, writeMachineSettings,
|
|
20
|
+
readInstalledApplyPolicy, readLocalPointer, readMachineSettings, revokeApplyPolicy, withDecision, writeMachineSettings,
|
|
21
21
|
} from '../runtime/obsidian/machine-settings.mjs'
|
|
22
22
|
import {
|
|
23
23
|
currentLoginItemPlan, installLoginItem, loginItemDataRoot, loginItemLabel, loginItemStarter, loginItemStatus, planLoginItem, projectNameOf, readLoginItemRecord, realNodePath,
|
|
@@ -29,7 +29,7 @@ import { pluginPresenceOf, turnPluginOnNext, withPluginFilesPending, withPluginR
|
|
|
29
29
|
import { planViewAdd, viewFromRequest, writeViewPlan } from '../runtime/obsidian/project-views.mjs'
|
|
30
30
|
import { askGoAhead, createQuestioner } from '../runtime/obsidian/questions.mjs'
|
|
31
31
|
import { readServiceSettings } from '../runtime/obsidian/service-record.mjs'
|
|
32
|
-
import { resolveServiceWorkspace } from '../runtime/obsidian/service.mjs'
|
|
32
|
+
import { resolveServiceWorkspace, serviceLocationCheck } from '../runtime/obsidian/service.mjs'
|
|
33
33
|
import { STARTUP_PLATFORMS, buildStartupAdapter, startupSearchPath } from '../runtime/obsidian/startup-adapters.mjs'
|
|
34
34
|
import { OBSIDIAN_SETTINGS_FILE, obsidianSandboxedBuild, obsidianUserDataDir, readObsidianSettings } from '../projection/obsidian/publication/vault-list.mjs'
|
|
35
35
|
import { checkVaultParent, projectDisplayName, vaultFolderName } from '../runtime/obsidian/vault-location.mjs'
|
|
@@ -69,7 +69,7 @@ const PRODUCTION_ADAPTER = 'obsidian-cli'
|
|
|
69
69
|
const MAX_POLICY_BYTES = 64 * 1024
|
|
70
70
|
const AUDIENCE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
71
71
|
|
|
72
|
-
export const USAGE = `Usage: atelier obsidian <operation> [--project atelier.project.json] [--data-root DIR] [--json] [--no-input]
|
|
72
|
+
export const USAGE = `Usage: atelier obsidian <operation> [--project atelier.project.json | --personal-home DIR] [--data-root DIR] [--json] [--no-input]
|
|
73
73
|
|
|
74
74
|
status Enablement, machine settings, service and per-view freshness. Read-only.
|
|
75
75
|
settings What this machine remembers for this workspace, and how to change it. Read-only.
|
|
@@ -111,6 +111,11 @@ export const USAGE = `Usage: atelier obsidian <operation> [--project atelier.pro
|
|
|
111
111
|
service publishes the view at once (with --adapter, one of an earlier release
|
|
112
112
|
is replaced first).
|
|
113
113
|
|
|
114
|
+
--personal-home DIR, given instead of --project, follows the generation last confirmed in that private home of a personal
|
|
115
|
+
workspace (the personal-workspace module's selection); its views come from the overlay. Nothing here confirms one: while
|
|
116
|
+
none is confirmed and current nothing is published, and status, open and service start refuse with the reason. service
|
|
117
|
+
status and stop, unit --remove and uninstall find the service from the home alone. Who may see its vaults stays this
|
|
118
|
+
machine's audience setting: owner only when undecided; an explicit clear stays empty.
|
|
114
119
|
Reaching the installed app needs --adapter=${PRODUCTION_ADAPTER} once for a workspace; it is remembered after that.
|
|
115
120
|
Obsidian's own settings file (obsidian.json) is written only while no Obsidian runs: open adds the view's vault to its
|
|
116
121
|
list, turns its command line on (cli: true), and creates the file for an Obsidian that never started; every write is
|
|
@@ -139,7 +144,7 @@ Exit codes: 0 done; 1 internal error; 2 refusal or usage; 3 ran, and the answer
|
|
|
139
144
|
// spelling of `project`); a test holds it to that.
|
|
140
145
|
export const FLAGS = Object.freeze({
|
|
141
146
|
json: 'flag', 'allow-stale': 'flag', 'restart-obsidian': 'flag', print: 'flag', install: 'flag', remove: 'flag', help: 'flag', 'no-input': 'flag', 'allow-synced-location': 'flag', all: 'flag', default: 'flag', 'allow-empty': 'flag', yes: 'flag',
|
|
142
|
-
project: 'value', 'project-config': 'value', 'data-root': 'value', scope: 'value', 'consent-actor': 'value', actor: 'value', adapter: 'value', 'wait-ms': 'value', repo: 'value', tag: 'value',
|
|
147
|
+
project: 'value', 'project-config': 'value', 'personal-home': 'value', 'data-root': 'value', scope: 'value', 'consent-actor': 'value', actor: 'value', adapter: 'value', 'wait-ms': 'value', repo: 'value', tag: 'value',
|
|
143
148
|
expand: 'value', folder: 'values',
|
|
144
149
|
})
|
|
145
150
|
|
|
@@ -276,6 +281,7 @@ const NEXT = Object.freeze({
|
|
|
276
281
|
'login-item-remove-failed': 'see `atelier obsidian status`; `atelier obsidian service unit --remove` tries again',
|
|
277
282
|
'login-item-file-unsafe': 'the login item\'s file is a link or not a regular file and is left alone; remove it by hand, then try again',
|
|
278
283
|
'invalid-login-item': 'the login item record in private state does not validate; inspect it, remove it, then install the login item again',
|
|
284
|
+
'selection-history-shortened': 'records of the selection history were removed after this process read them; put them back, or run the command in a process of its own, which reads the history as it now stands',
|
|
279
285
|
})
|
|
280
286
|
|
|
281
287
|
// The login item of `status`, in words.
|
|
@@ -372,10 +378,16 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
372
378
|
|
|
373
379
|
const dataRoot = flags['data-root'] === undefined ? options.dataRoot : path.resolve(cwd, flags['data-root'])
|
|
374
380
|
const configFlag = flags.project ?? flags['project-config']
|
|
375
|
-
|
|
381
|
+
// A personal home is followed instead of a project: the generation confirmed there is loaded (composed in this
|
|
382
|
+
// thread, as for a command run once), and the service's workspace is located from the home alone.
|
|
383
|
+
const personalHome = flags['personal-home'] === undefined ? null : path.resolve(cwd, flags['personal-home'])
|
|
384
|
+
if (personalHome !== null && configFlag !== undefined) refuse('usage', '--personal-home is given instead of --project, not with it')
|
|
385
|
+
const selection = personalHome === null ? null : await import('../runtime/obsidian/personal-selection.mjs')
|
|
386
|
+
const loadProject = options.loadProject ?? (selection !== null ? () => selection.loadSelectedProject({ personalHome, dataRoot, env, platform }) : () => resolveProjectConfig({ argv: configFlag === undefined ? [] : [`--project=${path.resolve(cwd, configFlag)}`], cwd, env, writeLocalState: false }))
|
|
387
|
+
const locateProject = selection === null ? null : () => selection.locatePersonalHome({ personalHome })
|
|
376
388
|
const registry = createObsidianRegistry({ reservedOperations: BUILT_IN_OPERATIONS, contributions: contributions ?? await loadContributions(contributionsDirectory === undefined ? {} : { directory: contributionsDirectory }) })
|
|
377
389
|
const applyAvailable = registry.extensions.applyOperation() !== UNAVAILABLE_APPLY_OPERATION
|
|
378
|
-
const lifecycle = { loadProject, dataRoot, env, platform, ...(options.probeTimeoutMs === undefined ? {} : { probeTimeoutMs: options.probeTimeoutMs }) }
|
|
390
|
+
const lifecycle = { loadProject, ...(locateProject === null ? {} : { locateProject }), dataRoot, env, platform, ...(options.probeTimeoutMs === undefined ? {} : { probeTimeoutMs: options.probeTimeoutMs }) }
|
|
379
391
|
|
|
380
392
|
// The adapter of this run: given now, or remembered by this workspace (read only when the real entry has no flag).
|
|
381
393
|
const chooseAdapter = () => selectAdapter({ flag: flags.adapter, remembered: flags.adapter === undefined && production === true ? rememberedAdapter() : null, production, env })
|
|
@@ -458,8 +470,8 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
458
470
|
const pluginOf = async (scopeId) => pluginPresenceOf((await readServiceStatusDocument(lifecycle, lifecycleRules)).document, scopeId)
|
|
459
471
|
const pluginLine = (plugin) => `${plugin.present ? `; plugin present (Obsidian ${plugin.appVersion})` : plugin.reason === 'turned-off-in-this-vault' ? '; plugin turned off in this vault' : `; plugin not present (${plugin.reason})`}${plugin.files === 'waits-for-app' ? '; plugin files wait for the app' : ''}`
|
|
460
472
|
|
|
461
|
-
// A
|
|
462
|
-
//
|
|
473
|
+
// A personal binding takes its views from the overlay and keeps its audience owner only (or explicitly empty).
|
|
474
|
+
// Operations outside those machine decisions refuse before anything is resolved or written.
|
|
463
475
|
const refuseOnPersonalBinding = async (project, operation) => (await import('../projection/obsidian/personal-workspace.mjs')).refuseOnPersonalBinding(project, operation)
|
|
464
476
|
const configured = () => {
|
|
465
477
|
const project = loadProject()
|
|
@@ -470,7 +482,7 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
470
482
|
// Private state of this workspace, created on demand for an operation that writes it.
|
|
471
483
|
const writable = () => {
|
|
472
484
|
const { project, enablement } = configured()
|
|
473
|
-
ensureWorkspaceIdentity({ project, ...(dataRoot === undefined ? {} : { dataRoot }) })
|
|
485
|
+
ensureWorkspaceIdentity({ project, ...(dataRoot === undefined ? {} : { dataRoot }), validate: serviceLocationCheck({ project, dataRoot, env, platform }) })
|
|
474
486
|
const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform, create: true })
|
|
475
487
|
return { project, enablement, workspace, repositoryRoots: protectedRoots(project), now: isoTime(clock) }
|
|
476
488
|
}
|
|
@@ -617,10 +629,10 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
617
629
|
const removeUnit = async () => {
|
|
618
630
|
if (!STARTUP_PLATFORMS.includes(platform)) buildStartupAdapter({ platform })
|
|
619
631
|
const manager = await managerSeam()
|
|
620
|
-
const removed = await removeLoginItem({ loadProject, dataRoot, env, platform, manager, clock })
|
|
632
|
+
const removed = await removeLoginItem({ loadProject: locateProject ?? loadProject, dataRoot, env, platform, manager, clock })
|
|
621
633
|
const failed = removed.removed !== true && removed.reason !== undefined && removed.reason !== 'workspace-not-prepared'
|
|
622
634
|
if (failed) return { exit: EXIT.notSuccess, document: { loginItem: removed }, human: [`login item not removed: ${removed.reason}${removed.message ? ` (${removed.message})` : ''}`, `Next: ${NEXT[removed.reason] ?? 'see `atelier obsidian status`'}`] }
|
|
623
|
-
const remembered = removed.reason === 'workspace-not-prepared' ? false : rememberLoginItem('off')
|
|
635
|
+
const remembered = removed.reason === 'workspace-not-prepared' ? false : rememberLoginItem('off', { locate: true })
|
|
624
636
|
let service = null
|
|
625
637
|
if (removed.removed === true) {
|
|
626
638
|
let entry = null
|
|
@@ -645,9 +657,18 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
645
657
|
// The person's answer to "start at login?", remembered for this workspace as the `loginItem` decision of its machine
|
|
646
658
|
// settings, given on the command line. By whom: the actor named, else a person at a terminal by the account's name,
|
|
647
659
|
// else nobody known. Answers whether it wrote; an answer already remembered is not written again.
|
|
648
|
-
const rememberLoginItem = (choice) => {
|
|
649
|
-
|
|
650
|
-
|
|
660
|
+
const rememberLoginItem = (choice, { locate = false } = {}) => {
|
|
661
|
+
let workspace, repositoryRoots, now
|
|
662
|
+
if (locate && locateProject !== null) {
|
|
663
|
+
const project = locateProject()
|
|
664
|
+
workspace = resolveServiceWorkspace({ project, dataRoot, env, platform })
|
|
665
|
+
if (!workspace?.workspaceRoot) return false
|
|
666
|
+
repositoryRoots = protectedRoots(project)
|
|
667
|
+
now = isoTime(clock)
|
|
668
|
+
} else {
|
|
669
|
+
if (readable().enablement.reason === 'not-configured') return false
|
|
670
|
+
;({ workspace, repositoryRoots, now } = writable())
|
|
671
|
+
}
|
|
651
672
|
const current = machineOf(workspace) ?? defaultMachineSettings({ workspaceId: workspace.workspaceId, updatedAt: now })
|
|
652
673
|
if (current.decisions.loginItem?.choice === choice) return false
|
|
653
674
|
const decidedBy = flags['consent-actor'] ?? (interactive ? accountActor(account()) : null)
|
|
@@ -681,7 +702,7 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
681
702
|
}
|
|
682
703
|
const vaults = [...new Set([...underDataRoot, ...allocated])].sort()
|
|
683
704
|
const userDataDir = obsidianUserDataDir({ platform, env })
|
|
684
|
-
return { vaults, privateState: workspace?.workspaceRoot ?? null, projectFile: project.configPath ?? null, pointer: project.
|
|
705
|
+
return { vaults, privateState: workspace?.workspaceRoot ?? null, projectFile: project.configPath ?? null, pointer: typeof project.configDir === 'string' && readLocalPointer(project) !== null ? localPointerPath(project) : null, obsidianList: userDataDir === null ? null : path.join(userDataDir, OBSIDIAN_SETTINGS_FILE) }
|
|
685
706
|
}
|
|
686
707
|
|
|
687
708
|
// Where a view's vault is, or will be: { path, origin }. A view the next tick will place is shown where it would go,
|
|
@@ -720,7 +741,10 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
720
741
|
const plugin = withPluginFilesPending(pluginView(running, workspace, scopeId), report.freshness)
|
|
721
742
|
return enablement.state === 'disabled' ? { ...report, outcome: 'disabled', reason: enablement.reason, next: OPENING_OUTCOMES.disabled.next, plugin } : { ...report, plugin }
|
|
722
743
|
})
|
|
744
|
+
// The confirmed selection a personal home's project was loaded under; reported, never confirmed here.
|
|
745
|
+
const pinned = selection === null ? null : selection.personalSelectionOf(project)
|
|
723
746
|
const document = {
|
|
747
|
+
...(pinned === null ? {} : { selection: { generationId: pinned.generationId, sequence: pinned.sequence } }),
|
|
724
748
|
enablement: { state: enablement.state, reason: enablement.reason, defaultScopeId: enablement.defaultScopeId }, workspace: { workspaceId, prepared: workspace !== null },
|
|
725
749
|
machine: shownMachine(machineOf(workspace), workspace), apply: applyShown,
|
|
726
750
|
service: { state: service.state, reason: service.reason ?? null, address: service.address ?? null, runtimeId: service.record?.runtimeId ?? null, pid: service.record?.pid ?? null, lastTick: running?.lastTick ?? null, lastError: running?.lastError ?? null, app },
|
|
@@ -729,6 +753,7 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
729
753
|
return {
|
|
730
754
|
exit: EXIT.ok, document,
|
|
731
755
|
human: [
|
|
756
|
+
...(pinned === null ? [] : [`personal workspace: generation ${pinned.generationId}, confirmed as selection ${pinned.sequence}`]),
|
|
732
757
|
`obsidian: ${enablement.state} (${enablement.reason}); mode ${document.machine.maintenanceMode}; audiences ${document.machine.audienceAllow.join(', ') || 'none'}`,
|
|
733
758
|
`remembered: ${decisionSummary(document.machine.decisions)}`,
|
|
734
759
|
`service: ${service.state} (${service.reason ?? 'no reason'})${app ? `; app ${app.outcome} (${app.reason})` : ''}`,
|
|
@@ -885,7 +910,7 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
885
910
|
// `me` stands for "only you". Named beside other audiences, it adds its own to theirs, and the list is the person's own choice.
|
|
886
911
|
const audienceAllow = [...new Set(named.flatMap((item) => (item === 'me' ? ONLY_YOU_AUDIENCES : [item])))]
|
|
887
912
|
const choice = named.length === 1 && named[0] === 'me' ? 'only-you' : 'custom'
|
|
888
|
-
await refuseOnPersonalBinding(loadProject(), `audience ${sub}`)
|
|
913
|
+
await refuseOnPersonalBinding(loadProject(), choice === 'only-you' ? 'audience set me' : `audience ${sub}`)
|
|
889
914
|
const { workspace, repositoryRoots, now } = writable()
|
|
890
915
|
const current = machineOf(workspace) ?? defaultMachineSettings({ workspaceId: workspace.workspaceId, updatedAt: now })
|
|
891
916
|
// A vault that is only yours shows the notes that carry no classification too; any other list withholds them.
|
|
@@ -1073,17 +1098,19 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
1073
1098
|
// Maintenance of this workspace ends here: the login item goes first, so its manager does not start the service
|
|
1074
1099
|
// again, then the proven service stops. Everything a person made or may want back stays, and is named.
|
|
1075
1100
|
async uninstall() {
|
|
1076
|
-
const project = loadProject()
|
|
1077
|
-
|
|
1078
|
-
|
|
1101
|
+
const project = (locateProject ?? loadProject)()
|
|
1102
|
+
// Only an established absence (no pointer, or no private state yet) is unprepared. A location or identity refusal
|
|
1103
|
+
// stops here, before any login-item or service action, so uninstall never reports success over it.
|
|
1104
|
+
const found = resolveServiceWorkspace({ project, dataRoot, env, platform })
|
|
1105
|
+
const workspace = found?.workspaceRoot ? found : null
|
|
1079
1106
|
const record = workspace === null ? null : readLoginItemRecord(workspace)
|
|
1080
1107
|
// What is kept is read before anything is stopped or removed, so nothing it finds can stop the answer after that.
|
|
1081
1108
|
const kept = keptLocations(project, workspace)
|
|
1082
|
-
const item = record === null ? { removed: false, reason: 'not-installed' } : await removeLoginItem({ loadProject, dataRoot, env, platform, manager: await managerSeam(), clock })
|
|
1109
|
+
const item = record === null ? { removed: false, reason: 'not-installed' } : await removeLoginItem({ loadProject: locateProject ?? loadProject, dataRoot, env, platform, manager: await managerSeam(), clock })
|
|
1083
1110
|
const service = workspace === null ? { state: 'stopped', stopped: false, refused: false, reason: 'workspace-not-prepared' } : await stopService(lifecycle, lifecycleRules)
|
|
1084
1111
|
const itemGone = item.removed === true || item.reason === 'not-installed' || item.reason === 'workspace-not-prepared'
|
|
1085
1112
|
const serviceGone = service.stopped === true || service.state === 'stopped'
|
|
1086
|
-
const remembered = workspace !== null && itemGone ? rememberLoginItem('off') : false
|
|
1113
|
+
const remembered = workspace !== null && itemGone ? rememberLoginItem('off', { locate: true }) : false
|
|
1087
1114
|
return {
|
|
1088
1115
|
exit: itemGone && serviceGone ? EXIT.ok : EXIT.notSuccess,
|
|
1089
1116
|
document: { loginItem: item, service, kept, rememberedNow: { loginItem: remembered ? 'off' : null } },
|
|
@@ -1095,6 +1122,7 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
1095
1122
|
...kept.vaults.map((vault) => ` vault ${vault}`),
|
|
1096
1123
|
...(kept.privateState === null ? [] : [` private state ${kept.privateState}`]),
|
|
1097
1124
|
` project file ${kept.projectFile ?? '(none)'}; its Obsidian settings stay`,
|
|
1125
|
+
...(kept.pointer === null ? [] : [` workspace pointer ${kept.pointer}`]),
|
|
1098
1126
|
...(kept.obsidianList === null ? [] : [` Obsidian's list ${kept.obsidianList}; the vaults stay in it`]),
|
|
1099
1127
|
],
|
|
1100
1128
|
}
|
package/src/commands/review.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
createProtocolRegistry,
|
|
8
8
|
} from '../extension-packs/loader.mjs'
|
|
9
9
|
import { runProtocol } from '../readiness-protocols/runtime.mjs'
|
|
10
|
+
import { bundledReadinessEnabled, requireSelectedReadinessProtocol } from '../readiness-protocols/bundled-selection.mjs'
|
|
10
11
|
import { recordReviewEvidence } from '../readiness-protocols/evidence.mjs'
|
|
11
12
|
import { createReviewStore } from '../collaboration/review-store.mjs'
|
|
12
13
|
import {
|
|
@@ -73,7 +74,9 @@ let result
|
|
|
73
74
|
if (args._[0] === 'run') {
|
|
74
75
|
const registry = createProtocolRegistry({
|
|
75
76
|
packs: loadExtensionPacks(project).packs,
|
|
77
|
+
includeBundled: bundledReadinessEnabled(project),
|
|
76
78
|
})
|
|
79
|
+
requireSelectedReadinessProtocol(project, registry, args._[1])
|
|
77
80
|
const answers = args.answers ? readJson(path.resolve(args.answers)) : {}
|
|
78
81
|
const { run } = runProtocol(project, args._[1], {
|
|
79
82
|
answers,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import { canonicalize } from '../attestation/jcs.mjs'
|
|
3
|
+
|
|
4
|
+
// Experimental adapter shapes, not an ontology or identity authority.
|
|
5
|
+
export const EDGE_RECORD_SCHEMA = 'atelier-edge-record@experimental-v1'
|
|
6
|
+
export const EDGE_DECISION_SCHEMA = 'atelier-edge-decision@experimental-v1'
|
|
7
|
+
export const EDGE_FAMILIES = Object.freeze(['hyperlink', 'source-declaration', 'reviewed-assertion'])
|
|
8
|
+
export const EDGE_ACTIONS = Object.freeze(['propose', 'approve', 'accept', 'reject', 'suspend', 'withdraw', 'supersede'])
|
|
9
|
+
const digestPattern = /^sha256:[a-f0-9]{64}$/
|
|
10
|
+
const text = (v, max = 256) => typeof v === 'string' && v.length > 0 && v.trim() === v && v.length <= max && !/[\u0000-\u001f\u007f]/.test(v)
|
|
11
|
+
const object = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
12
|
+
const closed = (v, keys) => object(v) && Object.keys(v).length === keys.length && keys.every((k) => Object.hasOwn(v, k))
|
|
13
|
+
const relativePath = (v) => text(v, 2048) && !v.includes('\\') && !v.startsWith('/') && !/^[A-Za-z]:/.test(v) && v.split('/').every((p) => p && p !== '.' && p !== '..')
|
|
14
|
+
export const validNodeRef = (v) => closed(v, ['repoId', 'localId']) && text(v.repoId) && text(v.localId)
|
|
15
|
+
export const bytesDigest = (v) => `sha256:${crypto.createHash('sha256').update(v).digest('hex')}`
|
|
16
|
+
export const edgeDigest = (v) => bytesDigest(canonicalize(v))
|
|
17
|
+
export function immutable(value) {
|
|
18
|
+
if (value && typeof value === 'object') {
|
|
19
|
+
for (const child of Object.values(value)) immutable(child)
|
|
20
|
+
Object.freeze(value)
|
|
21
|
+
}
|
|
22
|
+
return value
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validSourceRef(v) {
|
|
26
|
+
return closed(v, ['repoId', 'path', 'revision', 'fileDigest', 'locator', 'citedDigest']) &&
|
|
27
|
+
text(v.repoId) && relativePath(v.path) && text(v.revision) && digestPattern.test(v.fileDigest) && digestPattern.test(v.citedDigest) &&
|
|
28
|
+
closed(v.locator, ['byteStart', 'byteEnd']) && Number.isSafeInteger(v.locator.byteStart) && v.locator.byteStart >= 0 &&
|
|
29
|
+
Number.isSafeInteger(v.locator.byteEnd) && v.locator.byteEnd > v.locator.byteStart
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function validateEdgeRecord(v) {
|
|
33
|
+
try {
|
|
34
|
+
if (!closed(v, ['schema', 'family', 'subject', 'predicate', 'object', 'home', 'origin', 'supports', 'policyDigest', 'proposalRef', 'sourceChange', 'qualifications'])) return false
|
|
35
|
+
if (v.schema !== EDGE_RECORD_SCHEMA || !EDGE_FAMILIES.includes(v.family) || !validNodeRef(v.subject) || !validNodeRef(v.object) || !text(v.predicate)) return false
|
|
36
|
+
if (!closed(v.home, ['repoId', 'path', 'readership']) || !text(v.home.repoId) || !relativePath(v.home.path) || !text(v.home.readership)) return false
|
|
37
|
+
if (!validSourceRef(v.origin) || !Array.isArray(v.supports) || v.supports.length > 256 ||
|
|
38
|
+
v.supports.some((s) => !closed(s, ['id', 'source']) || !text(s.id) || !validSourceRef(s.source)) || new Set(v.supports.map((s) => s.id)).size !== v.supports.length) return false
|
|
39
|
+
if (!digestPattern.test(v.policyDigest) || (v.proposalRef !== null && !text(v.proposalRef, 2048)) ||
|
|
40
|
+
!Array.isArray(v.qualifications) || v.qualifications.length > 64 || v.qualifications.some((q) => !text(q, 2048))) return false
|
|
41
|
+
if (v.family === 'hyperlink' && (v.predicate !== 'links-to' || v.sourceChange !== null)) return false
|
|
42
|
+
if (v.sourceChange !== null && (!closed(v.sourceChange, ['repoId', 'path', 'baseRevision', 'diffDigest', 'reviewRef']) ||
|
|
43
|
+
!text(v.sourceChange.repoId) || !relativePath(v.sourceChange.path) || !text(v.sourceChange.baseRevision) || !digestPattern.test(v.sourceChange.diffDigest) || !text(v.sourceChange.reviewRef, 2048))) return false
|
|
44
|
+
edgeDigest(v)
|
|
45
|
+
return true
|
|
46
|
+
} catch { return false }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function validateEdgeDecision(v) {
|
|
50
|
+
try {
|
|
51
|
+
if (!closed(v, ['schema', 'recordDigest', 'sequence', 'previousDigest', 'action', 'actorId', 'at', 'policyDigest', 'reason', 'roles', 'signature'])) return false
|
|
52
|
+
if (v.schema !== EDGE_DECISION_SCHEMA || !digestPattern.test(v.recordDigest) || !Number.isSafeInteger(v.sequence) || v.sequence < 1 ||
|
|
53
|
+
(v.previousDigest !== null && !digestPattern.test(v.previousDigest)) || !EDGE_ACTIONS.includes(v.action) || !text(v.actorId) ||
|
|
54
|
+
!text(v.at) || !Number.isFinite(Date.parse(v.at)) || new Date(v.at).toISOString() !== v.at || !digestPattern.test(v.policyDigest) || !text(v.reason, 2048)) return false
|
|
55
|
+
if (!Array.isArray(v.roles) || v.roles.length > 3 || v.roles.some((r) => !closed(r, ['repoId', 'role']) || !text(r.repoId) || !['home-owner', 'endpoint-steward'].includes(r.role)) ||
|
|
56
|
+
new Set(v.roles.map((r) => canonicalize(r))).size !== v.roles.length) return false
|
|
57
|
+
if (v.action !== 'approve' && v.roles.length !== 0) return false
|
|
58
|
+
if (v.action === 'approve' && v.roles.length === 0) return false
|
|
59
|
+
if (!closed(v.signature, ['algorithm', 'keyId', 'value']) || !['ed25519', 'es256'].includes(v.signature.algorithm) || !text(v.signature.keyId) || !text(v.signature.value, 512)) return false
|
|
60
|
+
edgeDigest(v)
|
|
61
|
+
return true
|
|
62
|
+
} catch { return false }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function edgeIdentity(record) {
|
|
66
|
+
if (!validateEdgeRecord(record)) throw new TypeError('invalid edge record')
|
|
67
|
+
const recordDigest = edgeDigest(record)
|
|
68
|
+
return Object.freeze({ edgeId: `edge:${recordDigest.slice(7)}`, recordDigest })
|
|
69
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { verifyDocument } from '../attestation/sign.mjs'
|
|
2
|
+
import { bytesDigest, edgeDigest, edgeIdentity, immutable, validateEdgeDecision, validateEdgeRecord } from './contract.mjs'
|
|
3
|
+
|
|
4
|
+
const roleKey = (role) => `${role.role}\0${role.repoId}`
|
|
5
|
+
const same = (a, b) => edgeDigest(a) === edgeDigest(b)
|
|
6
|
+
const fail = (state, reason) => ({ state, eligible: false, reason })
|
|
7
|
+
const decisions = new WeakSet()
|
|
8
|
+
const seal = (value) => { const result = immutable(value); decisions.add(result); return result }
|
|
9
|
+
function demand(value, reason) { if (!value) throw new Error(reason) }
|
|
10
|
+
|
|
11
|
+
function validPolicy(p) {
|
|
12
|
+
return p && Object.keys(p).sort().join(',') === 'allowProposerReview,allowSharedSteward,version' &&
|
|
13
|
+
typeof p.version === 'string' && p.version.length > 0 && typeof p.allowProposerReview === 'boolean' && typeof p.allowSharedSteward === 'boolean'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Content-bound occurrence identity only; this does not establish access,
|
|
17
|
+
// source currency or eligibility. Keep caller-owned records untouched.
|
|
18
|
+
export function edgeEvidenceOccurrences(record) {
|
|
19
|
+
const { edgeId } = edgeIdentity(record)
|
|
20
|
+
const occurrence = (id, source) => ({ id: `evidence:${edgeDigest({ edgeId, occurrenceId: id, source }).slice(7)}`, occurrenceId: id, source: structuredClone(source) })
|
|
21
|
+
return immutable([occurrence('origin', record.origin), ...record.supports.map((s) => occurrence(`support:${s.id}`, s.source))])
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Host functions are trusted integration ports. No record-supplied identity,
|
|
25
|
+
// eligible flag, audience or local asserted review substitutes for these ports.
|
|
26
|
+
// The host must hold one coherent current snapshot across an evaluation.
|
|
27
|
+
export async function evaluateEdge(input, host) {
|
|
28
|
+
let record, events
|
|
29
|
+
try {
|
|
30
|
+
record = structuredClone(input.record)
|
|
31
|
+
events = structuredClone(input.events)
|
|
32
|
+
if (!validateEdgeRecord(record) || !Array.isArray(events) || events.length > 1024 || events.some((e) => !validateEdgeDecision(e))) return immutable(fail('suspended', 'invalid-input'))
|
|
33
|
+
immutable(record); immutable(events)
|
|
34
|
+
const identity = edgeIdentity(record)
|
|
35
|
+
const policy = immutable(structuredClone(host.policy))
|
|
36
|
+
demand(validPolicy(policy), 'policy-unavailable')
|
|
37
|
+
const policyDigest = edgeDigest(policy)
|
|
38
|
+
demand(typeof host.bindingId === 'string' && host.bindingId.length > 0, 'snapshot-binding-unavailable')
|
|
39
|
+
const base = { ...identity, family: record.family, policyDigest, verification: { checkedAt: host.now, bindingId: host.bindingId }, reviewRefs: [], triple: { subject: record.subject, predicate: record.predicate, object: record.object },
|
|
40
|
+
supportRefs: edgeEvidenceOccurrences(record), qualifications: record.qualifications,
|
|
41
|
+
sourceChange: record.sourceChange, history: events }
|
|
42
|
+
const result = (value) => seal({ ...base, ...value })
|
|
43
|
+
if (record.policyDigest !== policyDigest) return result(fail('suspended', 'policy-changed'))
|
|
44
|
+
const now = Date.parse(host.now)
|
|
45
|
+
demand(Number.isFinite(now), 'clock-unavailable')
|
|
46
|
+
const head = await host.readRecordHead(identity)
|
|
47
|
+
if (!head || head.recordDigest !== identity.recordDigest || head.eventCount !== events.length || head.eventDigest !== (events.length ? edgeDigest(events.at(-1)) : null)) return result(fail('suspended', 'history-head-mismatch'))
|
|
48
|
+
const origin = await host.verifyOrigin({ record, ...identity, policyDigest })
|
|
49
|
+
if (!origin || origin.recordDigest !== identity.recordDigest || origin.bindingId !== host.bindingId || origin.matches !== true) return result(fail('suspended', 'origin-mismatch'))
|
|
50
|
+
// Safe placement considers the home's entire intended readership, not only
|
|
51
|
+
// this reader. The host checks endpoints, all evidence and record metadata.
|
|
52
|
+
const placement = await host.checkSourceHome({ record, ...identity, policyDigest })
|
|
53
|
+
if (!placement || placement.recordDigest !== identity.recordDigest || placement.policyDigest !== policyDigest || placement.bindingId !== host.bindingId || placement.allowed !== true) return result(fail('suspended', 'unsafe-source-home'))
|
|
54
|
+
const endpoints = await Promise.all([record.subject, record.object].map((ref) => host.resolveEndpoint(ref)))
|
|
55
|
+
if (endpoints.some((e, i) => !e || !same(e.ref, [record.subject, record.object][i]) || e.current !== true || typeof e.stewardId !== 'string' || !e.stewardId)) return result(fail('suspended', 'endpoint-unavailable'))
|
|
56
|
+
const home = await host.resolveHomeOwner(record.home.repoId)
|
|
57
|
+
if (!home || typeof home.actorId !== 'string' || !home.actorId) return result(fail('suspended', 'home-owner-unavailable'))
|
|
58
|
+
for (const { source } of base.supportRefs) {
|
|
59
|
+
const observed = await host.readSource(source)
|
|
60
|
+
if (!observed || observed.revision !== source.revision || !(observed.bytes instanceof Uint8Array)) return result(fail('stale', 'source-changed'))
|
|
61
|
+
const bytes = Buffer.from(observed.bytes)
|
|
62
|
+
if (bytesDigest(bytes) !== source.fileDigest || source.locator.byteEnd > bytes.length ||
|
|
63
|
+
bytesDigest(bytes.subarray(source.locator.byteStart, source.locator.byteEnd)) !== source.citedDigest) return result(fail('stale', 'citation-changed'))
|
|
64
|
+
}
|
|
65
|
+
const semantic = record.family !== 'hyperlink'
|
|
66
|
+
const crossRepo = record.subject.repoId !== record.object.repoId
|
|
67
|
+
const governed = semantic && (crossRepo || record.family === 'reviewed-assertion' || events.length > 0)
|
|
68
|
+
if (!semantic) return result(events.length ? fail('suspended', 'navigation-has-decisions') : { state: 'active', eligible: true, assurance: 'navigation' })
|
|
69
|
+
if (!governed) return result({ state: 'active', eligible: true, assurance: 'source-declaration' })
|
|
70
|
+
if (!record.proposalRef || !record.sourceChange || record.sourceChange.repoId !== record.home.repoId || record.sourceChange.path !== record.home.path) return result(fail('suspended', 'source-owner-handoff-missing'))
|
|
71
|
+
// Review roles in this adapter are repository-scoped. Conflicting resource
|
|
72
|
+
// stewards inside one repository cannot be collapsed into one approval.
|
|
73
|
+
if (!crossRepo && endpoints[0].stewardId !== endpoints[1].stewardId) return result(fail('suspended', 'endpoint-steward-conflict'))
|
|
74
|
+
const required = [{ repoId: record.home.repoId, role: 'home-owner', actorId: home.actorId },
|
|
75
|
+
...[...new Map(endpoints.map((e) => [e.ref.repoId, { repoId: e.ref.repoId, role: 'endpoint-steward', actorId: e.stewardId }])).values()]]
|
|
76
|
+
if (crossRepo && endpoints[0].stewardId === endpoints[1].stewardId && !policy.allowSharedSteward) return result(fail('suspended', 'shared-steward-not-delegated'))
|
|
77
|
+
let previous = null, state = 'draft', proposer = null, lastTime = -Infinity
|
|
78
|
+
const approvals = new Map()
|
|
79
|
+
for (const [index, event] of events.entries()) {
|
|
80
|
+
if (event.sequence !== index + 1 || event.previousDigest !== previous || event.recordDigest !== identity.recordDigest || event.policyDigest !== policyDigest || Date.parse(event.at) < lastTime || Date.parse(event.at) > now) return result(fail('suspended', 'decision-chain-invalid'))
|
|
81
|
+
if (['withdrawn', 'rejected', 'superseded', 'suspended'].includes(state)) return result(fail('suspended', 'terminal-history-continued'))
|
|
82
|
+
const binding = await host.resolveActorKey({ actorId: event.actorId, keyId: event.signature.keyId, at: host.now })
|
|
83
|
+
if (!binding || binding.actorId !== event.actorId || binding.revoked !== false || !Number.isFinite(Date.parse(binding.notBefore)) || !Number.isFinite(Date.parse(binding.expiresAt)) ||
|
|
84
|
+
Date.parse(binding.notBefore) > Date.parse(event.at) || Date.parse(binding.expiresAt) <= now || !verifyDocument(event, { publicKey: binding.publicKey }).valid) return result(fail('suspended', 'actor-key-unavailable'))
|
|
85
|
+
const grant = await host.authorizeDecision({ event, record, required, policyDigest })
|
|
86
|
+
if (!grant || grant.eventDigest !== edgeDigest(event) || grant.policyDigest !== policyDigest || grant.bindingId !== host.bindingId || grant.allowed !== true || grant.revoked !== false ||
|
|
87
|
+
!Number.isFinite(Date.parse(grant.notBefore)) || Date.parse(grant.notBefore) > Date.parse(event.at) || !Number.isFinite(Date.parse(grant.expiresAt)) || Date.parse(grant.expiresAt) <= now) return result(fail('suspended', 'decision-grant-unavailable'))
|
|
88
|
+
if (event.action === 'propose') {
|
|
89
|
+
if (state !== 'draft') return result(fail('suspended', 'invalid-transition'))
|
|
90
|
+
proposer = event.actorId; state = 'awaiting-endpoint-review'
|
|
91
|
+
} else if (event.action === 'approve') {
|
|
92
|
+
if (state !== 'awaiting-endpoint-review' || (!policy.allowProposerReview && event.actorId === proposer)) return result(fail('suspended', 'invalid-reviewer'))
|
|
93
|
+
for (const role of event.roles) {
|
|
94
|
+
const match = required.find((r) => roleKey(r) === roleKey(role) && r.actorId === event.actorId)
|
|
95
|
+
if (!match) return result(fail('suspended', 'invalid-reviewer'))
|
|
96
|
+
approvals.set(roleKey(role), edgeDigest(event))
|
|
97
|
+
}
|
|
98
|
+
} else if (event.action === 'accept') {
|
|
99
|
+
if (state !== 'awaiting-endpoint-review' || event.actorId !== home.actorId || required.some((r) => !approvals.has(roleKey(r)))) return result(fail('suspended', 'missing-reviewer'))
|
|
100
|
+
state = 'active'
|
|
101
|
+
} else {
|
|
102
|
+
if (state === 'draft' || ![home.actorId, proposer, ...endpoints.map((e) => e.stewardId)].includes(event.actorId)) return result(fail('suspended', 'invalid-transition'))
|
|
103
|
+
state = { reject: 'rejected', suspend: 'suspended', withdraw: 'withdrawn', supersede: 'superseded' }[event.action]
|
|
104
|
+
}
|
|
105
|
+
previous = edgeDigest(event); lastTime = Date.parse(event.at)
|
|
106
|
+
}
|
|
107
|
+
return result({ state, eligible: state === 'active', assurance: state === 'active' ? 'reviewed-assertion' : 'pending', reviewRefs: [...new Set(approvals.values())].sort() })
|
|
108
|
+
} catch {
|
|
109
|
+
// Port errors must not disclose hidden paths, actor details or source bytes.
|
|
110
|
+
return immutable(fail('suspended', 'verification-unavailable'))
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Input results must already be authorized for one recipient and evaluated in
|
|
115
|
+
// the same host snapshot. Preserve each complete assertion and its supports.
|
|
116
|
+
export function groupEligibleEdges(results) {
|
|
117
|
+
const groups = new Map()
|
|
118
|
+
let binding = null
|
|
119
|
+
for (const result of results) {
|
|
120
|
+
if (!decisions.has(result)) throw new Error('unverified edge decision')
|
|
121
|
+
if (!result.eligible || result.state !== 'active') continue
|
|
122
|
+
const current = edgeDigest({ bindingId: result.verification.bindingId, policyDigest: result.policyDigest })
|
|
123
|
+
if (binding !== null && binding !== current) throw new Error('mixed edge decision bindings')
|
|
124
|
+
binding = current
|
|
125
|
+
const key = edgeDigest({ family: result.family, triple: result.triple, qualifications: result.qualifications })
|
|
126
|
+
const group = groups.get(key) ?? { groupId: `edge-group:${key.slice(7)}`, family: result.family, triple: result.triple, assertions: [] }
|
|
127
|
+
if (group.assertions.some((a) => a.edgeId === result.edgeId && edgeDigest(a) !== edgeDigest(result))) throw new Error('conflicting edge decisions')
|
|
128
|
+
if (!group.assertions.some((a) => a.edgeId === result.edgeId)) group.assertions.push(structuredClone(result))
|
|
129
|
+
groups.set(key, group)
|
|
130
|
+
}
|
|
131
|
+
return immutable([...groups.values()].map((g) => ({ ...g, assertions: g.assertions.sort((a, b) => a.edgeId.localeCompare(b.edgeId, 'en')) })).sort((a, b) => a.groupId.localeCompare(b.groupId, 'en')))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Only evaluated decisions may enter this seam; a deserialized eligible flag is
|
|
135
|
+
// never authority. The access host revalidates bindingId/current grants at use.
|
|
136
|
+
export function displayEligibleEdge(result) {
|
|
137
|
+
if (!decisions.has(result) || !result.eligible || result.state !== 'active') return null
|
|
138
|
+
return immutable({ edgeId: result.edgeId, source: result.triple.subject, target: result.triple.object,
|
|
139
|
+
predicate: result.triple.predicate, family: result.family, acceptanceState: result.state, assurance: result.assurance,
|
|
140
|
+
supportRefs: result.supportRefs.map((s) => s.id), sourceRef: result.supportRefs[0].id, reviewRefs: result.reviewRefs,
|
|
141
|
+
qualifications: result.qualifications, recordDigest: result.recordDigest, policyDigest: result.policyDigest, verification: result.verification })
|
|
142
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { canonicalize } from '../attestation/jcs.mjs'
|
|
2
|
+
import { validateRepositoryContribution } from '../graph/repo-contribution.mjs'
|
|
3
|
+
import { validSourceRef } from './contract.mjs'
|
|
4
|
+
|
|
5
|
+
const need = (value) => { if (!value) throw new TypeError('Invalid source citation input') }
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert an exact observed occurrence to the policy's raw-byte citation shape.
|
|
9
|
+
* This pure structural binding does not authenticate source bytes, source-home
|
|
10
|
+
* custody, freshness or a declared triple. The policy host must still implement
|
|
11
|
+
* readSource, verifyOrigin and checkSourceHome against its current snapshot.
|
|
12
|
+
* Original provenance, including UTF16 offsets and any pointer/target range,
|
|
13
|
+
* stays unchanged in the contribution. Only its outer raw-byte range is cited.
|
|
14
|
+
* @param {import('../graph/repo-contribution.types.mjs').RepositoryContribution} contribution
|
|
15
|
+
* @param {import('../graph/repo-contribution.types.mjs').SourceRef} provenance
|
|
16
|
+
*/
|
|
17
|
+
export function sourceCitationFromProvenance(contribution, provenance) {
|
|
18
|
+
try {
|
|
19
|
+
need(validateRepositoryContribution(contribution).ok)
|
|
20
|
+
const { snapshot, repository } = contribution
|
|
21
|
+
need(provenance?.repoId === repository.repoId && provenance.revision === snapshot.digest)
|
|
22
|
+
const file = snapshot.files.find((pin) => pin.path === provenance.path)
|
|
23
|
+
need(file && provenance.digest === file.rawDigest)
|
|
24
|
+
const { range, rangeDigest } = provenance.locator ?? {}
|
|
25
|
+
need(range && Number.isSafeInteger(range.byteStart) && range.byteStart >= 0 &&
|
|
26
|
+
Number.isSafeInteger(range.byteEnd) && range.byteEnd > range.byteStart && range.byteEnd <= file.byteLength)
|
|
27
|
+
// A matching file pin alone cannot invent a source occurrence. Accept a
|
|
28
|
+
// detached copy too, while retaining every field of the recorded provenance.
|
|
29
|
+
const observed = canonicalize(provenance)
|
|
30
|
+
need([...contribution.ordinaryLinks, ...contribution.declaredRelations]
|
|
31
|
+
.some((occurrence) => canonicalize(occurrence.provenance) === observed))
|
|
32
|
+
const citation = {
|
|
33
|
+
repoId: provenance.repoId, path: provenance.path, revision: provenance.revision,
|
|
34
|
+
fileDigest: provenance.digest,
|
|
35
|
+
locator: { byteStart: range.byteStart, byteEnd: range.byteEnd },
|
|
36
|
+
citedDigest: rangeDigest,
|
|
37
|
+
}
|
|
38
|
+
need(validSourceRef(citation))
|
|
39
|
+
return citation
|
|
40
|
+
} catch {
|
|
41
|
+
throw new TypeError('Invalid source citation input')
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -468,7 +468,8 @@ export function loadExtensionPacks(project, { report = false, verifyLock = true
|
|
|
468
468
|
// Explicit registry object, no global state. Bundled resolution keeps its
|
|
469
469
|
// existing id | slug | namespaced affordance; pack protocols resolve by full
|
|
470
470
|
// namespaced id only, so a pack can never shadow or squat a bundled slug.
|
|
471
|
-
export function createProtocolRegistry({ packs = [] } = {}) {
|
|
471
|
+
export function createProtocolRegistry({ packs = [], includeBundled = true } = {}) {
|
|
472
|
+
if (typeof includeBundled !== 'boolean') throw new TypeError('includeBundled must be a boolean')
|
|
472
473
|
const packProtocols = new Map()
|
|
473
474
|
for (const pack of packs) {
|
|
474
475
|
for (const record of pack.protocols ?? []) packProtocols.set(record.id, record)
|
|
@@ -476,14 +477,14 @@ export function createProtocolRegistry({ packs = [] } = {}) {
|
|
|
476
477
|
return {
|
|
477
478
|
packs,
|
|
478
479
|
protocolById(id) {
|
|
479
|
-
const bundled = getBundledReadinessProtocol(id)
|
|
480
|
+
const bundled = includeBundled ? getBundledReadinessProtocol(id) : null
|
|
480
481
|
if (bundled) return bundled
|
|
481
482
|
const record = packProtocols.get(id)
|
|
482
483
|
return record ? record.protocol : null
|
|
483
484
|
},
|
|
484
485
|
listProtocols() {
|
|
485
486
|
return [
|
|
486
|
-
...bundledReadinessProtocols,
|
|
487
|
+
...(includeBundled ? bundledReadinessProtocols : []),
|
|
487
488
|
...[...packProtocols.values()].map((record) => record.protocol),
|
|
488
489
|
]
|
|
489
490
|
},
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { canonicalJson, exact, ExtensionError } from './contracts.mjs'
|
|
2
|
+
|
|
3
|
+
function joinBytes(chunks, size) {
|
|
4
|
+
const bytes = new Uint8Array(size)
|
|
5
|
+
let offset = 0
|
|
6
|
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength }
|
|
7
|
+
return bytes
|
|
8
|
+
}
|
|
9
|
+
async function cancelResponseBody(body) {
|
|
10
|
+
if (!body) return
|
|
11
|
+
let timer
|
|
12
|
+
try {
|
|
13
|
+
await Promise.race([
|
|
14
|
+
Promise.resolve().then(() => body.cancel()).catch(() => {}),
|
|
15
|
+
new Promise(resolve => { timer = setTimeout(resolve, 100) }),
|
|
16
|
+
])
|
|
17
|
+
} finally { clearTimeout(timer) }
|
|
18
|
+
}
|
|
19
|
+
export async function readInvocationBody(request) {
|
|
20
|
+
const body = request.body?.getReader()
|
|
21
|
+
if (!body) throw new ExtensionError('invalid-input')
|
|
22
|
+
try {
|
|
23
|
+
let size = 0
|
|
24
|
+
const chunks = []
|
|
25
|
+
for (;;) {
|
|
26
|
+
const { value, done } = await body.read()
|
|
27
|
+
if (done) break
|
|
28
|
+
size += value.byteLength
|
|
29
|
+
if (size > 65536) throw new ExtensionError('invalid-input')
|
|
30
|
+
chunks.push(value)
|
|
31
|
+
}
|
|
32
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(joinBytes(chunks, size)))
|
|
33
|
+
} finally { await body.cancel().catch(() => {}) }
|
|
34
|
+
}
|
|
35
|
+
export function createRootAdmissionConsumer(options) {
|
|
36
|
+
return createAdmissionConsumer(options, options.allowLocalTesting && ['test', 'development'].includes(process.env.NODE_ENV))
|
|
37
|
+
}
|
|
38
|
+
export function createAdmissionConsumer({ origin, fetch: hostFetch = globalThis.fetch }, localTestingAuthorized = false) {
|
|
39
|
+
const target = new URL(origin)
|
|
40
|
+
const local = localTestingAuthorized &&
|
|
41
|
+
target.protocol === 'http:' && ['127.0.0.1', '[::1]'].includes(target.hostname)
|
|
42
|
+
if ((target.protocol !== 'https:' && !local) || target.username || target.password || target.pathname !== '/' || target.search || target.hash) throw new ExtensionError('invalid-input')
|
|
43
|
+
const endpoint = new URL('/api/root-runtime/extensions/admissions/consume', target)
|
|
44
|
+
return async (_claims, delegation) => {
|
|
45
|
+
// Convex supports AbortController and action timers, but not every runtime
|
|
46
|
+
// implements AbortSignal.timeout. The deadline includes the response body.
|
|
47
|
+
const controller = new AbortController()
|
|
48
|
+
let timer, response, reader
|
|
49
|
+
const expired = new Promise((_, reject) => {
|
|
50
|
+
timer = setTimeout(() => { controller.abort(); reject(new ExtensionError('unavailable', 503)) }, 5000)
|
|
51
|
+
})
|
|
52
|
+
const consume = async () => {
|
|
53
|
+
response = await hostFetch(endpoint, { method: 'POST', redirect: 'error', signal: controller.signal,
|
|
54
|
+
headers: { 'content-type': 'application/json' }, body: canonicalJson({ delegation }) })
|
|
55
|
+
if (response.status === 401 || response.status === 403) return false
|
|
56
|
+
if (response.status != 200 || response.headers.get('content-type')?.split(';')[0] !== 'application/json') throw new ExtensionError('unavailable', 503)
|
|
57
|
+
reader = response.body?.getReader()
|
|
58
|
+
if (!reader) throw new ExtensionError('unavailable', 503)
|
|
59
|
+
try {
|
|
60
|
+
let size = 0
|
|
61
|
+
const chunks = []
|
|
62
|
+
for (;;) {
|
|
63
|
+
const { value, done } = await reader.read()
|
|
64
|
+
if (done) break
|
|
65
|
+
size += value.byteLength
|
|
66
|
+
if (size > 1024) throw new ExtensionError('unavailable', 503)
|
|
67
|
+
chunks.push(value)
|
|
68
|
+
}
|
|
69
|
+
const body = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(joinBytes(chunks, size)))
|
|
70
|
+
exact(body, ['admitted'])
|
|
71
|
+
if (body.admitted !== true) throw new ExtensionError('unavailable', 503)
|
|
72
|
+
return true
|
|
73
|
+
} catch { throw new ExtensionError('unavailable', 503) }
|
|
74
|
+
}
|
|
75
|
+
try { return await Promise.race([consume(), expired]) } finally {
|
|
76
|
+
clearTimeout(timer)
|
|
77
|
+
// Also close refusal/error responses, whose body reader was never acquired.
|
|
78
|
+
// Abort first; a broken/custom stream cancellation cannot extend transport.
|
|
79
|
+
controller.abort()
|
|
80
|
+
await cancelResponseBody(reader ?? response?.body)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ExtensionReleaseBundle, ExtensionReleaseRegistration, ExtensionManifest, InstallationBinding, Invocation, Json, OperationReceipt } from './contracts.mjs'
|
|
2
|
+
export interface HostTransport {
|
|
3
|
+
getInstallation(id: string): Promise<InstallationBinding>;
|
|
4
|
+
invoke(request: Invocation): Promise<OperationReceipt>;
|
|
5
|
+
lookupOperation(id: string, key: string, action: string): Promise<OperationReceipt | null>;
|
|
6
|
+
recoverOperation(binding: InstallationBinding, key: string, action: string): Promise<OperationReceipt>;
|
|
7
|
+
recordResult(operationId: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export interface AdminHostTransport {
|
|
10
|
+
registerRelease?(bundle: ExtensionReleaseBundle): Promise<ExtensionReleaseRegistration>;
|
|
11
|
+
configure(manifest: ExtensionManifest, connectionRef: string, environment: 'development' | 'staging' | 'production'): Promise<InstallationBinding>;
|
|
12
|
+
transition(binding: InstallationBinding, transition: 'verify' | 'enable' | 'disable' | 'remove'): Promise<InstallationBinding>;
|
|
13
|
+
upgrade(binding: InstallationBinding, manifest: ExtensionManifest): Promise<InstallationBinding>;
|
|
14
|
+
}
|
|
15
|
+
export function createHttpHostTransport(hostFetch: typeof fetch): HostTransport & AdminHostTransport
|
|
16
|
+
export function createExtensionClient(transport: HostTransport): {
|
|
17
|
+
getInstallation(id: string): Promise<InstallationBinding>;
|
|
18
|
+
invoke(binding: InstallationBinding, actionId: string, input: Json, idempotencyKey: string): Promise<OperationReceipt>;
|
|
19
|
+
lookupOperation(id: string, key: string, action: string): Promise<OperationReceipt | null>;
|
|
20
|
+
recoverOperation(binding: InstallationBinding, key: string, action: string): Promise<OperationReceipt>;
|
|
21
|
+
recordResult(operationId: string): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export function createExtensionAdminClient(transport: AdminHostTransport): Omit<AdminHostTransport, 'configure' | 'registerRelease'> & { registerRelease(bundle: ExtensionReleaseBundle): Promise<ExtensionReleaseRegistration>; configure(manifest: ExtensionManifest, connectionRef: string, environment?: 'development' | 'staging' | 'production'): Promise<InstallationBinding> }
|