Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ jobs:
# file" structural; the assertion below fails if it is ever lost.
- name: Assert the holder-scoped half of persona is confined to the console
run: |
for task in 'persona/attribute/put/1.0' 'persona/attribute/list/1.0' 'persona/attribute/delete/1.0' 'persona/profile/put/1.0' 'persona/profile/get/1.0' 'persona/profile/list/1.0' 'persona/profile/delete/1.0' 'persona/binding/set/1.0' 'persona/facet/put/1.0' 'persona/facet/list/1.0' 'persona/facet/delete/1.0' 'persona/correlation/analyze/1.0' 'persona/disclosure/history/1.0'; do
for task in 'persona/attribute/put/1.0' 'persona/attribute/list/1.0' 'persona/attribute/delete/1.0' 'persona/attribute/purge-version/1.0' 'persona/profile/put/1.0' 'persona/profile/get/1.0' 'persona/profile/list/1.0' 'persona/profile/delete/1.0' 'persona/binding/set/1.0' 'persona/facet/put/1.0' 'persona/facet/list/1.0' 'persona/facet/delete/1.0' 'persona/correlation/analyze/1.0' 'persona/disclosure/history/1.0'; do
leaked=$(grep -rlF "$task" packages/extension/dist/ | grep -v '^packages/extension/dist/manager\.js$' || true)
if [ -n "$leaked" ]; then
echo "::error::$leaked contains $task — this half of persona/* reads or writes the holder's attribute pool, which sits above every trust context. A wallet surface naming it is the boundary being crossed the wrong way; only the console may. Check for an import of @openvtc/pnm-core/admin from a wallet entry, or a shared chunk. See packages/core/src/admin/persona.ts."
Expand All @@ -199,13 +199,13 @@ jobs:
- name: Assert the console still carries the persona surface it is the exception for
run: |
bundle=packages/extension/dist/manager.js
for task in 'persona/attribute/put/1.0' 'persona/attribute/list/1.0' 'persona/attribute/delete/1.0' 'persona/profile/put/1.0' 'persona/profile/get/1.0' 'persona/profile/list/1.0' 'persona/profile/delete/1.0' 'persona/binding/set/1.0' 'persona/facet/put/1.0' 'persona/facet/list/1.0' 'persona/facet/delete/1.0' 'persona/correlation/analyze/1.0' 'persona/disclosure/history/1.0'; do
for task in 'persona/attribute/put/1.0' 'persona/attribute/list/1.0' 'persona/attribute/delete/1.0' 'persona/attribute/purge-version/1.0' 'persona/profile/put/1.0' 'persona/profile/get/1.0' 'persona/profile/list/1.0' 'persona/profile/delete/1.0' 'persona/binding/set/1.0' 'persona/facet/put/1.0' 'persona/facet/list/1.0' 'persona/facet/delete/1.0' 'persona/correlation/analyze/1.0' 'persona/disclosure/history/1.0'; do
if ! grep -qF "$task" "$bundle"; then
echo "::error::$bundle no longer contains $task. The guard above permits it here precisely because the console's persona pane calls it; a build without it is a pane that cannot do what it offers. If the pane genuinely dropped this task, remove it from BOTH lists in the same change."
exit 1
fi
done
echo "OK: the console carries all thirteen holder-scoped persona tasks"
echo "OK: the console carries all fourteen holder-scoped persona tasks"

