@sentropic/artifacts 0.2.3 → 0.2.5
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 +15 -0
- package/README.md +101 -1
- package/dist/adapters/postgres-activation-vectors.d.ts +31 -0
- package/dist/adapters/postgres-activation-vectors.js +178 -0
- package/dist/adapters/postgres-management.d.ts +4 -0
- package/dist/adapters/postgres-management.js +36 -0
- package/dist/adapters/postgres.js +19 -4
- package/dist/http.d.ts +9 -0
- package/dist/http.js +56 -7
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mcp-core.d.ts +2 -2
- package/dist/mcp-core.js +11 -2
- package/dist/profile/index.js +7 -1
- package/dist/profile/schemas.js +11 -4
- package/dist/profile/validation.js +5 -1
- package/dist/server/access.d.ts +6 -0
- package/dist/server/command-identity.d.ts +3 -2
- package/dist/server/command-identity.js +6 -6
- package/dist/server/commit/coordinator.js +1 -1
- package/dist/server/failures.d.ts +5 -2
- package/dist/server/failures.js +4 -1
- package/dist/server/lifecycle.d.ts +2 -0
- package/dist/server/lifecycle.js +12 -0
- package/dist/server/management.d.ts +45 -0
- package/dist/server/management.js +79 -0
- package/dist/server/outcome-projection.js +4 -2
- package/dist/server/publication.d.ts +4 -10
- package/dist/server/publication.js +2 -2
- package/dist/server/source-read.d.ts +38 -0
- package/dist/server/source-read.js +68 -0
- package/dist/server/summary.d.ts +39 -0
- package/dist/server/summary.js +17 -0
- package/migrations/0004_terminal_controls.sql +47 -0
- package/package.json +18 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.5
|
|
4
|
+
|
|
5
|
+
- D1: Read verified author manifests and file bytes for the current or a selected
|
|
6
|
+
release, with path selection, source authorization, bounded snapshots and
|
|
7
|
+
separate provenance. Reject purged releases and mismatched record digests.
|
|
8
|
+
- D2: Grant or revoke named read access and delete artifacts terminally with
|
|
9
|
+
host-approved confirmation, transactional receipts and idempotent replay.
|
|
10
|
+
Mask artifacts outside the caller's visibility as `not_found`.
|
|
11
|
+
- D4: Enrich get/list results with binding names, dates, current release IDs,
|
|
12
|
+
digests, stable/pinned URLs, permissions and available optional metadata.
|
|
13
|
+
|
|
14
|
+
Apply `0004_terminal_controls.sql` after migrations 0001–0003 before enabling
|
|
15
|
+
the management capabilities.
|
package/README.md
CHANGED
|
@@ -76,7 +76,69 @@ Reconcile current authority independently of the restored ACL snapshot. Fresh
|
|
|
76
76
|
authorized sessions create fresh flows; revoked readers stay denied. Rollback
|
|
77
77
|
disables activation while retaining the additive schema and accepted artifacts.
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
### Importable host conformance (UP-T-2, 0.2.4)
|
|
80
|
+
|
|
81
|
+
The packed package exports the framework-independent host suite and its stable
|
|
82
|
+
vector IDs. Install the SQL asset through your test migration executor first:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import assert from 'node:assert/strict';
|
|
86
|
+
import {
|
|
87
|
+
POSTGRES_ACTIVATION_VECTOR_IDS, runPostgresActivationConformance,
|
|
88
|
+
} from '@sentropic/artifacts/adapters/postgres-activation/vectors';
|
|
89
|
+
|
|
90
|
+
assert.equal(process.env.ACTIVATION_DISPOSABLE, '1');
|
|
91
|
+
const { rows: [{ databaseName }] } = await pool.query('SELECT current_database() AS "databaseName"');
|
|
92
|
+
assert.match(databaseName, /^test[_-][a-z0-9_-]+$/i); // Check before any schema setup/reset.
|
|
93
|
+
const passed = await runPostgresActivationConformance({
|
|
94
|
+
disposable: { confirmed: true, databaseName },
|
|
95
|
+
createStore: namespace => createPostgresActivationStore({ pool, namespace }),
|
|
96
|
+
inspectTicket, // Host query returning persisted issuedAt/expiresAt and current DB now, in milliseconds.
|
|
97
|
+
createStoreWithCommitAckLoss, // Host composition with acknowledgement loss AFTER real consume COMMIT.
|
|
98
|
+
wait: async (ms, client) => { await (client ?? pool).query('SELECT pg_sleep($1)', [ms / 1000]); },
|
|
99
|
+
onPass: id => console.log('activation vector passed:', id),
|
|
100
|
+
});
|
|
101
|
+
assert.deepEqual(passed, POSTGRES_ACTIVATION_VECTOR_IDS); // 16 vectors
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Supply your host's store composition in `createStore`, using the supplied namespace
|
|
105
|
+
and the same disposable PostgreSQL database/executor for every instance. Replica
|
|
106
|
+
instances must share durable state. `wait` uses database time and the supplied
|
|
107
|
+
leased client during authority checks, including when the pool has one connection.
|
|
108
|
+
`inspectTicket(namespace, ticketId)` reads `issued_ms`, `expires_ms` and
|
|
109
|
+
`floor(extract(epoch FROM clock_timestamp()) * 1000)` through that same executor.
|
|
110
|
+
`createStoreWithCommitAckLoss(namespace)` wraps the host's leased client: execute
|
|
111
|
+
COMMIT, then throw before returning its acknowledgement. Seed with the normal
|
|
112
|
+
store; inject only into the store returned by this hook. Both hooks are optional
|
|
113
|
+
in the interface for additive compatibility; their vectors always run and fail
|
|
114
|
+
if either hook is absent. A complete host fixture is in
|
|
115
|
+
`packages/cluster-mesh/tests/packaging/activation-vectors.spec.ts`.
|
|
116
|
+
Run in an isolated process: the clock-skew vector temporarily replaces `Date.now`
|
|
117
|
+
by +/-120 seconds and restores it even on failure. TypeScript hosts install
|
|
118
|
+
`@types/pg` alongside `pg` in their consumer project.
|
|
119
|
+
The runner throws with the failing vector ID and original cause; it returns all
|
|
120
|
+
16 IDs only after every assertion passes. It uses fresh namespaces and invalidates
|
|
121
|
+
and cleans its records after each vector. Namespace rows remain in the disposable
|
|
122
|
+
database. The runner refuses without explicit disposable confirmation and an
|
|
123
|
+
actual database name starting `test_` or `test-`. Check `current_database()` on the
|
|
124
|
+
host executor before installing/resetting the schema. Never use a serving database.
|
|
125
|
+
|
|
126
|
+
The vectors cover namespace isolation, immutable records, all nine exact binding
|
|
127
|
+
comparisons, wrong IDs, denial/outage preservation, single use, concurrent replicas,
|
|
128
|
+
expiry without cleanup and after authority waits, bounded cleanup, invalidation
|
|
129
|
+
and fresh recovery, the 60-second TTL cap, flow/ticket association, DB-time-only
|
|
130
|
+
issuance/expiry under app clock skew, and `ActivationCommitUnknownError` without
|
|
131
|
+
a grant followed by replay denial. This adds
|
|
132
|
+
only a testing export in 0.2.4; adapter behavior and SQL bytes are unchanged.
|
|
133
|
+
|
|
134
|
+
`make -f packages/cluster-mesh/packaging.mk test-lazy-package-activation ENV=test-artifacts-upt2`
|
|
135
|
+
packs and installs the candidate in a clean consumer, verifies the SQL hash, and
|
|
136
|
+
runs all 16 vectors on that consumer's PostgreSQL executor (pool max 1 and 4).
|
|
137
|
+
It requires explicit disposable configuration and checks the JSON result for
|
|
138
|
+
nine passing tests and zero skipped tests; missing CI wiring fails. Follow with
|
|
139
|
+
`make down ENV=test-artifacts-upt2` (use the shared Docker lock for both commands).
|
|
140
|
+
|
|
141
|
+
Additional fault/process conformance lives in `tests/postgres-activation.test.ts` and
|
|
80
142
|
`tests/postgres-activation.integration.test.ts`, with the independent process
|
|
81
143
|
witness `tests/fixtures/activation-process.mjs`. Run through
|
|
82
144
|
`make test-artifacts` and `make test-artifacts-integration` on an isolated test
|
|
@@ -355,3 +417,41 @@ publication, use the current package version and this bootstrap sequence
|
|
|
355
417
|
3. After that, merges to `main` touching the artifacts package publish
|
|
356
418
|
through the `publish-artifacts` job (`make publish-artifacts`, OIDC +
|
|
357
419
|
provenance), gated by `validate-artifacts`.
|
|
420
|
+
|
|
421
|
+
## Source and management capabilities
|
|
422
|
+
|
|
423
|
+
Version 0.3.0 introduces D1, D2 and D4; see [CHANGELOG.md](./CHANGELOG.md).
|
|
424
|
+
|
|
425
|
+
Hosts may explicitly select `artifacts_get_source`, `artifacts_set_access`, and
|
|
426
|
+
`artifacts_delete` in their compiled profile. Configure `get_source` with empty
|
|
427
|
+
limits/defaults, and `set_access`/`delete` with `idempotencyKeyMaxUnits: 256` and
|
|
428
|
+
empty defaults. Existing profile selections remain explicit.
|
|
429
|
+
|
|
430
|
+
`POST /get_source` accepts `artifactId`, optional `releaseId`, and optional unique
|
|
431
|
+
`paths` (1–32 entries). Bind `publication.getRelease` and `sources.getSource` on
|
|
432
|
+
the HTTP app. It returns the canonical manifest, verified author file bytes,
|
|
433
|
+
and separate nullable `sourceRef`; readers need the distinct source permission.
|
|
434
|
+
The snapshot is limited to 32 files and 262144 decoded bytes. Hosts must qualify
|
|
435
|
+
a 2 MiB serialized MCP result budget before selecting this capability.
|
|
436
|
+
|
|
437
|
+
`POST /set_access` grants or revokes a named principal's read access, requires a
|
|
438
|
+
caller `idempotencyKey`, and keeps the artifact private. Bind
|
|
439
|
+
`isApprovedAccount` to the host's principal policy. `POST /delete` also requires
|
|
440
|
+
`expectedCurrentReleaseId` (nullable) and a trusted `management.confirmation`
|
|
441
|
+
port. Its `request` method issues a challenge for the host management UI;
|
|
442
|
+
`consume` verifies the human-approved proof, expiration and full command context
|
|
443
|
+
on the supplied transaction. A caller boolean is never confirmation. Repeated
|
|
444
|
+
successful deletes return the stored receipt without consuming proof again.
|
|
445
|
+
|
|
446
|
+
Apply `migrations/0004_terminal_controls.sql` after `0001_artifacts.sql`,
|
|
447
|
+
`0002_lifecycle.sql` and `0003_coordinator.sql` before activation.
|
|
448
|
+
The default management writer uses the coordinator's PostgreSQL
|
|
449
|
+
transaction; alternative stores implement `management.createUnit` with the same
|
|
450
|
+
parent/ACL/current lock order and transactional effects. Terminal markers and
|
|
451
|
+
SQL guards prevent restoration and later publication. Deletion reports
|
|
452
|
+
`purgeState: "pending"`; physical purge belongs to host retention policy.
|
|
453
|
+
|
|
454
|
+
Get/list include binding, creation/modification dates, current release, digest,
|
|
455
|
+
host URLs and available metadata. Historical modification dates stay nullable.
|
|
456
|
+
The PostgreSQL projection reads the optional metadata column added by the
|
|
457
|
+
publication parity lane without requiring that column for older stores.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PoolClient } from 'pg';
|
|
2
|
+
import { type PostgresActivationStore } from './postgres-activation.js';
|
|
3
|
+
export declare const POSTGRES_ACTIVATION_VECTOR_IDS: readonly ["namespace-isolation", "immutable-records", "binding-mismatch", "wrong-ids", "authority-denial", "authority-outage", "single-use", "concurrent-consume", "expiry-without-cleanup", "expiry-after-authority", "bounded-cleanup", "invalidation-recovery", "ttl-cap", "flow-association", "db-time-skew", "commit-unknown"];
|
|
4
|
+
export type PostgresActivationVectorId = typeof POSTGRES_ACTIVATION_VECTOR_IDS[number];
|
|
5
|
+
export interface PostgresActivationConformanceHost {
|
|
6
|
+
/** Explicit opt-in; databaseName must be the executor's actual current_database(), starting test_ or test-. */
|
|
7
|
+
disposable?: {
|
|
8
|
+
confirmed: true;
|
|
9
|
+
databaseName: string;
|
|
10
|
+
};
|
|
11
|
+
/** Same disposable DB/executor for every namespace and replica; schema installed by the host. */
|
|
12
|
+
createStore(namespace: string): PostgresActivationStore;
|
|
13
|
+
/** Wait on DB time; use the supplied leased client during authority checks (supports max:1). */
|
|
14
|
+
wait(milliseconds: number, client?: PoolClient): Promise<void>;
|
|
15
|
+
/** Read persisted issuance/expiry and current DB milliseconds through the host executor. */
|
|
16
|
+
inspectTicket?(namespace: string, ticketId: string): Promise<{
|
|
17
|
+
issuedAt: number;
|
|
18
|
+
expiresAt: number;
|
|
19
|
+
now: number;
|
|
20
|
+
}>;
|
|
21
|
+
/** Same host composition, but throw AFTER the consume COMMIT succeeds, losing its acknowledgement. */
|
|
22
|
+
createStoreWithCommitAckLoss?(namespace: string): PostgresActivationStore;
|
|
23
|
+
onPass?(id: PostgresActivationVectorId): void;
|
|
24
|
+
}
|
|
25
|
+
/** Test-only, framework-independent host conformance. Throws on the first failing vector.
|
|
26
|
+
* Uses fresh isolated namespaces; invalidates and collects its own records after each vector.
|
|
27
|
+
* Process crashes and restore snapshots remain package integration tests.
|
|
28
|
+
* Optional hooks preserve interface compatibility; their vectors are mandatory and fail if absent.
|
|
29
|
+
* Run in an isolated process: db-time-skew temporarily replaces Date.now and always restores it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runPostgresActivationConformance(host: PostgresActivationConformanceHost): Promise<PostgresActivationVectorId[]>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { ActivationCommitUnknownError } from './postgres-activation.js';
|
|
4
|
+
export const POSTGRES_ACTIVATION_VECTOR_IDS = Object.freeze([
|
|
5
|
+
'namespace-isolation', 'immutable-records', 'binding-mismatch', 'wrong-ids',
|
|
6
|
+
'authority-denial', 'authority-outage', 'single-use', 'concurrent-consume',
|
|
7
|
+
'expiry-without-cleanup', 'expiry-after-authority', 'bounded-cleanup',
|
|
8
|
+
'invalidation-recovery', 'ttl-cap', 'flow-association',
|
|
9
|
+
'db-time-skew', 'commit-unknown',
|
|
10
|
+
]);
|
|
11
|
+
/** Test-only, framework-independent host conformance. Throws on the first failing vector.
|
|
12
|
+
* Uses fresh isolated namespaces; invalidates and collects its own records after each vector.
|
|
13
|
+
* Process crashes and restore snapshots remain package integration tests.
|
|
14
|
+
* Optional hooks preserve interface compatibility; their vectors are mandatory and fail if absent.
|
|
15
|
+
* Run in an isolated process: db-time-skew temporarily replaces Date.now and always restores it.
|
|
16
|
+
*/
|
|
17
|
+
export async function runPostgresActivationConformance(host) {
|
|
18
|
+
assert.ok(host.disposable?.confirmed === true, 'Explicit disposable database confirmation is required');
|
|
19
|
+
assert.match(host.disposable.databaseName, /^test[_-][a-z0-9_-]+$/i, 'A test database name is required');
|
|
20
|
+
const passed = [];
|
|
21
|
+
for (const id of POSTGRES_ACTIVATION_VECTOR_IDS) {
|
|
22
|
+
const namespace = `upt2:${randomUUID()}`;
|
|
23
|
+
const store = host.createStore(namespace);
|
|
24
|
+
const foreign = host.createStore(`${namespace}:other`);
|
|
25
|
+
const expected = { subjectId: 'alice', artifactId: 'a', releaseId: 'r', channel: 'stable',
|
|
26
|
+
nonceDigest: 'ab'.repeat(32), sessionId: 'session', profileDigest: 'cd'.repeat(32),
|
|
27
|
+
policyRevision: 'p1', targetOrigin: 'https://activation.example.test' };
|
|
28
|
+
async function seed(ttl = 50_000, target = store) {
|
|
29
|
+
assert.ok(target.tickets.issue, 'DB-time ticket issuance is required');
|
|
30
|
+
const ticket = await target.tickets.issue({ ...expected, ticketId: randomUUID() }, ttl);
|
|
31
|
+
const flow = { ...expected, ticketId: ticket.ticketId, flowId: randomUUID(), expiresAt: ticket.expiresAt };
|
|
32
|
+
await target.flows.put(flow);
|
|
33
|
+
return { ticket, flow, input: { ticketId: ticket.ticketId, flowId: flow.flowId, expected } };
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const { ticket, flow, input } = await seed();
|
|
37
|
+
const redeem = () => store.consume(input, async () => true);
|
|
38
|
+
const preserved = async () => {
|
|
39
|
+
assert.deepEqual(await store.tickets.peek(ticket.ticketId), ticket);
|
|
40
|
+
assert.deepEqual(await store.flows.peek(flow.flowId), flow);
|
|
41
|
+
};
|
|
42
|
+
if (id === 'namespace-isolation') {
|
|
43
|
+
assert.equal(await foreign.tickets.peek(ticket.ticketId), undefined);
|
|
44
|
+
assert.equal(await foreign.flows.peek(flow.flowId), undefined);
|
|
45
|
+
assert.equal(await foreign.consume(input, async () => true), undefined);
|
|
46
|
+
await foreign.tickets.remove(ticket.ticketId);
|
|
47
|
+
await preserved();
|
|
48
|
+
}
|
|
49
|
+
else if (id === 'immutable-records') {
|
|
50
|
+
await assert.rejects(store.tickets.put({ ...ticket, subjectId: 'mallory' }));
|
|
51
|
+
await assert.rejects(store.flows.put({ ...flow, sessionId: 'other' }));
|
|
52
|
+
await preserved();
|
|
53
|
+
}
|
|
54
|
+
else if (id === 'binding-mismatch') {
|
|
55
|
+
for (const key of Object.keys(expected)) {
|
|
56
|
+
const mismatch = { ...expected, [key]: key === 'channel' ? 'pinned' : 'different' };
|
|
57
|
+
assert.equal(await store.consume({ ...input, expected: mismatch }, async () => true), undefined, key);
|
|
58
|
+
}
|
|
59
|
+
await preserved();
|
|
60
|
+
assert.deepEqual(await redeem(), ticket);
|
|
61
|
+
}
|
|
62
|
+
else if (id === 'wrong-ids') {
|
|
63
|
+
for (const key of ['ticketId', 'flowId']) {
|
|
64
|
+
assert.equal(await store.consume({ ...input, [key]: randomUUID() }, async () => true), undefined);
|
|
65
|
+
}
|
|
66
|
+
await preserved();
|
|
67
|
+
}
|
|
68
|
+
else if (id === 'authority-denial') {
|
|
69
|
+
assert.equal(await store.consume(input, async () => false), undefined);
|
|
70
|
+
await preserved();
|
|
71
|
+
}
|
|
72
|
+
else if (id === 'authority-outage') {
|
|
73
|
+
const outage = new Error('conformance authority unavailable');
|
|
74
|
+
await assert.rejects(store.consume(input, async () => { throw outage; }), error => error === outage);
|
|
75
|
+
await preserved();
|
|
76
|
+
}
|
|
77
|
+
else if (id === 'single-use') {
|
|
78
|
+
assert.deepEqual(await redeem(), ticket);
|
|
79
|
+
assert.equal(await redeem(), undefined);
|
|
80
|
+
assert.equal(await store.flows.peek(flow.flowId), undefined);
|
|
81
|
+
}
|
|
82
|
+
else if (id === 'concurrent-consume') {
|
|
83
|
+
const replica = host.createStore(namespace);
|
|
84
|
+
const results = await Promise.all([redeem(), replica.consume(input, async () => true)]);
|
|
85
|
+
assert.deepEqual(results.filter(Boolean), [ticket]);
|
|
86
|
+
}
|
|
87
|
+
else if (id === 'expiry-without-cleanup' || id === 'expiry-after-authority') {
|
|
88
|
+
const expired = await seed(1000);
|
|
89
|
+
if (id === 'expiry-without-cleanup')
|
|
90
|
+
await host.wait(1200);
|
|
91
|
+
assert.equal(await store.consume(expired.input, async ({ client }) => {
|
|
92
|
+
await host.wait(1200, client);
|
|
93
|
+
return true;
|
|
94
|
+
}), undefined);
|
|
95
|
+
assert.equal(await store.tickets.peek(expired.ticket.ticketId), undefined);
|
|
96
|
+
assert.equal(await store.flows.peek(expired.flow.flowId), undefined);
|
|
97
|
+
}
|
|
98
|
+
else if (id === 'bounded-cleanup') {
|
|
99
|
+
for (let i = 0; i < 3; i++)
|
|
100
|
+
await seed(1000);
|
|
101
|
+
const other = await seed(50_000, foreign);
|
|
102
|
+
await host.wait(1200);
|
|
103
|
+
let total = 0;
|
|
104
|
+
for (let i = 0; i < 4; i++) {
|
|
105
|
+
const removed = await store.cleanup(2);
|
|
106
|
+
const count = removed.flows + removed.tickets;
|
|
107
|
+
assert.ok(count >= 0 && count <= 2);
|
|
108
|
+
total += count;
|
|
109
|
+
}
|
|
110
|
+
assert.equal(total, 6);
|
|
111
|
+
await preserved();
|
|
112
|
+
assert.deepEqual(await foreign.tickets.peek(other.ticket.ticketId), other.ticket);
|
|
113
|
+
}
|
|
114
|
+
else if (id === 'invalidation-recovery') {
|
|
115
|
+
await store.invalidate();
|
|
116
|
+
const replica = host.createStore(namespace);
|
|
117
|
+
assert.equal(await replica.consume(input, async () => true), undefined);
|
|
118
|
+
assert.equal(await replica.tickets.peek(ticket.ticketId), undefined);
|
|
119
|
+
assert.equal(await replica.flows.peek(flow.flowId), undefined);
|
|
120
|
+
const fresh = await seed();
|
|
121
|
+
assert.deepEqual(await replica.consume(fresh.input, async () => true), fresh.ticket);
|
|
122
|
+
}
|
|
123
|
+
else if (id === 'ttl-cap') {
|
|
124
|
+
for (const ttl of [0, 60_001]) {
|
|
125
|
+
await assert.rejects(store.tickets.issue({ ...expected, ticketId: randomUUID() }, ttl));
|
|
126
|
+
}
|
|
127
|
+
await seed(60_000);
|
|
128
|
+
}
|
|
129
|
+
else if (id === 'flow-association') {
|
|
130
|
+
await assert.rejects(store.flows.put({ ...flow, flowId: randomUUID(), subjectId: 'mallory' }));
|
|
131
|
+
await assert.rejects(store.flows.put({ ...flow, flowId: randomUUID(), expiresAt: ticket.expiresAt + 1 }));
|
|
132
|
+
}
|
|
133
|
+
else if (id === 'db-time-skew') {
|
|
134
|
+
assert.ok(host.inspectTicket, 'inspectTicket hook is required for db-time-skew');
|
|
135
|
+
const originalNow = Date.now;
|
|
136
|
+
try {
|
|
137
|
+
for (const skew of [-120_000, 120_000]) {
|
|
138
|
+
Date.now = () => originalNow() + skew;
|
|
139
|
+
const issued = await seed(60_000);
|
|
140
|
+
const db = await host.inspectTicket(namespace, issued.ticket.ticketId);
|
|
141
|
+
assert.equal(db.expiresAt - db.issuedAt, 60_000);
|
|
142
|
+
assert.equal(issued.ticket.expiresAt, db.expiresAt);
|
|
143
|
+
assert.ok(db.expiresAt - db.now > 59_000 && db.expiresAt - db.now <= 60_000);
|
|
144
|
+
assert.deepEqual(await store.consume(issued.input, async () => true), issued.ticket);
|
|
145
|
+
const expired = await seed(50);
|
|
146
|
+
await host.wait(80);
|
|
147
|
+
assert.equal(await store.consume(expired.input, async () => true), undefined);
|
|
148
|
+
assert.equal(await store.tickets.peek(expired.ticket.ticketId), undefined);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
Date.now = originalNow;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else if (id === 'commit-unknown') {
|
|
156
|
+
assert.ok(host.createStoreWithCommitAckLoss, 'createStoreWithCommitAckLoss hook is required for commit-unknown');
|
|
157
|
+
const uncertain = host.createStoreWithCommitAckLoss(namespace);
|
|
158
|
+
const grants = [];
|
|
159
|
+
await assert.rejects(uncertain.consume(input, async () => true).then(result => grants.push(result)), error => error instanceof ActivationCommitUnknownError);
|
|
160
|
+
assert.deepEqual(grants, []);
|
|
161
|
+
assert.equal(await host.createStore(namespace).consume(input, async () => true), undefined);
|
|
162
|
+
assert.equal(await store.flows.peek(flow.flowId), undefined);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (cause) {
|
|
166
|
+
throw new Error(`UP-T conformance failed: ${id}`, { cause });
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
for (const target of [store, foreign]) {
|
|
170
|
+
await target.invalidate();
|
|
171
|
+
await target.cleanup(1000);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
passed.push(id);
|
|
175
|
+
host.onPass?.(id);
|
|
176
|
+
}
|
|
177
|
+
return passed;
|
|
178
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FinalizeTxClient } from '../server/commit/types.js';
|
|
2
|
+
import type { ManagementUnit } from '../server/management.js';
|
|
3
|
+
/** Parent lock serializes ACL inserts, lifecycle changes and publication FKs. */
|
|
4
|
+
export declare function createPostgresManagementUnit(client: FinalizeTxClient): ManagementUnit;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Parent lock serializes ACL inserts, lifecycle changes and publication FKs. */
|
|
2
|
+
export function createPostgresManagementUnit(client) {
|
|
3
|
+
return {
|
|
4
|
+
async lock(id) {
|
|
5
|
+
await client.query('SELECT id FROM artifacts.artifacts WHERE id = $1 FOR UPDATE', [id]);
|
|
6
|
+
await client.query('SELECT principal_id FROM artifacts.artifact_access WHERE artifact_id = $1 ORDER BY principal_id FOR UPDATE', [id]);
|
|
7
|
+
await client.query('SELECT release_id FROM artifacts.artifact_current WHERE artifact_id = $1 FOR UPDATE', [id]);
|
|
8
|
+
},
|
|
9
|
+
async read(id) {
|
|
10
|
+
const { rows } = await client.query(`SELECT a.owner_id, a.storage_scope_id, a.lifecycle_state, a.is_public, a.terminal_deleted_at, c.release_id
|
|
11
|
+
FROM artifacts.artifacts a LEFT JOIN artifacts.artifact_current c ON c.artifact_id = a.id WHERE a.id = $1`, [id]);
|
|
12
|
+
const row = rows[0];
|
|
13
|
+
if (!row)
|
|
14
|
+
return undefined;
|
|
15
|
+
const acl = { ownerId: row.owner_id, storageScopeId: row.storage_scope_id,
|
|
16
|
+
isPublic: row.is_public, withdrawn: row.lifecycle_state === 'withdrawn', grants: [] };
|
|
17
|
+
return { acl, state: row.lifecycle_state, terminal: row.terminal_deleted_at !== null,
|
|
18
|
+
currentReleaseId: row.release_id };
|
|
19
|
+
},
|
|
20
|
+
async setAccess(id, principalId, action, updatedAt) {
|
|
21
|
+
const changed = action === 'grant' ? await client.query(`INSERT INTO artifacts.artifact_access (artifact_id, principal_id, permission) VALUES ($1, $2, 'read')
|
|
22
|
+
ON CONFLICT (artifact_id, principal_id) DO NOTHING`, [id, principalId])
|
|
23
|
+
: await client.query('DELETE FROM artifacts.artifact_access WHERE artifact_id = $1 AND principal_id = $2', [id, principalId]);
|
|
24
|
+
await client.query(`UPDATE artifacts.artifacts SET is_public = false, updated_at = $2
|
|
25
|
+
WHERE id = $1 AND ($3::boolean OR is_public)`, [id, updatedAt, changed.rowCount > 0]);
|
|
26
|
+
const result = await client.query('SELECT principal_id, permission FROM artifacts.artifact_access WHERE artifact_id = $1 ORDER BY principal_id', [id]);
|
|
27
|
+
const stamp = await client.query('SELECT updated_at FROM artifacts.artifacts WHERE id = $1', [id]);
|
|
28
|
+
return { grants: result.rows.map((row) => ({ principalId: row.principal_id, grant: row.permission })),
|
|
29
|
+
updatedAt: stamp.rows[0]?.updated_at?.toISOString() ?? null };
|
|
30
|
+
},
|
|
31
|
+
async delete(id, deletedAt) {
|
|
32
|
+
await client.query(`UPDATE artifacts.artifacts SET lifecycle_state = 'deleted', terminal_deleted_at = $2,
|
|
33
|
+
updated_at = $2, is_public = false WHERE id = $1`, [id, deletedAt]);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ArtifactOperationError } from '../server/failures.js';
|
|
1
2
|
/** Read-only migration inventory. Never promotes a legacy success into an authorized receipt. */
|
|
2
3
|
export function createPostgresLegacyArtifactRecords(pool) {
|
|
3
4
|
return {
|
|
@@ -34,8 +35,10 @@ export function createPostgresArtifactStore(pool) {
|
|
|
34
35
|
async ensureArtifact(input) {
|
|
35
36
|
const where = `WHERE storage_scope_id = $1 AND owner_id = $2 AND binding_name = $3`;
|
|
36
37
|
const args = [input.storageScopeId, input.ownerId, input.bindingName];
|
|
37
|
-
const found = await pool.query(`SELECT id FROM artifacts.artifacts ${where}`, args);
|
|
38
|
+
const found = await pool.query(`SELECT id, (to_jsonb(a)->>'terminal_deleted_at') IS NOT NULL AS terminal FROM artifacts.artifacts a ${where}`, args);
|
|
38
39
|
if (found.rowCount !== 0 && found.rows[0] !== undefined) {
|
|
40
|
+
if (found.rows[0].terminal)
|
|
41
|
+
throw new ArtifactOperationError('gone');
|
|
39
42
|
return { artifactId: found.rows[0].id, created: false };
|
|
40
43
|
}
|
|
41
44
|
const inserted = await pool.query(`INSERT INTO artifacts.artifacts (id, storage_scope_id, owner_id, binding_name, profile)
|
|
@@ -44,10 +47,12 @@ export function createPostgresArtifactStore(pool) {
|
|
|
44
47
|
if (inserted.rowCount !== 0 && inserted.rows[0] !== undefined) {
|
|
45
48
|
return { artifactId: inserted.rows[0].id, created: true };
|
|
46
49
|
}
|
|
47
|
-
const raced = await pool.query(`SELECT id FROM artifacts.artifacts ${where}`, args);
|
|
50
|
+
const raced = await pool.query(`SELECT id, (to_jsonb(a)->>'terminal_deleted_at') IS NOT NULL AS terminal FROM artifacts.artifacts a ${where}`, args);
|
|
48
51
|
const row = raced.rows[0];
|
|
49
52
|
if (row === undefined)
|
|
50
53
|
throw new Error(`artifact binding lost for ${input.bindingName}`);
|
|
54
|
+
if (row.terminal)
|
|
55
|
+
throw new ArtifactOperationError('gone');
|
|
51
56
|
return { artifactId: row.id, created: false };
|
|
52
57
|
},
|
|
53
58
|
async record(kind, payload) {
|
|
@@ -185,14 +190,17 @@ export function createPostgresArtifactStore(pool) {
|
|
|
185
190
|
return rows.map((row) => row.digest);
|
|
186
191
|
},
|
|
187
192
|
async getArtifactAcl(artifactId) {
|
|
188
|
-
const { rows } = await pool.query(`SELECT owner_id, storage_scope_id, is_public, lifecycle_state
|
|
189
|
-
|
|
193
|
+
const { rows } = await pool.query(`SELECT owner_id, storage_scope_id, is_public, lifecycle_state, binding_name, created_at,
|
|
194
|
+
to_jsonb(a)->>'updated_at' AS updated_at, to_jsonb(a)->'metadata' AS metadata
|
|
195
|
+
FROM artifacts.artifacts a WHERE id = $1`, [artifactId]);
|
|
190
196
|
const row = rows[0];
|
|
191
197
|
if (row === undefined)
|
|
192
198
|
return undefined;
|
|
193
199
|
const grants = await pool.query(`SELECT principal_id, permission FROM artifacts.artifact_access
|
|
194
200
|
WHERE artifact_id = $1 ORDER BY principal_id`, [artifactId]);
|
|
195
201
|
return {
|
|
202
|
+
bindingName: row.binding_name, createdAt: row.created_at.toISOString(), updatedAt: row.updated_at,
|
|
203
|
+
...(row.metadata ?? {}),
|
|
196
204
|
ownerId: row.owner_id,
|
|
197
205
|
storageScopeId: row.storage_scope_id,
|
|
198
206
|
isPublic: row.is_public,
|
|
@@ -263,6 +271,13 @@ export function createPostgresArtifactStore(pool) {
|
|
|
263
271
|
const { rows } = await pool.query(`SELECT lifecycle_state FROM artifacts.artifacts WHERE id = $1`, [artifactId]);
|
|
264
272
|
return rows[0]?.lifecycle_state ?? 'active';
|
|
265
273
|
},
|
|
274
|
+
async isTerminal(artifactId) {
|
|
275
|
+
const { rows } = await pool.query("SELECT (to_jsonb(a)->>'terminal_deleted_at') IS NOT NULL AS terminal FROM artifacts.artifacts a WHERE id = $1", [artifactId]);
|
|
276
|
+
return rows[0]?.terminal === true;
|
|
277
|
+
},
|
|
278
|
+
async markTerminal(artifactId) {
|
|
279
|
+
await pool.query("UPDATE artifacts.artifacts SET terminal_deleted_at = coalesce(terminal_deleted_at, now()), lifecycle_state = 'deleted' WHERE id = $1", [artifactId]);
|
|
280
|
+
},
|
|
266
281
|
async setArtifactState(artifactId, state) {
|
|
267
282
|
const updated = await pool.query(`UPDATE artifacts.artifacts SET lifecycle_state = $2 WHERE id = $1`, [artifactId, state]);
|
|
268
283
|
if ((updated.rowCount ?? 0) === 0)
|
package/dist/http.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ import type { AclStore } from './server/access.js';
|
|
|
7
7
|
import type { LifecycleService } from './server/lifecycle.js';
|
|
8
8
|
import { type ArtifactInvocationContext, type ArtifactCommitCoordinator } from './server/commit/types.js';
|
|
9
9
|
import { type ArtifactOutcome, type ArtifactHostFailureMapper } from './server/failures.js';
|
|
10
|
+
import { type ManagementUnit, type ArtifactConfirmationPort } from './server/management.js';
|
|
11
|
+
import type { FinalizeTxClient } from './server/commit/types.js';
|
|
12
|
+
import type { SourceStore } from './server/sources.js';
|
|
10
13
|
export { projectArtifactHttpOutcome } from './server/outcome-projection.js';
|
|
11
14
|
export interface HttpRequest {
|
|
12
15
|
method: string;
|
|
@@ -37,6 +40,12 @@ export interface HttpAppDeps {
|
|
|
37
40
|
ensureHost: GuardedEnsureHost;
|
|
38
41
|
publication: Omit<GuardedPublicationDeps, 'coordinator' | 'limits'> & {
|
|
39
42
|
getCurrent: PublicationService['getCurrent'];
|
|
43
|
+
getRelease?: PublicationService['getRelease'];
|
|
44
|
+
};
|
|
45
|
+
sources?: Pick<SourceStore, 'getSource'>;
|
|
46
|
+
management?: {
|
|
47
|
+
createUnit?: (client: FinalizeTxClient) => ManagementUnit;
|
|
48
|
+
confirmation?: ArtifactConfirmationPort;
|
|
40
49
|
};
|
|
41
50
|
acls: AclStore;
|
|
42
51
|
lifecycle: Pick<LifecycleService, 'stateOf' | 'canRead'>;
|
package/dist/http.js
CHANGED
|
@@ -7,6 +7,10 @@ import { createGuardedPublicationService } from './server/publication.js';
|
|
|
7
7
|
import { CommitQuarantineError } from './server/commit/types.js';
|
|
8
8
|
import { ArtifactOperationError, classifyArtifactFailure } from './server/failures.js';
|
|
9
9
|
import { projectArtifactHttpOutcome } from './server/outcome-projection.js';
|
|
10
|
+
import { readArtifactSource } from './server/source-read.js';
|
|
11
|
+
import { artifactSummary } from './server/summary.js';
|
|
12
|
+
import { manageArtifact } from './server/management.js';
|
|
13
|
+
import { createPostgresManagementUnit } from './adapters/postgres-management.js';
|
|
10
14
|
export { projectArtifactHttpOutcome } from './server/outcome-projection.js';
|
|
11
15
|
/** Stable content-route error codes; private driver messages never leave the host. */
|
|
12
16
|
export function errorBody(status) {
|
|
@@ -24,7 +28,8 @@ function object(value) {
|
|
|
24
28
|
/** Other mutation surfaces need their own qualification before host activation. */
|
|
25
29
|
export function assertArtifactsTransportProfile(value) {
|
|
26
30
|
assertCompiledArtifactsProfile(value);
|
|
27
|
-
const supported = ['artifacts_ensure', 'artifacts_publish', 'artifacts_list', 'artifacts_get'
|
|
31
|
+
const supported = ['artifacts_ensure', 'artifacts_publish', 'artifacts_list', 'artifacts_get',
|
|
32
|
+
'artifacts_get_source', 'artifacts_set_access', 'artifacts_delete'];
|
|
28
33
|
if (value.selectedCapabilities.some((name) => !supported.includes(name))) {
|
|
29
34
|
throw new Error('unsupported artifacts transport capability');
|
|
30
35
|
}
|
|
@@ -40,13 +45,25 @@ export function createHttpArtifactApp(deps) {
|
|
|
40
45
|
if (typeof deps.legacyRecords?.get !== 'function')
|
|
41
46
|
throw new Error('artifacts requires a legacy record inventory');
|
|
42
47
|
const profile = deps.profile;
|
|
48
|
+
if (profile.selectedCapabilities.includes('artifacts_get_source') &&
|
|
49
|
+
(!deps.sources || !deps.publication.getRelease))
|
|
50
|
+
throw new Error('artifact source read ports required');
|
|
51
|
+
if (profile.selectedCapabilities.includes('artifacts_delete') && !deps.management?.confirmation) {
|
|
52
|
+
throw new Error('artifact delete confirmation port required');
|
|
53
|
+
}
|
|
43
54
|
const ensure = createGuardedEnsureService({ profile, coordinator: deps.coordinator,
|
|
44
55
|
derivation: deps.derivation, host: deps.ensureHost });
|
|
45
|
-
async function visible(artifactId, invocation) {
|
|
56
|
+
async function visible(artifactId, invocation, allowTerminalOwner = false) {
|
|
46
57
|
const acl = await deps.acls.getArtifactAcl(artifactId);
|
|
47
58
|
if (!acl || acl.storageScopeId !== invocation.storageScopeId)
|
|
48
59
|
throw new ArtifactOperationError('not_found');
|
|
49
60
|
const state = await deps.lifecycle.stateOf(artifactId);
|
|
61
|
+
if (acl.ownerId === invocation.actorId && (state === 'deleted' || state === 'purged')) {
|
|
62
|
+
// Management must still authorize terminal owners before replaying a receipt.
|
|
63
|
+
if (allowTerminalOwner)
|
|
64
|
+
return acl;
|
|
65
|
+
throw new ArtifactOperationError('gone');
|
|
66
|
+
}
|
|
50
67
|
if (!deps.lifecycle.canRead({ artifactId, state }, acl, invocation.actorId, 'read', deps.isApprovedAccount)) {
|
|
51
68
|
throw new ArtifactOperationError('not_found');
|
|
52
69
|
}
|
|
@@ -93,9 +110,9 @@ export function createHttpArtifactApp(deps) {
|
|
|
93
110
|
}
|
|
94
111
|
}
|
|
95
112
|
const normalized = profile.validate(capability, args, request.idempotencyKey);
|
|
96
|
-
const command =
|
|
113
|
+
const command = ['artifacts_ensure', 'artifacts_publish', 'artifacts_set_access', 'artifacts_delete'].includes(capability)
|
|
97
114
|
? createArtifactCommand(profile, capability, args, invocation, deps.derivation, request.idempotencyKey) : undefined;
|
|
98
|
-
if (command) {
|
|
115
|
+
if (command && ['ensure', 'publish'].includes(command.operation)) {
|
|
99
116
|
phase = 'migration';
|
|
100
117
|
await assertArtifactReplayMigration(deps.legacyRecords, command, invocation, normalized.idempotencyKey);
|
|
101
118
|
}
|
|
@@ -103,8 +120,39 @@ export function createHttpArtifactApp(deps) {
|
|
|
103
120
|
if (capability === 'artifacts_ensure') {
|
|
104
121
|
const outcome = await ensure.ensure(invocation, { ...normalized,
|
|
105
122
|
envelopeKey: request.idempotencyKey });
|
|
123
|
+
if (outcome.ok) {
|
|
124
|
+
const state = await deps.lifecycle.stateOf(outcome.result.artifactId);
|
|
125
|
+
if (state === 'deleted' || state === 'purged')
|
|
126
|
+
throw new ArtifactOperationError('gone');
|
|
127
|
+
}
|
|
106
128
|
return outcome.ok ? { ...outcome, result: { status: 201, body: outcome.result } } : outcome;
|
|
107
129
|
}
|
|
130
|
+
if (capability === 'artifacts_get_source') {
|
|
131
|
+
const artifactId = normalized.artifactId;
|
|
132
|
+
const result = await readArtifactSource({
|
|
133
|
+
storageScopeId: invocation.storageScopeId,
|
|
134
|
+
authorize: async () => {
|
|
135
|
+
const acl = await visible(artifactId, invocation);
|
|
136
|
+
const state = await deps.lifecycle.stateOf(artifactId);
|
|
137
|
+
if (!deps.lifecycle.canRead({ artifactId, state }, acl, invocation.actorId, 'source', deps.isApprovedAccount)) {
|
|
138
|
+
throw new ArtifactOperationError('access_denied');
|
|
139
|
+
}
|
|
140
|
+
}, getCurrent: deps.publication.getCurrent, getRelease: deps.publication.getRelease,
|
|
141
|
+
getSource: (id) => deps.sources.getSource(id), getBlob: (key) => deps.publication.blobs.getBlob(key),
|
|
142
|
+
urls: deps.publicationUrls, maxFiles: 32, maxTotalBytes: 262144,
|
|
143
|
+
}, normalized);
|
|
144
|
+
return { ok: true, result: { status: 200, body: result }, invocationId: invocation.invocationId,
|
|
145
|
+
commitState: 'not_committed', replayed: false };
|
|
146
|
+
}
|
|
147
|
+
if (capability === 'artifacts_set_access' || capability === 'artifacts_delete') {
|
|
148
|
+
await visible(normalized.artifactId, invocation, true);
|
|
149
|
+
const outcome = await manageArtifact({ coordinator: deps.coordinator,
|
|
150
|
+
createUnit: deps.management?.createUnit ?? createPostgresManagementUnit,
|
|
151
|
+
confirmation: deps.management?.confirmation, readPolicyRevision: () => deps.ensureHost.readPolicyRevision(),
|
|
152
|
+
isApprovedPrincipal: deps.isApprovedAccount ?? (() => false),
|
|
153
|
+
}, invocation, command, normalized.confirmationRef);
|
|
154
|
+
return outcome.ok ? { ...outcome, result: { status: 200, body: outcome.result } } : outcome;
|
|
155
|
+
}
|
|
108
156
|
if (capability === 'artifacts_publish') {
|
|
109
157
|
const artifactId = normalized.artifactId;
|
|
110
158
|
const acl = await visible(artifactId, invocation);
|
|
@@ -146,8 +194,8 @@ export function createHttpArtifactApp(deps) {
|
|
|
146
194
|
const state = await deps.lifecycle.stateOf(artifactId);
|
|
147
195
|
const current = await deps.publication.getCurrent(artifactId);
|
|
148
196
|
return { ok: true, invocationId: invocation.invocationId, commitState: 'not_committed', replayed: false,
|
|
149
|
-
result: { status: 200, body: { artifactId,
|
|
150
|
-
|
|
197
|
+
result: { status: 200, body: { ...artifactSummary({ artifactId, acl, state, current,
|
|
198
|
+
actorId: invocation.actorId, urls: deps.publicationUrls, approved: deps.isApprovedAccount }),
|
|
151
199
|
...(acl.ownerId === invocation.actorId ? { grants: acl.grants } : {}) } } };
|
|
152
200
|
}
|
|
153
201
|
const visibleArtifacts = [];
|
|
@@ -157,7 +205,8 @@ export function createHttpArtifactApp(deps) {
|
|
|
157
205
|
continue;
|
|
158
206
|
const state = await deps.lifecycle.stateOf(artifactId);
|
|
159
207
|
if (deps.lifecycle.canRead({ artifactId, state }, acl, invocation.actorId, 'read', deps.isApprovedAccount)) {
|
|
160
|
-
visibleArtifacts.push({ artifactId,
|
|
208
|
+
visibleArtifacts.push(artifactSummary({ artifactId, acl, state, actorId: invocation.actorId,
|
|
209
|
+
current: await deps.publication.getCurrent(artifactId), urls: deps.publicationUrls, approved: deps.isApprovedAccount }));
|
|
161
210
|
}
|
|
162
211
|
}
|
|
163
212
|
const offset = normalized.offset;
|
package/dist/index.d.ts
CHANGED
|
@@ -25,3 +25,6 @@ export * from './server/reader-profile.js';
|
|
|
25
25
|
export * from './http.js';
|
|
26
26
|
export * from './server/commit-contract.js';
|
|
27
27
|
export * from './server/outcome-contract.js';
|
|
28
|
+
export * from './server/source-read.js';
|
|
29
|
+
export * from './server/management.js';
|
|
30
|
+
export * from './adapters/postgres-management.js';
|
package/dist/index.js
CHANGED
|
@@ -29,3 +29,6 @@ export * from './server/reader-profile.js';
|
|
|
29
29
|
export * from './http.js';
|
|
30
30
|
export * from './server/commit-contract.js';
|
|
31
31
|
export * from './server/outcome-contract.js';
|
|
32
|
+
export * from './server/source-read.js';
|
|
33
|
+
export * from './server/management.js';
|
|
34
|
+
export * from './adapters/postgres-management.js';
|
package/dist/mcp-core.d.ts
CHANGED
|
@@ -10,11 +10,11 @@ export { createRadarKeyDerivation } from './server/command-identity.js';
|
|
|
10
10
|
export type { ArtifactKeyDerivationBinding } from './server/command-identity.js';
|
|
11
11
|
export declare const ARTIFACTS_PROVIDER_ID = "sentropic.artifacts";
|
|
12
12
|
export declare const ARTIFACTS_APP_ID = "sentropic-artifacts";
|
|
13
|
-
export declare const ARTIFACTS_PROVIDER_VERSION = "0.2.
|
|
13
|
+
export declare const ARTIFACTS_PROVIDER_VERSION = "0.2.5";
|
|
14
14
|
export declare const ARTIFACTS_TOOL_NAMES: readonly ["artifacts_get_template", "artifacts_get_api_guide", "artifacts_ensure", "artifacts_publish", "artifacts_create_upload", "artifacts_commit_upload", "artifacts_list", "artifacts_get", "artifacts_get_source", "artifacts_fork", "artifacts_set_access", "artifacts_promote", "artifacts_renew", "artifacts_delete", "artifacts_restore"];
|
|
15
15
|
export type ArtifactsToolName = (typeof ARTIFACTS_TOOL_NAMES)[number];
|
|
16
16
|
export declare const ARTIFACTS_TOOL_SPECS: {
|
|
17
|
-
name: "artifacts_ensure" | "artifacts_publish" | "artifacts_get_template" | "artifacts_get_api_guide" | "artifacts_create_upload" | "artifacts_commit_upload" | "artifacts_list" | "artifacts_get" | "artifacts_get_source" | "artifacts_fork" | "
|
|
17
|
+
name: "artifacts_ensure" | "artifacts_publish" | "artifacts_set_access" | "artifacts_delete" | "artifacts_get_template" | "artifacts_get_api_guide" | "artifacts_create_upload" | "artifacts_commit_upload" | "artifacts_list" | "artifacts_get" | "artifacts_get_source" | "artifacts_fork" | "artifacts_promote" | "artifacts_renew" | "artifacts_restore";
|
|
18
18
|
description: string;
|
|
19
19
|
mutability: import("@sentropic/mcp-platform").Mutability;
|
|
20
20
|
}[];
|