# The console's isolation rests on it being one self-contained file: the
# guard above names exactly one exception, so a second chunk would be a
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@
"@swc/wasm": "~1.15.47"
},
"dependencies": {
"@openvtc/trust-tasks": "^0.19.13"
"@openvtc/trust-tasks": "^0.19.14"
}
}
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
"dependencies": {
"@cfworker/json-schema": "^4.1.1",
"@noble/curves": "^2.4.0",
"@openvtc/trust-tasks": "^0.19.13",
"@openvtc/trust-tasks": "^0.19.14",
"@openvtc/vti-didcomm-js": "^0.10.0",
"@openvtc/vti-tsp-js": "^0.3.0",
"@scure/base": "^2.2.0",
Expand Down
40 changes: 40 additions & 0 deletions packages/core/src/admin/persona.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ import {
type PersonaAttributeDeletePayload,
type PersonaAttributeDeleteResponsePayload,
} from "@openvtc/trust-tasks/persona/attribute/delete/1.0/payload";
import {
TYPE_URI as ATTRIBUTE_PURGE_VERSION,
RESPONSE_TYPE_URI as ATTRIBUTE_PURGE_VERSION_RESPONSE,
type PersonaAttributePurgeVersionPayload,
type PersonaAttributePurgeVersionResponsePayload,
} from "@openvtc/trust-tasks/persona/attribute/purge-version/1.0/payload";
import {
TYPE_URI as PROFILE_PUT,
RESPONSE_TYPE_URI as PROFILE_PUT_RESPONSE,
Expand Down Expand Up @@ -418,6 +424,40 @@ export async function personaAttributeDelete(
);
}

export interface AttributePurgeVersionParams extends PersonaHolderParams {
attributeId: string;
/** Kept versions to remove. Omit to remove every one. The current value is
* never removed here — the agent refuses it with `currentVersion`. */
versions?: NonNullable<PersonaAttributePurgeVersionPayload["versions"]>;
}

/**
* Permanently remove earlier versions of an attribute that the agent kept
* because a face pins them — an old name after a name change.
*
* The holder's override on retention. The faces that pinned a removed version
* come back in `stalePins`: they now show **nothing** for that entry, never the
* current value, because a pin exists so a counterparty is not shown a value
* the holder did not choose for them. `purged` empty is a successful no-op.
*/
export async function personaAttributePurgeVersion(
sender: TrustTaskSender,
params: AttributePurgeVersionParams,
): Promise<PersonaAttributePurgeVersionResponsePayload> {
const payload: PersonaAttributePurgeVersionPayload = {
attributeId: params.attributeId,
...(params.versions !== undefined ? { versions: params.versions } : {}),
};
return holderCall<PersonaAttributePurgeVersionPayload, PersonaAttributePurgeVersionResponsePayload>(
sender,
params,
ATTRIBUTE_PURGE_VERSION,
ATTRIBUTE_PURGE_VERSION_RESPONSE,
"persona/attribute/purge-version/1.0",
payload,
);
}

// ── Profiles ────────────────────────────────────────────────────────────────

/** Every profile the holder has — **to the end of the listing**, like
Expand Down
76 changes: 75 additions & 1 deletion packages/core/task-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$comment": "Generated by scripts/sync-task-surface.mjs from a vta-sdk checkout. Do not hand-edit: re-run the script. Checked by tests/task-surface.mjs.",
"source": {
"crate": "vta-sdk",
"version": "0.38.0",
"version": "0.45.1",
"scanned": "vta-sdk/src/**/*.rs"
},
"tasks": [
Expand Down Expand Up @@ -532,6 +532,12 @@
"TASK_PERSONA_ATTRIBUTE_LIST_1_0"
]
},
{
"uri": "https://trusttasks.org/spec/persona/attribute/purge-version/1.0",
"consts": [
"TASK_PERSONA_ATTRIBUTE_PURGE_VERSION_1_0"
]
},
{
"uri": "https://trusttasks.org/spec/persona/attribute/put/1.0",
"consts": [
Expand Down Expand Up @@ -1125,18 +1131,48 @@
"TASK_BACKUP_FINALIZE_IMPORT_1_0"
]
},
{
"uri": "https://trusttasks.org/spec/vta/backup/finalize-import/1.1",
"consts": [
"TASK_BACKUP_FINALIZE_IMPORT_1_1"
]
},
{
"uri": "https://trusttasks.org/spec/vta/backup/get-chunk/1.0",
"consts": [
"TASK_BACKUP_GET_CHUNK_1_0"
]
},
{
"uri": "https://trusttasks.org/spec/vta/backup/initiate-export/1.0",
"consts": [
"TASK_BACKUP_INITIATE_EXPORT_1_0"
]
},
{
"uri": "https://trusttasks.org/spec/vta/backup/initiate-export/1.1",
"consts": [
"TASK_BACKUP_INITIATE_EXPORT_1_1"
]
},
{
"uri": "https://trusttasks.org/spec/vta/backup/initiate-import/1.0",
"consts": [
"TASK_BACKUP_INITIATE_IMPORT_1_0"
]
},
{
"uri": "https://trusttasks.org/spec/vta/backup/initiate-import/1.1",
"consts": [
"TASK_BACKUP_INITIATE_IMPORT_1_1"
]
},
{
"uri": "https://trusttasks.org/spec/vta/backup/put-chunk/1.0",
"consts": [
"TASK_BACKUP_PUT_CHUNK_1_0"
]
},
{
"uri": "https://trusttasks.org/spec/vta/contexts/create/1.0",
"consts": [
Expand Down Expand Up @@ -1209,6 +1245,12 @@
"TASK_DID_TEMPLATES_CREATE_2_0"
]
},
{
"uri": "https://trusttasks.org/spec/vta/did-templates/create/3.0",
"consts": [
"TASK_DID_TEMPLATES_CREATE_3_0"
]
},
{
"uri": "https://trusttasks.org/spec/vta/did-templates/delete/2.0",
"consts": [
Expand All @@ -1221,12 +1263,24 @@
"TASK_DID_TEMPLATES_GET_2_0"
]
},
{
"uri": "https://trusttasks.org/spec/vta/did-templates/get/3.0",
"consts": [
"TASK_DID_TEMPLATES_GET_3_0"
]
},
{
"uri": "https://trusttasks.org/spec/vta/did-templates/list/2.0",
"consts": [
"TASK_DID_TEMPLATES_LIST_2_0"
]
},
{
"uri": "https://trusttasks.org/spec/vta/did-templates/list/3.0",
"consts": [
"TASK_DID_TEMPLATES_LIST_3_0"
]
},
{
"uri": "https://trusttasks.org/spec/vta/did-templates/render/2.0",
"consts": [
Expand All @@ -1239,6 +1293,12 @@
"TASK_DID_TEMPLATES_UPDATE_2_0"
]
},
{
"uri": "https://trusttasks.org/spec/vta/did-templates/update/3.0",
"consts": [
"TASK_DID_TEMPLATES_UPDATE_3_0"
]
},
{
"uri": "https://trusttasks.org/spec/vta/management/reload-services/1.0",
"consts": [
Expand Down Expand Up @@ -1503,6 +1563,20 @@
"TYPE"
]
},
{
"uri": "https://trusttasks.org/spec/vtc/join-requests/supplement/0.1",
"consts": [
"JOIN_REQUEST_SUPPLEMENT_RESPONSE_TYPE",
"JOIN_REQUEST_SUPPLEMENT_TYPE"
]
},
{
"uri": "https://trusttasks.org/spec/vtc/join-requests/withdraw/0.1",
"consts": [
"JOIN_REQUEST_WITHDRAW_RESPONSE_TYPE",
"JOIN_REQUEST_WITHDRAW_TYPE"
]
},
{
"uri": "https://trusttasks.org/spec/vtc/members/removal-notice/0.1",
"consts": [
Expand Down
15 changes: 15 additions & 0 deletions packages/core/tests/admin.persona.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
personaAttributeList,
personaAttributePut,
personaAttributeDelete,
personaAttributePurgeVersion,
personaProfileList,
personaProfileGet,
personaProfilePut,
Expand Down Expand Up @@ -68,6 +69,12 @@ test("every task names its 1.0 URI, request and response", async () => {
"persona/attribute/delete/1.0",
{ attributeId: "01J", existed: true },
],
[
personaAttributePurgeVersion,
{ ...PARTIES, attributeId: "01J", versions: [3] },
"persona/attribute/purge-version/1.0",
{ attributeId: "01J", purged: [3] },
],
[personaProfileList, { ...PARTIES }, "persona/profile/list/1.0", { profiles: [] }],
[
personaProfileGet,
Expand Down Expand Up @@ -605,3 +612,11 @@ test("deleting a facet sends only the facet — there is no cascade to send", as
assert.deepEqual(Object.keys(payload).sort(), ["facetId"]);
assert.equal(res.releasedFaces, 2, "the released-face count is what a screen reads");
});

test("purging every kept version sends no versions member at all", async () => {
// Omitted is "all of them"; an empty array would be a schema violation, and
// a client that sent one for "all" would be refused for doing the obvious.
const all = recorder({ attributeId: "01J", purged: [3, 5] });
await personaAttributePurgeVersion(all, { ...PARTIES, attributeId: "01J" });
assert.deepEqual(all.sent[0].envelope.payload, { attributeId: "01J" });
});
7 changes: 6 additions & 1 deletion packages/core/tests/task-surface.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,12 @@ test("coverage against the agent's surface is recorded, not discovered", () => {
// resolve them. So this is not the agent growing seven families — it is a
// snapshot that had been under-counting the denominator, and with it the gap
// this number exists to keep reviewable.
const expected = 205;
//
// 205 → 206 is `persona/attribute/purge-version/1.0` — removing an earlier
// value the agent kept for a pinned face (dtgwg-trust-tasks-tf#538). The
// canonical total moves 235 → 240 with the snapshot resynced from vta-sdk
// 0.45.1; the other four are families the agent gained since, not this one.
const expected = 206;
assert.equal(
implemented.size,
expected,
Expand Down
52 changes: 52 additions & 0 deletions packages/extension/src/manager/kept-versions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Earlier values the agent keeps because a face pins them.
//
// After a name change, a face pinned to the old version — the bank that
// verified the old name — keeps showing it, so the agent keeps that version.
// `Attribute.retainedVersions` says which versions are kept and which faces are
// the reason. A holder who overwrote a value may reasonably believe it gone;
// this is where the console tells them it is not, and offers to remove it.
//
// Removing it does not make those faces show the new value: a pin exists so a
// counterparty is not shown a value the holder did not choose for them. They
// show nothing for that entry until the holder repins or edits them. The words
// here say that, because "remove the old name" reads like "update the bank".

import type { PoolAttribute } from "@openvtc/pnm-core/admin";

export interface KeptVersion {
version: number;
/** The faces pinning it, by the holder's own name where the console has one. */
faces: string[];
}

/** What the agent keeps of this attribute beyond its current value. */
export function keptVersions(
attribute: PoolAttribute,
profiles: readonly { profileId: string; name: string }[],
): KeptVersion[] {
return (attribute.retainedVersions ?? []).map((k) => ({
version: k.version,
faces: k.pinnedBy.map((id) => profiles.find((p) => p.profileId === id)?.name ?? "a face"),
}));
}

/** One line for the strip, or `null` when nothing is kept. */
export function keptWords(kept: readonly KeptVersion[]): string | null {
if (kept.length === 0) return null;
const faces = [...new Set(kept.flatMap((k) => k.faces))];
const who = faces.length === 1 ? faces[0] : `${faces.slice(0, -1).join(", ")} and ${faces.at(-1)}`;
return kept.length === 1
? `An earlier value is still kept, because ${who} ${faces.length === 1 ? "is" : "are"} pinned to it.`
: `${kept.length} earlier values are still kept, because ${who} ${faces.length === 1 ? "is" : "are"} pinned to them.`;
}

/** What removing them does to the faces that pinned them. */
export function purgeConsequence(kept: readonly KeptVersion[]): string {
const faces = [...new Set(kept.flatMap((k) => k.faces))];
return (
`${faces.join(", ")} will show nothing for this afterwards — not the current value, ` +
`which you did not choose for ${faces.length === 1 ? "it" : "them"}. Repin or edit ` +
`${faces.length === 1 ? "it" : "them"} if that is not what you want. Anyone already shown ` +
`the old value keeps it.`
);
}
Loading
Loading